Skip to content

Commit

Permalink
fix: inView 로직 개선2
Browse files Browse the repository at this point in the history
  • Loading branch information
ssi02014 committed Nov 26, 2024
1 parent da6afc5 commit 0d308ef
Show file tree
Hide file tree
Showing 3 changed files with 15 additions and 129 deletions.
48 changes: 1 addition & 47 deletions docs/docs/react/components/InView.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { InView } from '@modern-kit/react';

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

`asChild`를 활용하면 자식 요소를 그대로 렌더링하고, 자식 요소를 관찰 대상으로 설정할 수 있습니다. 이때 자식 요소는 단일 요소만 허용됩니다.
기본 값은 `false`이며, `false`일 경우 `div`로 감싸지며, 해당 `div`를 관찰 대상으로 설정합니다.
- `ref` 활용 및 div가 아닌 특정 요소를 직접 관찰 대상으로 설정할 때 유용합니다.

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

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

Expand All @@ -50,42 +40,6 @@ const Example = () => {
<div>
<InView onIntersectStart={handleIntersectStart} onIntersectEnd={handleIntersectEnd}>
<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<HTMLUListElement>(null);

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

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

return (
<div>
<InView
asChild
onIntersectStart={handleIntersectStart}
onIntersectEnd={handleIntersectEnd}
>
<ul ref={ref} style={{ background: '#c0392B' }}>
<li>List Item1</li>
<li>List Item2</li>
</ul>
</InView>
</div>;
);
Expand Down
50 changes: 1 addition & 49 deletions packages/react/src/components/InView/InView.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ const TestComponent = ({
onIntersectStart,
onIntersectEnd,
calledOnce,
asChild,
}: TestComponentProps) => {
return (
<InView
asChild={asChild}
onIntersectStart={onIntersectStart}
onIntersectEnd={onIntersectEnd}
calledOnce={calledOnce}>
Expand All @@ -52,47 +50,14 @@ describe('InView', () => {
/>
);

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);
expect(intersectEndMock).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 @@ -121,17 +86,4 @@ 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일 경우 자식으로 단일 요소만 허용합니다.'
);
});
});
46 changes: 13 additions & 33 deletions packages/react/src/components/InView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React, { Children } from 'react';
import { Slot } from '../Slot';
import {
useIntersectionObserver,
UseIntersectionObserverProps,
} from '../../hooks/useIntersectionObserver';

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

/**
Expand All @@ -20,8 +18,7 @@ interface InViewProps extends UseIntersectionObserverProps {
* @see https://modern-agile-team.github.io/modern-kit/docs/react/hooks/useIntersectionObserver
*
* @param {InViewProps} props - 컴포넌트에 전달되는 속성들입니다.
* @param {React.ReactNode} props.children - 관찰할 자식 요소입니다.
* @param {boolean} props.asChild - 자식 요소를 그대로 렌더링할지 여부를 나타냅니다. `true`일 경우 자식 요소가 그대로 렌더링되며, 자식 요소가 관찰 대상이됩니다.
* @param {JSX.Element} props.children - 관찰 할 자식 요소입니다.
* @param {(entry: IntersectionObserverEntry) => void} props.onIntersectStart - 타겟 요소가 viewport 내에 들어갈 때 호출되는 콜백 함수입니다.
* @param {(entry: IntersectionObserverEntry) => void} props.onIntersectEnd - 타겟 요소가 viewport에서 나갈 때 호출되는 콜백 함수입니다.
* @param {number | number[]} props.threshold - 관찰을 시작할 viewport의 가시성 비율입니다.
Expand All @@ -34,44 +31,27 @@ interface InViewProps extends UseIntersectionObserverProps {
*
* @example
* ```tsx
* // 기본적으로 div로 감싸지며, 해당 div를 관찰 대상으로 설정합니다.
* // 해당 div가 viewport에 노출되거나 숨겨질 때 onIntersectStart/onIntersectEnd 콜백 함수를 호출합니다.
* // 자식 요소를 그대로 렌더링하며, 해당 자식 요소를 관찰 대상으로 설정합니다.
* // 자식 요소가 viewport에 노출되거나 숨겨질 때 onIntersectStart/onIntersectEnd 콜백 함수를 호출합니다.
* <InView onIntersectStart={onIntersectStart} onIntersectEnd={onIntersectEnd}>
* <div>Content1</div>
* <div>Content2</div>
* <p>Content1</p>
* </InView>
* ```
*
* @example
* ```tsx
* // asChild 프로퍼티를 사용하면 자식 요소를 그대로 렌더링하고, 자식 요소를 관찰 대상으로 설정합니다.
* // 자식 요소가 viewport에 노출되거나 숨겨질 때 onIntersectStart/onIntersectEnd 콜백이 호출됩니다.
* // 이때 자식 요소는 단일 요소만 허용됩니다.
* const ref = useRef<HTMLUListElement>(null);
* const ref = useRef<HTMLDivElement>(null);
*
* <InView asChild onIntersectStart={onIntersectStart} onIntersectEnd={onIntersectEnd}>
* <ul ref={ref} style={style}>
* <li>List Item1</li>
* <li>List Item2</li>
* </ul>
* <InView onIntersectStart={onIntersectStart} onIntersectEnd={onIntersectEnd}>
* <div ref={ref}>
* <p>Content1</p>
* <p>Content2</p>
* </div>
* </InView>
* ```
*/
export const InView = ({
children,
asChild = false,
...props
}: InViewProps): JSX.Element => {
const InViewWrapper = asChild ? Slot : 'div';
export const InView = ({ children, ...props }: InViewProps): JSX.Element => {
const { ref: intersectionObserverRef } = useIntersectionObserver(props);
const childrenCount = Children.count(children);

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

0 comments on commit 0d308ef

Please sign in to comment.