@x-plat/design-system 0.14.6 → 0.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7304,6 +7304,23 @@ var useFocusTrap = (containerRef, enabled, options = {}) => {
7304
7304
  return () => document.removeEventListener("keydown", onKey);
7305
7305
  }, [enabled, trap, containerRef]);
7306
7306
  };
7307
+ var useCloseOnContainerScroll = (enabled, popRef, onClose) => {
7308
+ const onCloseRef = React10.useRef(onClose);
7309
+ onCloseRef.current = onClose;
7310
+ React10.useEffect(() => {
7311
+ if (!enabled) return;
7312
+ const onScroll = (e) => {
7313
+ const target = e.target;
7314
+ if (target === document || target === document.documentElement || target === document.body) {
7315
+ return;
7316
+ }
7317
+ if (target instanceof Node && popRef.current?.contains(target)) return;
7318
+ onCloseRef.current();
7319
+ };
7320
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
7321
+ return () => window.removeEventListener("scroll", onScroll, true);
7322
+ }, [enabled, popRef]);
7323
+ };
7307
7324
 
7308
7325
  // src/components/Modal/Modal.tsx
7309
7326
  import { jsx as jsx313 } from "react/jsx-runtime";
@@ -7979,15 +7996,21 @@ import React19 from "react";
7979
7996
  var useAutoPosition = (triggerRef, popRef, enabled = true) => {
7980
7997
  const [direction, setDirection] = React19.useState("bottom");
7981
7998
  const directionRef = React19.useRef("bottom");
7982
- const calculatePosition = React19.useCallback(() => {
7999
+ const sizeRef = React19.useRef(null);
8000
+ const calculatePosition = React19.useCallback((remeasure = false) => {
7983
8001
  const pop = popRef.current;
7984
8002
  if (!triggerRef.current || !pop) return;
8003
+ if (remeasure || !sizeRef.current) {
8004
+ sizeRef.current = { w: pop.offsetWidth, h: pop.offsetHeight };
8005
+ }
8006
+ const { w: popW, h: popH } = sizeRef.current;
7985
8007
  const triggerRect = triggerRef.current.getBoundingClientRect();
7986
- const popW = pop.offsetWidth;
7987
- const popH = pop.offsetHeight;
7988
8008
  const viewportHeight = window.innerHeight;
7989
8009
  const viewportWidth = window.innerWidth;
7990
- if (popH === 0 || popW === 0) return;
8010
+ if (popH === 0 || popW === 0) {
8011
+ sizeRef.current = null;
8012
+ return;
8013
+ }
7991
8014
  let next = "bottom";
7992
8015
  let top;
7993
8016
  let left;
@@ -8010,27 +8033,31 @@ var useAutoPosition = (triggerRef, popRef, enabled = true) => {
8010
8033
  }
8011
8034
  }, [triggerRef, popRef]);
8012
8035
  React19.useLayoutEffect(() => {
8013
- if (!enabled) return;
8014
- calculatePosition();
8015
- const raf = requestAnimationFrame(calculatePosition);
8036
+ if (!enabled) {
8037
+ sizeRef.current = null;
8038
+ return;
8039
+ }
8040
+ calculatePosition(true);
8041
+ const raf = requestAnimationFrame(() => calculatePosition(true));
8016
8042
  return () => cancelAnimationFrame(raf);
8017
8043
  }, [calculatePosition, enabled]);
8018
8044
  React19.useEffect(() => {
8019
8045
  if (!enabled || !popRef.current) return;
8020
8046
  const observer = new ResizeObserver(() => {
8021
- calculatePosition();
8047
+ calculatePosition(true);
8022
8048
  });
8023
8049
  observer.observe(popRef.current);
8024
8050
  return () => observer.disconnect();
8025
8051
  }, [calculatePosition, enabled, popRef]);
8026
8052
  React19.useEffect(() => {
8027
8053
  if (!enabled) return;
8028
- const onScrollOrResize = () => calculatePosition();
8029
- window.addEventListener("resize", onScrollOrResize, { passive: true });
8030
- window.addEventListener("scroll", onScrollOrResize, { passive: true, capture: true });
8054
+ const onScroll = () => calculatePosition();
8055
+ const onResize = () => calculatePosition(true);
8056
+ window.addEventListener("resize", onResize, { passive: true });
8057
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
8031
8058
  return () => {
8032
- window.removeEventListener("resize", onScrollOrResize);
8033
- window.removeEventListener("scroll", onScrollOrResize, true);
8059
+ window.removeEventListener("resize", onResize);
8060
+ window.removeEventListener("scroll", onScroll, true);
8034
8061
  };
8035
8062
  }, [calculatePosition, enabled]);
8036
8063
  return { direction };
@@ -8073,6 +8100,7 @@ var Dropdown = (props) => {
8073
8100
  const menuRef = React21.useRef(null);
8074
8101
  const { direction } = useAutoPosition_default(triggerRef, menuRef, mounted);
8075
8102
  useClickOutside_default([triggerRef, menuRef], () => setIsOpen(false), isOpen);
8103
+ useCloseOnContainerScroll(isOpen, menuRef, () => setIsOpen(false));
8076
8104
  useEscapeKey(isOpen, () => {
8077
8105
  setIsOpen(false);
8078
8106
  triggerRef.current?.focus({ preventScroll: true });
@@ -10003,6 +10031,7 @@ var PopOver = (props) => {
10003
10031
  const [localOpen, setLocalOpen] = React27.useState(false);
10004
10032
  const [eventTrigger, setEventTrigger] = React27.useState(false);
10005
10033
  useClickOutside_default([popRef, triggerRef], onClose, isOpen);
10034
+ useCloseOnContainerScroll(isOpen, popRef, onClose);
10006
10035
  useEscapeKey(isOpen, () => {
10007
10036
  onClose();
10008
10037
  triggerRef.current?.focus({ preventScroll: true });
@@ -10283,6 +10312,7 @@ var SelectRoot = (props) => {
10283
10312
  }, []);
10284
10313
  const toggle = React31.useCallback(() => setOpen((prev) => !prev), []);
10285
10314
  useClickOutside_default([contentRef, triggerRef], close, isOpen);
10315
+ useCloseOnContainerScroll(isOpen, contentRef, close);
10286
10316
  useEscapeKey(isOpen, () => {
10287
10317
  close();
10288
10318
  triggerRef.current?.focus({ preventScroll: true });
@@ -11223,6 +11253,7 @@ var TimePicker = (props) => {
11223
11253
  }, [step, min, max]);
11224
11254
  const close = React40.useCallback(() => setOpen(false), []);
11225
11255
  useClickOutside_default([triggerRef, contentRef], close, isOpen);
11256
+ useCloseOnContainerScroll(isOpen, contentRef, close);
11226
11257
  useEscapeKey(isOpen, () => {
11227
11258
  close();
11228
11259
  triggerRef.current?.focus({ preventScroll: true });
@@ -284,6 +284,43 @@ Dropdown · PopOver · TimePicker 가 전부 애니메이션 없이 툭 튀어
284
284
  **올리기만 하면 된다. 쓰는 쪽에서 고칠 것은 없다.** `.repositioning` 클래스를
285
285
  직접 잡는 CSS 를 쓰고 있었다면 그 규칙만 지우면 된다.
286
286
 
287
+ ## 0.14.6 → 0.14.7 — 안쪽 상자가 스크롤되면 오버레이가 닫힌다
288
+
289
+ **동작 변경이다.** `Select` · `Dropdown` · `PopOver` · `TimePicker` 를 열어 둔
290
+ 채 **페이지가 아닌 안쪽 상자**(`overflow: auto`)를 스크롤하면 닫힌다.
291
+
292
+ 닫지 않는 경우:
293
+
294
+ - **페이지(문서) 스크롤** — 팝업이 `absolute` + 문서 좌표라 브라우저가 함께
295
+ 옮겨 준다. 지연도 겹침도 없으므로 닫을 이유가 없다.
296
+ - **팝업 자기 목록 스크롤** — 셀렉트 목록은 `max-height` 안에서 스스로
297
+ 스크롤한다. 그걸로 닫히면 고를 수가 없다.
298
+
299
+ ### 왜
300
+
301
+ 팝업은 `body` 밑 포털에 있고 트리거는 상자 안에 있다. 둘을 이어 주는 것이
302
+ 브라우저에 없으므로 JS 가 매번 다시 붙여야 하는데, **스크롤은 컴포지터가
303
+ 먼저 그리고 `scroll` 이벤트는 그 뒤에 메인 스레드로 온다.** 그래서 JS 로
304
+ 옮기는 한 항상 한 프레임 이상 늦는다. 0.12.0 부터 0.14.6 까지 네 번에 걸쳐
305
+ 줄여 봤지만(rAF 묶기 → DOM 직접 쓰기 → 문서 좌표 → rAF 제거) 원리가 그대로라
306
+ 남았다.
307
+
308
+ 두 번째 증상도 같은 뿌리다. 팝업이 상자 **바깥**에 있어 그 상자에 잘리지
309
+ 않으므로, 트리거가 헤더 밑으로 지나가도 팝업만 헤더 위에 남는다. **위치가
310
+ 틀린 것이지 순서가 틀린 게 아니라서 `z-index` 로는 못 고친다** — 헤더를
311
+ 올리면 이번엔 목록 윗부분이 잘린다.
312
+
313
+ 닫으면 둘 다 없어진다.
314
+
315
+ > **DS `Layout` 을 쓰면 이 조건에 기본으로 들어간다.**
316
+ > `.lib-xplat-layout` 이 `overflow: hidden` 이고 `.lib-xplat-layout-content`
317
+ > 만 `overflow: auto` 라 페이지가 아예 스크롤되지 않는다.
318
+
319
+ ### 쓰는 쪽에서 할 일
320
+
321
+ **없다.** 다만 「열어 둔 채 스크롤하며 고르는」 흐름이 있었다면 그것만 막힌다.
322
+ 목록이 길어 스크롤이 필요하다면 `Select` 의 `searchable` 로 걸러내는 쪽이 낫다.
323
+
287
324
  ---
288
325
 
289
326
  ## Next.js App Router 에서 반드시 걸리는 것
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-plat/design-system",
3
- "version": "0.14.6",
3
+ "version": "0.14.8",
4
4
  "description": "XPLAT UI Design System",
5
5
  "author": "XPLAT WOONG",
6
6
  "main": "dist/index.cjs",