Skip to content

Commit

Permalink
fix: InView 컴포넌트 개선
Browse files Browse the repository at this point in the history
  • Loading branch information
ssi02014 committed Nov 26, 2024
1 parent 93d7e8b commit aade6bf
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 15 deletions.
62 changes: 53 additions & 9 deletions docs/docs/react/components/InView.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { InView } from '@modern-kit/react';

`InView``IntersectionObserver`를 선언적으로 활용 할 수 있는 컴포넌트입니다.

`@modern-kit/react`**[useIntersectionObserver](https://modern-agile-team.github.io/modern-kit/docs/react/hooks/useIntersectionObserver)** 훅을 사용하여 구현되었습니다.

`Viewport`에 노출될 때(`onIntersectStart`) 혹은 나갈 때(`onIntersectEnd`) 특정 action 함수를 호출 할 수 있는 컴포넌트입니다.
관찰 대상이 `Viewport`에 노출될 때(`onIntersectStart`) 혹은 나갈 때(`onIntersectEnd`) 특정 action 함수를 호출 할 수 있는 컴포넌트입니다.

`calledOnce`를 활용하면 `onIntersectStart``onIntersectEnd`를 각각 한번씩 호출 할 수 있습니다.
`asChild`를 활용하면 자식 요소를 그대로 렌더링하고, 자식 요소를 관찰 대상으로 설정할 수 있습니다. 이때 자식 요소는 단일 요소만 허용됩니다.
기본 값은 `false`이며, `false`일 경우 `div`로 감싸지며, 해당 `div`를 관찰 대상으로 설정합니다.

Intersection Observer Option을 설정할 수 있습니다.(하단 `Note` 참고)
`@modern-kit/react`**[useIntersectionObserver](https://modern-agile-team.github.io/modern-kit/docs/react/hooks/useIntersectionObserver)** 훅을 사용하여 구현되었습니다.

<br />

Expand All @@ -20,14 +19,20 @@ Intersection Observer Option을 설정할 수 있습니다.(하단 `Note` 참고
## Interface
```ts title="typescript"
interface InViewProps extends UseIntersectionObserverProps {
children: JSX.Element;
children: React.ReactNode;
asChild?: boolean;
}
```
```ts title="typescript"
const InView: ({ children, ...props }: InViewProps) => JSX.Element
const InView: React.ForwardRefExoticComponent<
InViewProps & React.RefAttributes<HTMLElement>
>
```
## Usage
### Default
- 기본적으로 `div`로 감싸지며, 해당 `div`를 관찰 대상으로 설정합니다.
- 해당 `div`가 viewport에 노출되거나 숨겨질 때 `onIntersectStart/onIntersectEnd` 콜백 함수를 호출합니다.
```tsx title="typescript"
import { InView } from '@modern-kit/react';

Expand All @@ -45,9 +50,48 @@ const Example = () => {
{/* ... */}
<InView
onIntersectStart={handleIntersectStart}
onIntersectStart={handleIntersectEnd}
calledOnce>
onIntersectEnd={handleIntersectEnd}
calledOnce
>
<div>Box1</div>
<div>Box2</div>
</InView>
</div>;
);
};
```

### asChild
- 자식 요소를 그대로 렌더링하고, 해당 요소를 관찰 대상으로 설정합니다.
- 자식 요소가 viewport에 노출되거나 숨겨질 때 `onIntersectStart/onIntersectEnd` 콜백 함수를 호출합니다.
- 이때 자식 요소는 단일 요소만 허용됩니다.
```tsx title="typescript"
import { InView } from '@modern-kit/react';

const Example = () => {
const ref = useRef<HTMLDivElement>(null);

const handleIntersectStart = () => {
/* action */
}

const handleIntersectEnd = () => {
/* action */
}

return (
<div>
{/* ... */}
<InView
onIntersectStart={handleIntersectStart}
onIntersectEnd={handleIntersectEnd}
calledOnce
asChild
>
<div ref={ref} style={{ width: '100px', height: '100px', background: '#c0392B' }}>
<p>Content1</p>
<p>Content2</p>
</div>
</InView>
</div>;
);
Expand Down
51 changes: 50 additions & 1 deletion packages/react/src/components/InView/InView.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ interface TestComponentProps {
onIntersectStart: () => void;
onIntersectEnd: () => void;
calledOnce?: boolean;
asChild?: boolean;
}

const TestComponent = ({
onIntersectStart,
onIntersectEnd,
calledOnce,
asChild,
}: TestComponentProps) => {
return (
<InView
asChild={asChild}
onIntersectStart={onIntersectStart}
onIntersectEnd={onIntersectEnd}
calledOnce={calledOnce}>
Expand All @@ -49,14 +52,47 @@ describe('InView', () => {
/>
);

const box = screen.getByText('box');
const boxWrapper = screen.getByText('box').parentElement as HTMLElement;

expect(intersectStartMock).toBeCalledTimes(0);
expect(intersectEndMock).toBeCalledTimes(0);

await waitFor(() =>
mockIntersecting({ type: 'view', element: boxWrapper })
);
expect(intersectStartMock).toBeCalledTimes(1);

await waitFor(() =>
mockIntersecting({ type: 'hide', element: boxWrapper })
);
expect(intersectEndMock).toBeCalledTimes(1);
});

it('asChild 프로퍼티가 true이면 자식 요소가 그대로 렌더링되야 하며, 자식 요소를 관찰 대상으로 설정해야 합니다.', async () => {
renderSetup(
<TestComponent
onIntersectStart={intersectStartMock}
onIntersectEnd={intersectEndMock}
asChild={true}
/>
);

const boxWrapper = screen.getByText('box').parentElement as HTMLElement;
const box = screen.getByText('box');

await waitFor(() =>
mockIntersecting({ type: 'view', element: boxWrapper })
);
expect(intersectStartMock).toBeCalledTimes(0);

await waitFor(() => mockIntersecting({ type: 'view', element: box }));
expect(intersectStartMock).toBeCalledTimes(1);

await waitFor(() =>
mockIntersecting({ type: 'hide', element: boxWrapper })
);
expect(intersectEndMock).toBeCalledTimes(0);

await waitFor(() => mockIntersecting({ type: 'hide', element: box }));
expect(intersectEndMock).toBeCalledTimes(1);
});
Expand Down Expand Up @@ -85,4 +121,17 @@ describe('InView', () => {
expect(intersectStartMock).toBeCalledTimes(1);
expect(intersectEndMock).toBeCalledTimes(1);
});

it('asChild 프로퍼티가 true일 경우 자식 요소로 단일 요소가 아닐 경우 에러가 발생합니다.', () => {
expect(() =>
renderSetup(
<InView asChild={true}>
<div>box1</div>
<div>box2</div>
</InView>
)
).toThrow(
'InView 컴포넌트는 asChild 프로퍼티가 true일 경우 자식으로 단일 요소만 허용합니다.'
);
});
});
56 changes: 51 additions & 5 deletions packages/react/src/components/InView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,78 @@
import React, { Children } from 'react';
import { Slot } from '../Slot';
import {
useIntersectionObserver,
UseIntersectionObserverProps,
} from '../../hooks/useIntersectionObserver';

interface InViewProps extends UseIntersectionObserverProps {
children: JSX.Element;
children: React.ReactNode;
asChild?: boolean;
}

/**
* @description `InView`는 `IntersectionObserver`를 선언적으로 활용 할 수 있는 컴포넌트입니다.
*
* 관찰 대상이 `viewport`에 노출될 때(`onIntersectStart`) 혹은 나갈 때(`onIntersectEnd`) 특정 action 함수를 호출 할 수 있는 컴포넌트입니다.
*
* `@modern-kit/react`의 `useIntersectionObserver` 훅을 사용하여 구현되었습니다.
*
* @see https://modern-agile-team.github.io/modern-kit/docs/react/hooks/useIntersectionObserver
*
* @param {InViewProps} props - 컴포넌트에 전달되는 속성들입니다.
* @param {JSX.Element} props.children - 관찰할 자식 요소입니다.
* @param {React.ReactNode} props.children - 관찰할 자식 요소입니다.
* @param {boolean} props.asChild - 자식 요소를 그대로 렌더링할지 여부를 나타냅니다. `true`일 경우 자식 요소가 그대로 렌더링되며, 자식 요소가 관찰 대상이됩니다.
* @param {(entry: IntersectionObserverEntry) => void} props.onIntersectStart - 타겟 요소가 viewport 내에 들어갈 때 호출되는 콜백 함수입니다.
* @param {(entry: IntersectionObserverEntry) => void} props.onIntersectEnd - 타겟 요소가 viewport에서 나갈 때 호출되는 콜백 함수입니다.
* @param {number | number[]} props.threshold - 관찰을 시작할 viewport의 가시성 비율입니다.
* @param {Element | Document | null} props.root - 교차할 때 기준이 되는 root 요소입니다. 기본값은 `null`이며 이는 viewport를 의미합니다.
* @param {string} props.rootMargin - 루트 요소에 대한 마진을 지정합니다. 이는 뷰포트 또는 루트 요소의 경계를 확장하거나 축소하는데 사용됩니다.
* @param {boolean} props.enabled - Observer를 활성화할지 여부를 나타냅니다. `false`일 경우 Observer가 작동하지 않습니다.
* @param {boolean} props.calledOnce - 요소가 교차할 때 콜백을 `한 번`만 호출할지 여부를 나타냅니다.
*
* @returns {JSX.Element}
*
* @example
* ```tsx
* // 기본적으로 div로 감싸지며, 해당 div를 관찰 대상으로 설정합니다.
* // 해당 div가 viewport에 노출되거나 숨겨질 때 onIntersectStart/onIntersectEnd 콜백 함수를 호출합니다.
* <InView onIntersectStart={onIntersectStart} onIntersectEnd={onIntersectEnd}>
* <div>Content1</div>
* <div>Content2</div>
* </InView>
* ```
*
* @example
* ```tsx
* // asChild 프로퍼티를 사용하면 자식 요소를 그대로 렌더링하고, 자식 요소를 관찰 대상으로 설정합니다.
* // 자식 요소가 viewport에 노출되거나 숨겨질 때 onIntersectStart/onIntersectEnd 콜백이 호출됩니다.
* // 이때 자식 요소는 단일 요소만 허용됩니다.
* const ref = useRef<HTMLDivElement>(null);
*
* <InView asChild onIntersectStart={onIntersectStart} onIntersectEnd={onIntersectEnd}>
* <div ref={ref} style={style}>
* <span>Content1</span>
* <span>Content2</span>
* </div>
* </InView>
* ```
*/
export const InView = ({ children, ...props }: InViewProps) => {
const { ref: intersectionObserverRef } = useIntersectionObserver(props);
export const InView = ({
children,
asChild = false,
...props
}: InViewProps): JSX.Element => {
const InViewWrapper = asChild ? Slot : 'div';
const { ref: intersectionObserverRef } =
useIntersectionObserver<HTMLElement>(props);
const childrenCount = Children.count(children);

return <Slot ref={intersectionObserverRef}>{children}</Slot>;
if (asChild && childrenCount > 1) {
throw new Error(
'InView 컴포넌트는 asChild 프로퍼티가 true일 경우 자식으로 단일 요소만 허용합니다.'
);
}
return (
<InViewWrapper ref={intersectionObserverRef}>{children}</InViewWrapper>
);
};

0 comments on commit aade6bf

Please sign in to comment.