diff --git a/.changeset/gold-cows-travel.md b/.changeset/gold-cows-travel.md new file mode 100644 index 000000000..746c23d11 --- /dev/null +++ b/.changeset/gold-cows-travel.md @@ -0,0 +1,5 @@ +--- +'@modern-kit/react': patch +--- + +fix(react): useScrollTo align 옵션 추가 - @ssi02014 diff --git a/docs/docs/react/hooks/useScrollTo.mdx b/docs/docs/react/hooks/useScrollTo.mdx index 4c279d5ac..831c767bd 100644 --- a/docs/docs/react/hooks/useScrollTo.mdx +++ b/docs/docs/react/hooks/useScrollTo.mdx @@ -31,6 +31,7 @@ const { containerRef } = useScrollTo(); ## Interface ```ts title="typescript" type ScrollBehavior = "auto" | "instant" | "smooth"; +type Align = 'start' | 'center' | 'end'; interface ScrollOptions { behavior?: ScrollBehavior; // default: 'auto' @@ -45,6 +46,8 @@ interface ScrollToElementOptions { offsetX?: number; // default: 0 offsetY?: number; // default: 0 behavior?: ScrollBehavior; // default: 'auto' + alignY?: Align; // default: 'start' + alignX?: Align; // default: 'start' } ``` ```ts title="typescript" @@ -80,40 +83,72 @@ const Example = () => { scrollToPosition({ behavior: 'smooth', top: 400, - }) - } - + }); + }; + const handleScrollToElement = () => { scrollToElement(targetRef.current, { behavior: 'smooth', }); }; - const handleScrollToElementOffset = () => { + const handleScrollToElementOffset = (offset: number) => { scrollToElement(targetRef.current, { behavior: 'smooth', - offsetY: 200, + offsetY: offset, + }); + }; + + const handleScrollToElementAlign = (type: Align) => { + scrollToElement(targetRef.current, { + behavior: 'smooth', + alignY: type, }); }; return ( - <> - - - +
+
+ + + + +
-
-
InnerBox1 -
InnerBox2 +
+ + + +
-
InnerBox3 +
+
InnerBox1
+
InnerBox2
+
+ target Box +
InnerBox4
+
InnerBox4
- +
); }; + ``` ## Example + export const Example = () => { const { containerRef, scrollToPosition, scrollToElement } = useScrollTo(); const targetRef = useRef(null); @@ -128,26 +163,57 @@ export const Example = () => { behavior: 'smooth', }); }; - const handleScrollToElementOffset = () => { + const handleScrollToElementOffset = (offset) => { scrollToElement(targetRef.current, { behavior: 'smooth', - offsetY: 200, + offsetY: offset, + }); + }; + const handleScrollToElementAlign = (type) => { + scrollToElement(targetRef.current, { + behavior: 'smooth', + alignY: type, }); }; return ( - <> - - - +
+
+ + + + +
-
+
+ + + +
+ +
InnerBox1
InnerBox2
-
InnerBox3
+
+ target Box +
InnerBox4
+
InnerBox4
- +
); }; + diff --git a/packages/react/src/hooks/useScrollTo/index.ts b/packages/react/src/hooks/useScrollTo/index.ts index a820d218a..7ed9e05e6 100644 --- a/packages/react/src/hooks/useScrollTo/index.ts +++ b/packages/react/src/hooks/useScrollTo/index.ts @@ -16,7 +16,7 @@ import { ScrollToElementOptions, getRelativePosition } from './internal'; * @example * // scrollToElement를 사용하면 인자로 넣은 엘리먼트를 기준으로 이동합니다. * const { containerRef, scrollToElement } = useScrollTo(); - * scrollToElement(target.current, { offsetX, offsetY }); + * scrollToElement(target.current, { offsetX, offsetY, alignY, alignX, behavior }); */ export function useScrollTo(): { containerRef: React.MutableRefObject; @@ -61,7 +61,7 @@ export function useScrollTo() { target: E, scrollToElementOptions: ScrollToElementOptions = {} ) => { - if (!containerRef.current) return; + if (!target || !containerRef.current) return; const scrollElement = containerRef.current; const { behavior = 'auto' } = scrollToElementOptions; diff --git a/packages/react/src/hooks/useScrollTo/internal.ts b/packages/react/src/hooks/useScrollTo/internal.ts index 1a5944ccc..1fa8053ce 100644 --- a/packages/react/src/hooks/useScrollTo/internal.ts +++ b/packages/react/src/hooks/useScrollTo/internal.ts @@ -1,32 +1,73 @@ import { isWindow } from '@modern-kit/utils'; +type Align = 'start' | 'center' | 'end'; + export interface ScrollToElementOptions { offsetX?: number; offsetY?: number; behavior?: ScrollBehavior; + alignY?: Align; // 수직 + alignX?: Align; // 수평 } +const getAlignPosition = ( + containerDimension: number, + targetDimension: number, + align: Align +) => { + if (align === 'start') { + return 0; + } + if (align === 'center') { + return -containerDimension / 2 + targetDimension / 2; + } + return -containerDimension + targetDimension; +}; + /** * @description parent가 window인지 특정 요소인지에 child의 위치 값을 가져오는 함수 */ export const getRelativePosition = ( - parent: T | Window, - child: HTMLElement, + container: T | Window, + target: HTMLElement, scrollToOptions: ScrollToElementOptions ) => { - const childRect = child.getBoundingClientRect(); - const { offsetX = 0, offsetY = 0 } = scrollToOptions; + const targetRect = target.getBoundingClientRect(); + const { + offsetX = 0, + offsetY = 0, + alignY = 'start', + alignX = 'start', + } = scrollToOptions; - if (isWindow(parent)) { + if (isWindow(container)) { return { - top: childRect.top + window.scrollY + offsetY, - left: childRect.left + window.scrollX + offsetX, + top: + targetRect.top + + window.scrollY + + offsetY + + getAlignPosition(window.innerHeight, targetRect.height, alignY), + left: + targetRect.left + + window.scrollX + + offsetX + + getAlignPosition(window.innerWidth, targetRect.width, alignX), }; } - const parentRect = parent.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); return { - top: childRect.top - parentRect.top + parent.scrollTop + offsetY, - left: childRect.left - parentRect.left + parent.scrollLeft + offsetX, + top: + targetRect.top - + containerRect.top + + container.scrollTop + + offsetY + + getAlignPosition(containerRect.height, targetRect.height, alignY), + left: + targetRect.left - + containerRect.left + + container.scrollLeft + + offsetX + + getAlignPosition(containerRect.width, targetRect.width, alignX), }; }; diff --git a/packages/react/vitest.config.ts b/packages/react/vitest.config.ts index eedfd4f43..9e7bb2c2d 100644 --- a/packages/react/vitest.config.ts +++ b/packages/react/vitest.config.ts @@ -11,7 +11,11 @@ export default defineConfig({ setupFiles: './vitest.setup.ts', coverage: { provider: 'istanbul', - exclude: ['src/utils/test/**', 'src/hooks/useClipboard'], + exclude: [ + 'src/utils/test/**', + 'src/**/internal.ts', + 'src/hooks/useClipboard', + ], }, }, });