Skip to content

feat: useFocus 구현 #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@ You can determine the initial on/off state of the toggle through `defaultValue`.

`toggle` : Reverses the current state of the switch.

### useFocus

Executes the provided callback function when a DOM element becomes visible on the screen.

#### Parameters

`onFocusCallback`: A function to execute when the element comes into focus.
`threshold`: Determines the visibility threshold for the element. A value of 1 means the element must be fully visible within the viewport.
`rootMargin`: Adjusts the viewport’s dimensions using the format "top right bottom left", with units explicitly specified.

#### Return Value

`ref`: A ref object to assign to the DOM element that should trigger the focus callback.

## Animation

The animation of this package is based on ClassName by default.
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useDebounce from './useDebounce/useDebounce';
import useLocalStorage from './useLocalStorage/useLocalStorage';
import useDisclosure from './useDisclosure/useDisclosure';
import useModal from './useModal/useModal';
import useFocus from './useFocus/useFocus';

export {
useInput,
Expand All @@ -26,4 +27,5 @@ export {
useLocalStorage,
useDisclosure,
useModal,
useFocus,
};
22 changes: 22 additions & 0 deletions src/stories/useFocus/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Canvas, Meta, Description } from '@storybook/blocks';
import * as Focus from './Focus.stories';

<Meta of={Focus} />

# useFocus

DOM Element가 화면에 노출되었을때, callback으로 전달된 함수를 실행합니다.

## 함수인자

`onFocusCallback` : 요소가 focus되었을때, 수행할 함수입니다.

`threshold` : 요소가 화면에 보일때의 기준을 결정합니다. 1이면 전체요소가 모두 화면에 들어왔을때 입니다.

`rootMargin` : 화면의 너비, 높이를 조정할 수 있습니다. "top right bottom left" 형태로 기재하며 반드시 단위를 명시해야합니다.

## 반환값

`ref`: focus될 DOM요소에 할당해줄 ref 객체입니다.

<Canvas of={Focus.defaultStory} />
17 changes: 17 additions & 0 deletions src/stories/useFocus/Focus.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { style } from '@vanilla-extract/css';

export const backgroundDiv = style({
height: 100,
});

export const scrollDiv = style({
position: 'absolute',
backgroundColor: 'black',
color: 'white',
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
padding: 16,
top: 200,
});
21 changes: 21 additions & 0 deletions src/stories/useFocus/Focus.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Meta, StoryObj } from '@storybook/react';
import Focus from './Focus';

const meta = {
title: 'hooks/useFocus',
component: Focus,
parameters: {
layout: 'centered',
docs: {
canvas: {},
},
},
} satisfies Meta<typeof Focus>;

export default meta;

type Story = StoryObj<typeof meta>;

export const defaultStory: Story = {
args: {},
};
16 changes: 16 additions & 0 deletions src/stories/useFocus/Focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { useState } from 'react';
import useFocus from '@/useFocus/useFocus';
import { backgroundDiv, scrollDiv } from './Focus.css';

export default function Focus() {
const [message, setMessage] = useState('NOT FOCUS');
const ref = useFocus<HTMLDivElement>(() => setMessage('FOCUS!'), 1, '-10px');

return (
<div className={backgroundDiv}>
<div ref={ref} className={scrollDiv}>
{message}
</div>
</div>
);
}
57 changes: 57 additions & 0 deletions src/useFocus/useFocus.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { fireEvent, render } from '@testing-library/react';
import useFocus from './useFocus';
import React from 'react';

interface Entrie {
target: Element;
isIntersecting: boolean;
}

const mockIntersectionObserver = class {
entries: Entrie[];
constructor(callback) {
this.entries = [];
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
this.entries.map((entry) => {
entry.isIntersecting = this.isInViewPort();
});
}
callback(this.entries, this);
});
}

isInViewPort() {
return true;
}

observe(target: Element) {
this.entries.push({ isIntersecting: false, target });
}

unobserve(target) {
this.entries = this.entries.filter((ob) => ob.target !== target);
}

disconnect() {
this.entries = [];
}
};

describe('useFocus 기능 테스트', () => {
beforeEach(() => {
// eslint-disable-next-line
global.IntersectionObserver = mockIntersectionObserver as any;
});

it('focus되었을때 onFocusCallback을 실행시킬 수 있다.', async () => {
const mock = jest.fn();
const Test = () => {
const ref = useFocus<HTMLDivElement>(mock);
return <div ref={ref}>Test</div>;
};
render(<Test />);
fireEvent.scroll(window, { target: { scrollY: 100 } });
expect(mock).toHaveBeenCalled();
});
});
36 changes: 36 additions & 0 deletions src/useFocus/useFocus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useCallback, useEffect, useRef } from 'react';

export default function useFocus<T extends HTMLElement>(
onFocusCallback: (() => void) | (() => Promise<void>),
threshold?: number,
rootMargin?: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

threshold = 0.1과 같이 한다면 기본값임을 명확히 알 수 있고 밑에서는 조건이 빠져 읽기 쉬울 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다
2027151 에서 반영완료했습니다!

) {
const elementRef = useRef<T>(null);

const handleScroll: IntersectionObserverCallback = useCallback(([entry]) => {
const { current } = elementRef;
if (current) {
if (entry.isIntersecting) {
onFocusCallback();
}
}
}, []);

useEffect(() => {
let observer: IntersectionObserver;
const { current } = elementRef;

if (current) {
observer = new IntersectionObserver(handleScroll, {
threshold: threshold || 0.1,
rootMargin: rootMargin || '0px 0px 0px 0px',
});

observer.observe(current);

return () => observer && observer.disconnect();
}
}, [handleScroll]);

return elementRef;
}