diff --git a/.changepacks/changepack_log_react_18_support.json b/.changepacks/changepack_log_react_18_support.json new file mode 100644 index 000000000..ad74861ef --- /dev/null +++ b/.changepacks/changepack_log_react_18_support.json @@ -0,0 +1,8 @@ +{ + "changes": { + "packages/react/package.json": "Patch", + "packages/components/package.json": "Patch" + }, + "note": "@devup-ui/react and @devup-ui/components take React as a peer of ^18 || ^19 instead of `*` and no longer depend on React 19 themselves, so a React 18 app no longer gets a second React. Select and Stepper read their context with useContext instead of React 19's use() and provide it with Context.Provider instead of React 19's (which crashes React 18), and Select repositions its container from an effect instead of a callback ref returning cleanup, which React 18 ignores", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/bun.lock b/bun.lock index 906ce66d6..412668d4f 100644 --- a/bun.lock +++ b/bun.lock @@ -512,7 +512,6 @@ "dependencies": { "@devup-ui/react": "workspace:^", "clsx": "^2.1", - "react": "^19.3.0", }, "devDependencies": { "@devup-ui/vite-plugin": "workspace:^", @@ -522,6 +521,7 @@ "@types/react": "^19.3", "@typescript/typescript6": "^6.0", "eslint-plugin-storybook": "^10.6", + "react": "^19.3.0", "rollup-plugin-preserve-directives": "^0.4", "storybook": "^10.6", "typescript": "^7.0.2", @@ -530,7 +530,7 @@ }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "react": "*", + "react": "^18 || ^19", }, }, "packages/eslint-plugin": { @@ -580,12 +580,12 @@ "version": "1.0.44", "dependencies": { "csstype-extra": "latest", - "react": "^19.3", }, "devDependencies": { "@types/node": "^26.6", "@types/react": "^19.3", "@typescript/typescript6": "^6.0", + "react": "^19.3", "rollup-plugin-preserve-directives": "^0.4", "typescript": "^7.0.2", "vite": "^8.3", @@ -593,7 +593,7 @@ }, "peerDependencies": { "csstype-extra": "*", - "react": "*", + "react": "^18 || ^19", }, }, "packages/reset-css": { diff --git a/packages/components/package.json b/packages/components/package.json index 7572a3c1e..f45b583e0 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -44,7 +44,6 @@ "types": "./dist/index.d.ts", "dependencies": { "@devup-ui/react": "workspace:^", - "react": "^19.3.0", "clsx": "^2.1" }, "devDependencies": { @@ -59,10 +58,11 @@ "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "vite": "^8.3", - "vite-plugin-dts": "^5.1" + "vite-plugin-dts": "^5.1", + "react": "^19.3.0" }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "react": "*" + "react": "^18 || ^19" } } diff --git a/packages/components/src/components/Select/__tests__/index.browser.test.tsx b/packages/components/src/components/Select/__tests__/index.browser.test.tsx index 116572383..444e4db22 100644 --- a/packages/components/src/components/Select/__tests__/index.browser.test.tsx +++ b/packages/components/src/components/Select/__tests__/index.browser.test.tsx @@ -449,7 +449,8 @@ describe('Select', () => { // offsetWidth > 1024px Object.defineProperty(selectContainer, 'offsetWidth', { value: 1100 }) - // rerender + // the container repositions when the window resizes + window.dispatchEvent(new Event('resize')) rerender() expect(container).toMatchSnapshot() diff --git a/packages/components/src/components/Select/index.tsx b/packages/components/src/components/Select/index.tsx index 4ce1b0220..6818bb5e5 100644 --- a/packages/components/src/components/Select/index.tsx +++ b/packages/components/src/components/Select/index.tsx @@ -110,7 +110,7 @@ export function Select({ } return ( - - + ) } @@ -230,54 +230,59 @@ export function SelectContainer({ ...props }: SelectContainerProps) { const { open, setOpen, type, ref } = useSelect() + const containerRef = useRef(null) - if (!open) return null - return ( - { - if (!ref.current || !el) return - const combobox = ref.current - - // 요소가 움직일 때마다(스크롤, 리사이즈 등) 위치를 갱신하도록 이벤트를 등록합니다. - const updatePosition = () => { - const { - height, - x: comboboxX, - y: comboboxY, - top, - left, - } = combobox.getBoundingClientRect() - - const isOverflowBottom = - el.offsetHeight + top + window.scrollY + height + y > - document.documentElement.scrollHeight - - const isOverflowRight = - el.offsetWidth + left + window.scrollX + x > - document.documentElement.scrollWidth - - if (isOverflowBottom) - el.style.bottom = `${window.innerHeight - comboboxY + 10}px` - else el.style.top = `${comboboxY + height + 10 + y}px` - - if (isOverflowRight) - el.style.left = `${Math.max(comboboxX - el.offsetWidth + combobox.offsetWidth, 0) + x}px` - else el.style.left = `${comboboxX + x}px` - } + // An effect rather than a callback ref returning cleanup, which React 18 ignores + useEffect(() => { + const el = containerRef.current + if (!open || !ref.current || !el) return + const combobox = ref.current - // 최초 위치 설정 - updatePosition() + // 요소가 움직일 때마다(스크롤, 리사이즈 등) 위치를 갱신하도록 이벤트를 등록합니다. + const updatePosition = () => { + const { + height, + x: comboboxX, + y: comboboxY, + top, + left, + } = combobox.getBoundingClientRect() - // 스크롤, 리사이즈, DOM 변경 등 요소 위치가 변할 수 있는 이벤트에 리스너 등록 - window.addEventListener('scroll', updatePosition, true) - window.addEventListener('resize', updatePosition) + const isOverflowBottom = + el.offsetHeight + top + window.scrollY + height + y > + document.documentElement.scrollHeight - // 컴포넌트 언마운트 시 이벤트 해제 - return () => { - window.removeEventListener('scroll', updatePosition, true) - window.removeEventListener('resize', updatePosition) - } - }} + const isOverflowRight = + el.offsetWidth + left + window.scrollX + x > + document.documentElement.scrollWidth + + if (isOverflowBottom) + el.style.bottom = `${window.innerHeight - comboboxY + 10}px` + else el.style.top = `${comboboxY + height + 10 + y}px` + + if (isOverflowRight) + el.style.left = `${Math.max(comboboxX - el.offsetWidth + combobox.offsetWidth, 0) + x}px` + else el.style.left = `${comboboxX + x}px` + } + + // 최초 위치 설정 + updatePosition() + + // 스크롤, 리사이즈, DOM 변경 등 요소 위치가 변할 수 있는 이벤트에 리스너 등록 + window.addEventListener('scroll', updatePosition, true) + window.addEventListener('resize', updatePosition) + + // 컴포넌트 언마운트 시 이벤트 해제 + return () => { + window.removeEventListener('scroll', updatePosition, true) + window.removeEventListener('resize', updatePosition) + } + }, [open, ref, x, y]) + + if (!open) return null + return ( + (null) export const useStepper = () => { - const context = use(StepperContext) + const context = useContext(StepperContext) if (!context) { throw new Error('useStepper must be used within a StepperProvider') } @@ -58,7 +58,7 @@ function Stepper({ } return ( - {children} - + ) } diff --git a/packages/components/src/contexts/useSelect.ts b/packages/components/src/contexts/useSelect.ts index cb716bae2..da05b8aa4 100644 --- a/packages/components/src/contexts/useSelect.ts +++ b/packages/components/src/contexts/useSelect.ts @@ -1,5 +1,5 @@ 'use client' -import { createContext, use } from 'react' +import { createContext, useContext } from 'react' type SelectType = 'default' | 'radio' | 'checkbox' type SelectValue = T extends 'radio' ? string : string[] @@ -14,7 +14,7 @@ export const SelectContext = createContext<{ } | null>(null) export const useSelect = () => { - const context = use(SelectContext) + const context = useContext(SelectContext) if (!context) { throw new Error('useSelect must be used within a Select') } diff --git a/packages/react/package.json b/packages/react/package.json index a05b1c916..9a37d2af0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -66,7 +66,6 @@ ], "types": "./dist/index.d.ts", "dependencies": { - "react": "^19.3", "csstype-extra": "latest" }, "devDependencies": { @@ -76,10 +75,11 @@ "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "@types/react": "^19.3", - "@types/node": "^26.6" + "@types/node": "^26.6", + "react": "^19.3" }, "peerDependencies": { - "react": "*", + "react": "^18 || ^19", "csstype-extra": "*" } }