@umamichi-ui/common-components 0.2.0 → 0.3.1

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.
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode, RefObject } from 'react';
2
2
  import { type SiteOverlayAlign } from '../overlay/SiteOverlayBackdrop';
3
3
  export type ConfirmDialogOverlayProps = {
4
4
  open: boolean;
@@ -8,6 +8,8 @@ export type ConfirmDialogOverlayProps = {
8
8
  children: ReactNode;
9
9
  align?: SiteOverlayAlign;
10
10
  dismissOnEscape?: boolean;
11
+ focusManagement?: boolean;
12
+ initialFocusRef?: RefObject<HTMLElement | null>;
11
13
  };
12
- export declare function ConfirmDialogOverlay({ open, overlayId, onDismiss, onExited, children, align, dismissOnEscape, }: ConfirmDialogOverlayProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function ConfirmDialogOverlay({ open, overlayId, onDismiss, onExited, children, align, dismissOnEscape, focusManagement, initialFocusRef, }: ConfirmDialogOverlayProps): import("react/jsx-runtime").JSX.Element;
13
15
  //# sourceMappingURL=ConfirmDialogOverlay.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfirmDialogOverlay.d.ts","sourceRoot":"","sources":["../../src/dialog/ConfirmDialogOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAE5F,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,SAAS,EACT,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,KAAkB,EAClB,eAAsB,GACvB,EAAE,yBAAyB,2CAa3B"}
1
+ {"version":3,"file":"ConfirmDialogOverlay.d.ts","sourceRoot":"","sources":["../../src/dialog/ConfirmDialogOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAI5F,MAAM,MAAM,yBAAyB,GAAG;IAEtC,IAAI,EAAE,OAAO,CAAC;IAEd,SAAS,EAAE,MAAM,CAAC;IAElB,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,EAAE,SAAS,CAAC;IAEpB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAEzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAEjD,CAAC;AAIF,wBAAgB,oBAAoB,CAAC,EAEnC,IAAI,EAEJ,SAAS,EAET,SAAS,EAET,QAAQ,EAER,QAAQ,EAER,KAAkB,EAElB,eAAsB,EAEtB,eAAsB,EAEtB,eAAe,GAEhB,EAAE,yBAAyB,2CA8B3B"}
@@ -7,7 +7,9 @@ function ConfirmDialogOverlay({
7
7
  onExited,
8
8
  children,
9
9
  align = "centered",
10
- dismissOnEscape = true
10
+ dismissOnEscape = true,
11
+ focusManagement = true,
12
+ initialFocusRef
11
13
  }) {
12
14
  return /* @__PURE__ */ jsx(
13
15
  SiteOverlayBackdrop,
@@ -18,6 +20,8 @@ function ConfirmDialogOverlay({
18
20
  onDismiss,
19
21
  onExited,
20
22
  dismissOnEscape,
23
+ focusManagement,
24
+ initialFocusRef,
21
25
  children
22
26
  }
23
27
  );
@@ -1 +1 @@
1
- {"version":3,"file":"ConfirmDialogOverlay.js","sources":["../../src/dialog/ConfirmDialogOverlay.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { SiteOverlayBackdrop, type SiteOverlayAlign } from '../overlay/SiteOverlayBackdrop';\n\nexport type ConfirmDialogOverlayProps = {\n open: boolean;\n overlayId: string;\n onDismiss: () => void;\n onExited?: () => void;\n children: ReactNode;\n align?: SiteOverlayAlign;\n dismissOnEscape?: boolean;\n};\n\nexport function ConfirmDialogOverlay({\n open,\n overlayId,\n onDismiss,\n onExited,\n children,\n align = 'centered',\n dismissOnEscape = true,\n}: ConfirmDialogOverlayProps) {\n return (\n <SiteOverlayBackdrop\n open={open}\n overlayId={overlayId}\n align={align}\n onDismiss={onDismiss}\n onExited={onExited}\n dismissOnEscape={dismissOnEscape}\n >\n {children}\n </SiteOverlayBackdrop>\n );\n}\n"],"names":[],"mappings":";;AAaO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,kBAAkB;AACpB,GAA8B;AAC5B,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEC;AAAA,IAAA;AAAA,EAAA;AAGP;"}
1
+ {"version":3,"file":"ConfirmDialogOverlay.js","sources":["../../src/dialog/ConfirmDialogOverlay.tsx"],"sourcesContent":["import type { ReactNode, RefObject } from 'react';\r\r\nimport { SiteOverlayBackdrop, type SiteOverlayAlign } from '../overlay/SiteOverlayBackdrop';\r\r\n\r\r\nexport type ConfirmDialogOverlayProps = {\r\r\n open: boolean;\r\r\n overlayId: string;\r\r\n onDismiss: () => void;\r\r\n onExited?: () => void;\r\r\n children: ReactNode;\r\r\n align?: SiteOverlayAlign;\r\r\n dismissOnEscape?: boolean;\r\r\n focusManagement?: boolean;\r\r\n initialFocusRef?: RefObject<HTMLElement | null>;\r\r\n};\r\r\n\r\r\nexport function ConfirmDialogOverlay({\r\r\n open,\r\r\n overlayId,\r\r\n onDismiss,\r\r\n onExited,\r\r\n children,\r\r\n align = 'centered',\r\r\n dismissOnEscape = true,\r\r\n focusManagement = true,\r\r\n initialFocusRef,\r\r\n}: ConfirmDialogOverlayProps) {\r\r\n return (\r\r\n <SiteOverlayBackdrop\r\r\n open={open}\r\r\n overlayId={overlayId}\r\r\n align={align}\r\r\n onDismiss={onDismiss}\r\r\n onExited={onExited}\r\r\n dismissOnEscape={dismissOnEscape}\r\r\n focusManagement={focusManagement}\r\r\n initialFocusRef={initialFocusRef}\r\r\n >\r\r\n {children}\r\r\n </SiteOverlayBackdrop>\r\r\n );\r\r\n}\r\r\n"],"names":[],"mappings":";;AA8BO,SAAS,qBAAqB;AAAA,EAEnC;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA,QAAQ;AAAA,EAER,kBAAkB;AAAA,EAElB,kBAAkB;AAAA,EAElB;AAEF,GAA8B;AAE5B,SAEE;AAAA,IAAC;AAAA,IAAA;AAAA,MAEC;AAAA,MAEA;AAAA,MAEA;AAAA,MAEA;AAAA,MAEA;AAAA,MAEA;AAAA,MAEA;AAAA,MAEA;AAAA,MAIC;AAAA,IAAA;AAAA,EAAA;AAMP;"}
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { SiteOverlayBackdrop } from "./overlay/SiteOverlayBackdrop.js";
14
14
  import { computeFloatingMenuGeometry } from "./menu/computeFloatingMenuGeometry.js";
15
15
  import { mergeOverlayRefs, useOverlayPresence, withOverlayOpen } from "./presence/useOverlayPresence.js";
16
16
  import { useFloatingMenuGeometry } from "./menu/useFloatingMenuGeometry.js";
17
+ import { useOverlayFocus } from "./overlay/useOverlayFocus.js";
17
18
  import { useOverlayStackEntry } from "./overlay/useOverlayStackEntry.js";
18
19
  export {
19
20
  AboutDialog,
@@ -37,6 +38,7 @@ export {
37
38
  mergeOverlayRefs,
38
39
  pushOverlayHistoryState,
39
40
  useFloatingMenuGeometry,
41
+ useOverlayFocus,
40
42
  useOverlayPresence,
41
43
  useOverlayStack,
42
44
  useOverlayStackEntry,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;"}
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode, RefObject } from 'react';
2
2
  export type SiteOverlayAlign = 'centered' | 'top' | 'bottom';
3
3
  export type SiteOverlayBackdropProps = {
4
4
  open: boolean;
@@ -7,7 +7,9 @@ export type SiteOverlayBackdropProps = {
7
7
  onDismiss: () => void;
8
8
  onExited?: () => void;
9
9
  dismissOnEscape?: boolean;
10
+ focusManagement?: boolean;
11
+ initialFocusRef?: RefObject<HTMLElement | null>;
10
12
  children: ReactNode;
11
13
  };
12
- export declare function SiteOverlayBackdrop({ open, overlayId, align, onDismiss, onExited, dismissOnEscape, children, }: SiteOverlayBackdropProps): import("react").ReactPortal | null;
14
+ export declare function SiteOverlayBackdrop({ open, overlayId, align, onDismiss, onExited, dismissOnEscape, focusManagement, initialFocusRef, children, }: SiteOverlayBackdropProps): import("react").ReactPortal | null;
13
15
  //# sourceMappingURL=SiteOverlayBackdrop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SiteOverlayBackdrop.d.ts","sourceRoot":"","sources":["../../src/overlay/SiteOverlayBackdrop.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AAM1D,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;AAQ7D,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,SAAS,EACT,KAAkB,EAClB,SAAS,EACT,QAAQ,EACR,eAAsB,EACtB,QAAQ,GACT,EAAE,wBAAwB,sCA0C1B"}
1
+ {"version":3,"file":"SiteOverlayBackdrop.d.ts","sourceRoot":"","sources":["../../src/overlay/SiteOverlayBackdrop.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAcrE,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,CAAC;AAgB7D,MAAM,MAAM,wBAAwB,GAAG;IAErC,IAAI,EAAE,OAAO,CAAC;IAEd,SAAS,EAAE,MAAM,CAAC;IAElB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAEzB,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEhD,QAAQ,EAAE,SAAS,CAAC;CAErB,CAAC;AAIF,wBAAgB,mBAAmB,CAAC,EAElC,IAAI,EAEJ,SAAS,EAET,KAAkB,EAElB,SAAS,EAET,QAAQ,EAER,eAAsB,EAEtB,eAAsB,EAEtB,eAAe,EAEf,QAAQ,GAET,EAAE,wBAAwB,sCA8F1B"}
@@ -3,6 +3,7 @@ import { useEffect } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { useOverlayPresence, withOverlayOpen } from "../presence/useOverlayPresence.js";
5
5
  import { useOverlayStackEntry } from "./useOverlayStackEntry.js";
6
+ import { useOverlayFocus } from "./useOverlayFocus.js";
6
7
  const alignClassName = {
7
8
  centered: "site-overlay-backdrop site-overlay-backdrop--centered",
8
9
  top: "site-overlay-backdrop site-overlay-backdrop--top",
@@ -15,6 +16,8 @@ function SiteOverlayBackdrop({
15
16
  onDismiss,
16
17
  onExited,
17
18
  dismissOnEscape = true,
19
+ focusManagement = true,
20
+ initialFocusRef,
18
21
  children
19
22
  }) {
20
23
  const { mounted, isOpen, overlayRef } = useOverlayPresence(open);
@@ -24,6 +27,10 @@ function SiteOverlayBackdrop({
24
27
  onDismiss,
25
28
  dismissOnEscape
26
29
  });
30
+ useOverlayFocus(overlayRef, open, open && isBackdropActive && isOpen, {
31
+ enabled: focusManagement,
32
+ initialFocusRef
33
+ });
27
34
  useEffect(() => {
28
35
  if (!mounted) {
29
36
  onExited?.();
@@ -1 +1 @@
1
- {"version":3,"file":"SiteOverlayBackdrop.js","sources":["../../src/overlay/SiteOverlayBackdrop.tsx"],"sourcesContent":["import type { MouseEventHandler, ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useOverlayPresence, withOverlayOpen } from '../presence/useOverlayPresence';\nimport { useOverlayStackEntry } from './useOverlayStackEntry';\n\nexport type SiteOverlayAlign = 'centered' | 'top' | 'bottom';\n\nconst alignClassName: Record<SiteOverlayAlign, string> = {\n centered: 'site-overlay-backdrop site-overlay-backdrop--centered',\n top: 'site-overlay-backdrop site-overlay-backdrop--top',\n bottom: 'site-overlay-backdrop site-overlay-backdrop--bottom',\n};\n\nexport type SiteOverlayBackdropProps = {\n open: boolean;\n overlayId: string;\n align?: SiteOverlayAlign;\n onDismiss: () => void;\n onExited?: () => void;\n dismissOnEscape?: boolean;\n children: ReactNode;\n};\n\nexport function SiteOverlayBackdrop({\n open,\n overlayId,\n align = 'centered',\n onDismiss,\n onExited,\n dismissOnEscape = true,\n children,\n}: SiteOverlayBackdropProps) {\n const { mounted, isOpen, overlayRef } = useOverlayPresence<HTMLDivElement>(open);\n const { isBackdropActive, zIndex } = useOverlayStackEntry({\n id: overlayId,\n open,\n onDismiss,\n dismissOnEscape,\n });\n\n useEffect(() => {\n if (!mounted) {\n onExited?.();\n }\n }, [mounted, onExited]);\n\n if (!mounted) {\n return null;\n }\n\n const onBackdropClick: MouseEventHandler<HTMLDivElement> = (event) => {\n if (!isBackdropActive || event.target !== event.currentTarget) {\n return;\n }\n\n onDismiss();\n };\n\n return createPortal(\n <div\n ref={overlayRef}\n className={withOverlayOpen(\n `${alignClassName[align]}${isBackdropActive ? '' : ' site-overlay-backdrop-inactive'}`,\n isOpen,\n )}\n style={{ zIndex }}\n role=\"presentation\"\n onClick={onBackdropClick}\n >\n {children}\n </div>,\n document.body,\n );\n}\n"],"names":[],"mappings":";;;;;AAQA,MAAM,iBAAmD;AAAA,EACvD,UAAU;AAAA,EACV,KAAK;AAAA,EACL,QAAQ;AACV;AAYO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AACF,GAA6B;AAC3B,QAAM,EAAE,SAAS,QAAQ,WAAA,IAAe,mBAAmC,IAAI;AAC/E,QAAM,EAAE,kBAAkB,OAAA,IAAW,qBAAqB;AAAA,IACxD,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AAED,YAAU,MAAM;AACd,QAAI,CAAC,SAAS;AACZ,iBAAA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,QAAQ,CAAC;AAEtB,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,QAAM,kBAAqD,CAAC,UAAU;AACpE,QAAI,CAAC,oBAAoB,MAAM,WAAW,MAAM,eAAe;AAC7D;AAAA,IACF;AAEA,cAAA;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,WAAW;AAAA,UACT,GAAG,eAAe,KAAK,CAAC,GAAG,mBAAmB,KAAK,iCAAiC;AAAA,UACpF;AAAA,QAAA;AAAA,QAEF,OAAO,EAAE,OAAA;AAAA,QACT,MAAK;AAAA,QACL,SAAS;AAAA,QAER;AAAA,MAAA;AAAA,IAAA;AAAA,IAEH,SAAS;AAAA,EAAA;AAEb;"}
1
+ {"version":3,"file":"SiteOverlayBackdrop.js","sources":["../../src/overlay/SiteOverlayBackdrop.tsx"],"sourcesContent":["import type { MouseEventHandler, ReactNode, RefObject } from 'react';\r\r\nimport { useEffect } from 'react';\r\r\nimport { createPortal } from 'react-dom';\r\r\nimport { useOverlayPresence, withOverlayOpen } from '../presence/useOverlayPresence';\r\r\nimport { useOverlayStackEntry } from './useOverlayStackEntry';\r\r\nimport { useOverlayFocus } from './useOverlayFocus';\r\r\n\r\r\nexport type SiteOverlayAlign = 'centered' | 'top' | 'bottom';\r\r\n\r\r\nconst alignClassName: Record<SiteOverlayAlign, string> = {\r\r\n centered: 'site-overlay-backdrop site-overlay-backdrop--centered',\r\r\n top: 'site-overlay-backdrop site-overlay-backdrop--top',\r\r\n bottom: 'site-overlay-backdrop site-overlay-backdrop--bottom',\r\r\n};\r\r\n\r\r\nexport type SiteOverlayBackdropProps = {\r\r\n open: boolean;\r\r\n overlayId: string;\r\r\n align?: SiteOverlayAlign;\r\r\n onDismiss: () => void;\r\r\n onExited?: () => void;\r\r\n dismissOnEscape?: boolean;\r\r\n focusManagement?: boolean;\r\r\n initialFocusRef?: RefObject<HTMLElement | null>;\r\r\n children: ReactNode;\r\r\n};\r\r\n\r\r\nexport function SiteOverlayBackdrop({\r\r\n open,\r\r\n overlayId,\r\r\n align = 'centered',\r\r\n onDismiss,\r\r\n onExited,\r\r\n dismissOnEscape = true,\r\r\n focusManagement = true,\r\r\n initialFocusRef,\r\r\n children,\r\r\n}: SiteOverlayBackdropProps) {\r\r\n const { mounted, isOpen, overlayRef } = useOverlayPresence<HTMLDivElement>(open);\r\r\n const { isBackdropActive, zIndex } = useOverlayStackEntry({\r\r\n id: overlayId,\r\r\n open,\r\r\n onDismiss,\r\r\n dismissOnEscape,\r\r\n });\r\r\n\r\r\n useOverlayFocus(overlayRef, open, open && isBackdropActive && isOpen, {\r\r\n enabled: focusManagement,\r\r\n initialFocusRef,\r\r\n });\r\r\n\r\r\n useEffect(() => {\r\r\n if (!mounted) {\r\r\n onExited?.();\r\r\n }\r\r\n }, [mounted, onExited]);\r\r\n\r\r\n if (!mounted) {\r\r\n return null;\r\r\n }\r\r\n\r\r\n const onBackdropClick: MouseEventHandler<HTMLDivElement> = (event) => {\r\r\n if (!isBackdropActive || event.target !== event.currentTarget) {\r\r\n return;\r\r\n }\r\r\n\r\r\n onDismiss();\r\r\n };\r\r\n\r\r\n return createPortal(\r\r\n <div\r\r\n ref={overlayRef}\r\r\n className={withOverlayOpen(\r\r\n `${alignClassName[align]}${isBackdropActive ? '' : ' site-overlay-backdrop-inactive'}`,\r\r\n isOpen,\r\r\n )}\r\r\n style={{ zIndex }}\r\r\n role=\"presentation\"\r\r\n onClick={onBackdropClick}\r\r\n >\r\r\n {children}\r\r\n </div>,\r\r\n document.body,\r\r\n );\r\r\n}\r\r\n"],"names":[],"mappings":";;;;;;AAkBA,MAAM,iBAAmD;AAAA,EAEvD,UAAU;AAAA,EAEV,KAAK;AAAA,EAEL,QAAQ;AAEV;AA4BO,SAAS,oBAAoB;AAAA,EAElC;AAAA,EAEA;AAAA,EAEA,QAAQ;AAAA,EAER;AAAA,EAEA;AAAA,EAEA,kBAAkB;AAAA,EAElB,kBAAkB;AAAA,EAElB;AAAA,EAEA;AAEF,GAA6B;AAE3B,QAAM,EAAE,SAAS,QAAQ,WAAA,IAAe,mBAAmC,IAAI;AAE/E,QAAM,EAAE,kBAAkB,OAAA,IAAW,qBAAqB;AAAA,IAExD,IAAI;AAAA,IAEJ;AAAA,IAEA;AAAA,IAEA;AAAA,EAAA,CAED;AAID,kBAAgB,YAAY,MAAM,QAAQ,oBAAoB,QAAQ;AAAA,IAEpE,SAAS;AAAA,IAET;AAAA,EAAA,CAED;AAID,YAAU,MAAM;AAEd,QAAI,CAAC,SAAS;AAEZ,iBAAA;AAAA,IAEF;AAAA,EAEF,GAAG,CAAC,SAAS,QAAQ,CAAC;AAItB,MAAI,CAAC,SAAS;AAEZ,WAAO;AAAA,EAET;AAIA,QAAM,kBAAqD,CAAC,UAAU;AAEpE,QAAI,CAAC,oBAAoB,MAAM,WAAW,MAAM,eAAe;AAE7D;AAAA,IAEF;AAIA,cAAA;AAAA,EAEF;AAIA,SAAO;AAAA,IAEL;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,KAAK;AAAA,QAEL,WAAW;AAAA,UAET,GAAG,eAAe,KAAK,CAAC,GAAG,mBAAmB,KAAK,iCAAiC;AAAA,UAEpF;AAAA,QAAA;AAAA,QAIF,OAAO,EAAE,OAAA;AAAA,QAET,MAAK;AAAA,QAEL,SAAS;AAAA,QAIR;AAAA,MAAA;AAAA,IAAA;AAAA,IAIH,SAAS;AAAA,EAAA;AAIb;"}
@@ -2,5 +2,6 @@ export { OverlayStackProvider, useOverlayStack } from './OverlayStackProvider';
2
2
  export { isOverlayHistoryState, OVERLAY_HISTORY_STATE_KEY, pushOverlayHistoryState, type OverlayHistoryState, } from './overlayHistory';
3
3
  export { computeOverlayZIndex, OVERLAY_Z_BASE, OVERLAY_Z_STEP, type OverlayStackEntry, } from './overlayStackTypes';
4
4
  export { SiteOverlayBackdrop, type SiteOverlayAlign, type SiteOverlayBackdropProps } from './SiteOverlayBackdrop';
5
+ export { useOverlayFocus, type UseOverlayFocusOptions } from './useOverlayFocus';
5
6
  export { useOverlayStackEntry, type UseOverlayStackEntryOptions } from './useOverlayStackEntry';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/overlay/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,cAAc,EACd,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAClH,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/overlay/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,cAAc,EACd,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAClH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,wBAAwB,CAAC"}
@@ -2,6 +2,7 @@ import { OverlayStackProvider, useOverlayStack } from "./OverlayStackProvider.js
2
2
  import { OVERLAY_HISTORY_STATE_KEY, isOverlayHistoryState, pushOverlayHistoryState } from "./overlayHistory.js";
3
3
  import { OVERLAY_Z_BASE, OVERLAY_Z_STEP, computeOverlayZIndex } from "./overlayStackTypes.js";
4
4
  import { SiteOverlayBackdrop } from "./SiteOverlayBackdrop.js";
5
+ import { useOverlayFocus } from "./useOverlayFocus.js";
5
6
  import { useOverlayStackEntry } from "./useOverlayStackEntry.js";
6
7
  export {
7
8
  OVERLAY_HISTORY_STATE_KEY,
@@ -12,6 +13,7 @@ export {
12
13
  computeOverlayZIndex,
13
14
  isOverlayHistoryState,
14
15
  pushOverlayHistoryState,
16
+ useOverlayFocus,
15
17
  useOverlayStack,
16
18
  useOverlayStackEntry
17
19
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -0,0 +1,11 @@
1
+ import { type RefObject } from 'react';
2
+ export type UseOverlayFocusOptions = {
3
+ enabled?: boolean;
4
+ initialFocusRef?: RefObject<HTMLElement | null>;
5
+ };
6
+ /**
7
+ * Stack-aware focus management for modal overlays: initial focus, Tab trap while active,
8
+ * restore focus when the overlay session closes. Only traps while `active` (typically top layer).
9
+ */
10
+ export declare function useOverlayFocus(containerRef: RefObject<HTMLElement | null>, open: boolean, active: boolean, { enabled, initialFocusRef }?: UseOverlayFocusOptions): void;
11
+ //# sourceMappingURL=useOverlayFocus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOverlayFocus.d.ts","sourceRoot":"","sources":["../../src/overlay/useOverlayFocus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAW1D,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACjD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EAC3C,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,OAAO,EACf,EAAE,OAAc,EAAE,eAAe,EAAE,GAAE,sBAA2B,QAmFjE"}
@@ -0,0 +1,78 @@
1
+ import { useRef, useEffect } from "react";
2
+ const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
3
+ function getFocusableElements(container) {
4
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
5
+ (element) => !element.hasAttribute("disabled") && element.tabIndex !== -1
6
+ );
7
+ }
8
+ function useOverlayFocus(containerRef, open, active, { enabled = true, initialFocusRef } = {}) {
9
+ const previouslyFocusedRef = useRef(null);
10
+ const openSessionRef = useRef(false);
11
+ useEffect(() => {
12
+ if (!enabled) {
13
+ return;
14
+ }
15
+ if (!open) {
16
+ const toRestore = previouslyFocusedRef.current;
17
+ previouslyFocusedRef.current = null;
18
+ openSessionRef.current = false;
19
+ toRestore?.focus();
20
+ return;
21
+ }
22
+ if (!openSessionRef.current) {
23
+ previouslyFocusedRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
24
+ openSessionRef.current = true;
25
+ }
26
+ if (!active) {
27
+ return;
28
+ }
29
+ const focusInitial = () => {
30
+ if (initialFocusRef?.current) {
31
+ initialFocusRef.current.focus();
32
+ return;
33
+ }
34
+ const container = containerRef.current;
35
+ if (!container) {
36
+ return;
37
+ }
38
+ getFocusableElements(container)[0]?.focus();
39
+ };
40
+ const raf = requestAnimationFrame(focusInitial);
41
+ const onKeyDown = (event) => {
42
+ if (event.key !== "Tab") {
43
+ return;
44
+ }
45
+ const container = containerRef.current;
46
+ if (!container) {
47
+ return;
48
+ }
49
+ const focusables = getFocusableElements(container);
50
+ if (focusables.length === 0) {
51
+ return;
52
+ }
53
+ const first = focusables[0];
54
+ const last = focusables[focusables.length - 1];
55
+ const activeElement = document.activeElement;
56
+ if (event.shiftKey) {
57
+ if (activeElement === first || !container.contains(activeElement)) {
58
+ event.preventDefault();
59
+ last.focus();
60
+ }
61
+ return;
62
+ }
63
+ if (activeElement === last || !container.contains(activeElement)) {
64
+ event.preventDefault();
65
+ first.focus();
66
+ }
67
+ };
68
+ document.addEventListener("keydown", onKeyDown);
69
+ return () => {
70
+ cancelAnimationFrame(raf);
71
+ document.removeEventListener("keydown", onKeyDown);
72
+ };
73
+ }, [active, containerRef, enabled, initialFocusRef, open]);
74
+ }
75
+ export {
76
+ useOverlayFocus
77
+ };
78
+ //# sourceMappingURL=useOverlayFocus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOverlayFocus.js","sources":["../../src/overlay/useOverlayFocus.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nfunction getFocusableElements(container: HTMLElement) {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(\n (element) => !element.hasAttribute('disabled') && element.tabIndex !== -1,\n );\n}\n\nexport type UseOverlayFocusOptions = {\n enabled?: boolean;\n initialFocusRef?: RefObject<HTMLElement | null>;\n};\n\n/**\n * Stack-aware focus management for modal overlays: initial focus, Tab trap while active,\n * restore focus when the overlay session closes. Only traps while `active` (typically top layer).\n */\nexport function useOverlayFocus(\n containerRef: RefObject<HTMLElement | null>,\n open: boolean,\n active: boolean,\n { enabled = true, initialFocusRef }: UseOverlayFocusOptions = {},\n) {\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n const openSessionRef = useRef(false);\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n if (!open) {\n const toRestore = previouslyFocusedRef.current;\n previouslyFocusedRef.current = null;\n openSessionRef.current = false;\n toRestore?.focus();\n return;\n }\n\n if (!openSessionRef.current) {\n previouslyFocusedRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n openSessionRef.current = true;\n }\n\n if (!active) {\n return;\n }\n\n const focusInitial = () => {\n if (initialFocusRef?.current) {\n initialFocusRef.current.focus();\n return;\n }\n\n const container = containerRef.current;\n if (!container) {\n return;\n }\n\n getFocusableElements(container)[0]?.focus();\n };\n\n const raf = requestAnimationFrame(focusInitial);\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Tab') {\n return;\n }\n\n const container = containerRef.current;\n if (!container) {\n return;\n }\n\n const focusables = getFocusableElements(container);\n if (focusables.length === 0) {\n return;\n }\n\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const activeElement = document.activeElement;\n\n if (event.shiftKey) {\n if (activeElement === first || !container.contains(activeElement)) {\n event.preventDefault();\n last.focus();\n }\n return;\n }\n\n if (activeElement === last || !container.contains(activeElement)) {\n event.preventDefault();\n first.focus();\n }\n };\n\n document.addEventListener('keydown', onKeyDown);\n\n return () => {\n cancelAnimationFrame(raf);\n document.removeEventListener('keydown', onKeyDown);\n };\n }, [active, containerRef, enabled, initialFocusRef, open]);\n}\n"],"names":[],"mappings":";AAEA,MAAM,qBACJ;AAEF,SAAS,qBAAqB,WAAwB;AACpD,SAAO,MAAM,KAAK,UAAU,iBAA8B,kBAAkB,CAAC,EAAE;AAAA,IAC7E,CAAC,YAAY,CAAC,QAAQ,aAAa,UAAU,KAAK,QAAQ,aAAa;AAAA,EAAA;AAE3E;AAWO,SAAS,gBACd,cACA,MACA,QACA,EAAE,UAAU,MAAM,gBAAA,IAA4C,IAC9D;AACA,QAAM,uBAAuB,OAA2B,IAAI;AAC5D,QAAM,iBAAiB,OAAO,KAAK;AAEnC,YAAU,MAAM;AACd,QAAI,CAAC,SAAS;AACZ;AAAA,IACF;AAEA,QAAI,CAAC,MAAM;AACT,YAAM,YAAY,qBAAqB;AACvC,2BAAqB,UAAU;AAC/B,qBAAe,UAAU;AACzB,iBAAW,MAAA;AACX;AAAA,IACF;AAEA,QAAI,CAAC,eAAe,SAAS;AAC3B,2BAAqB,UAAU,SAAS,yBAAyB,cAAc,SAAS,gBAAgB;AACxG,qBAAe,UAAU;AAAA,IAC3B;AAEA,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AAEA,UAAM,eAAe,MAAM;AACzB,UAAI,iBAAiB,SAAS;AAC5B,wBAAgB,QAAQ,MAAA;AACxB;AAAA,MACF;AAEA,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,WAAW;AACd;AAAA,MACF;AAEA,2BAAqB,SAAS,EAAE,CAAC,GAAG,MAAA;AAAA,IACtC;AAEA,UAAM,MAAM,sBAAsB,YAAY;AAE9C,UAAM,YAAY,CAAC,UAAyB;AAC1C,UAAI,MAAM,QAAQ,OAAO;AACvB;AAAA,MACF;AAEA,YAAM,YAAY,aAAa;AAC/B,UAAI,CAAC,WAAW;AACd;AAAA,MACF;AAEA,YAAM,aAAa,qBAAqB,SAAS;AACjD,UAAI,WAAW,WAAW,GAAG;AAC3B;AAAA,MACF;AAEA,YAAM,QAAQ,WAAW,CAAC;AAC1B,YAAM,OAAO,WAAW,WAAW,SAAS,CAAC;AAC7C,YAAM,gBAAgB,SAAS;AAE/B,UAAI,MAAM,UAAU;AAClB,YAAI,kBAAkB,SAAS,CAAC,UAAU,SAAS,aAAa,GAAG;AACjE,gBAAM,eAAA;AACN,eAAK,MAAA;AAAA,QACP;AACA;AAAA,MACF;AAEA,UAAI,kBAAkB,QAAQ,CAAC,UAAU,SAAS,aAAa,GAAG;AAChE,cAAM,eAAA;AACN,cAAM,MAAA;AAAA,MACR;AAAA,IACF;AAEA,aAAS,iBAAiB,WAAW,SAAS;AAE9C,WAAO,MAAM;AACX,2BAAqB,GAAG;AACxB,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,QAAQ,cAAc,SAAS,iBAAiB,IAAI,CAAC;AAC3D;"}
@@ -1 +1 @@
1
- {"version":3,"file":"useOverlayPresence.d.ts","sourceRoot":"","sources":["../../src/presence/useOverlayPresence.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA0EpE,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,MAAM,EAAE,OAAO;;;;EAwCtF;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,UAEjE;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACpD,UAAU,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EAC/B,GAAG,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,IAE3E,MAAM,CAAC,GAAG,IAAI,UAavB"}
1
+ {"version":3,"file":"useOverlayPresence.d.ts","sourceRoot":"","sources":["../../src/presence/useOverlayPresence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AA0ErF,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,MAAM,EAAE,OAAO;;;;EA4DtF;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,UAEjE;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,WAAW,EACpD,UAAU,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,EAC/B,GAAG,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,IAE3E,MAAM,CAAC,GAAG,IAAI,UAavB"}
@@ -1,4 +1,4 @@
1
- import { useRef, useState, useEffect } from "react";
1
+ import { useRef, useState, useLayoutEffect, useEffect } from "react";
2
2
  const FALLBACK_OVERLAY_MS = 200;
3
3
  function parseCssTime(value) {
4
4
  const trimmed = value.trim();
@@ -63,17 +63,30 @@ function useOverlayPresence(active) {
63
63
  const overlayRef = useRef(null);
64
64
  const [mounted, setMounted] = useState(active);
65
65
  const [isOpen, setIsOpen] = useState(false);
66
- useEffect(() => {
66
+ useLayoutEffect(() => {
67
67
  if (active) {
68
68
  setMounted(true);
69
- let raf2 = 0;
70
- const raf1 = requestAnimationFrame(() => {
71
- raf2 = requestAnimationFrame(() => setIsOpen(true));
72
- });
73
- return () => {
74
- cancelAnimationFrame(raf1);
75
- cancelAnimationFrame(raf2);
76
- };
69
+ }
70
+ }, [active]);
71
+ useLayoutEffect(() => {
72
+ if (!active || !mounted) {
73
+ return;
74
+ }
75
+ const el = overlayRef.current;
76
+ if (!el) {
77
+ return;
78
+ }
79
+ void el.getBoundingClientRect();
80
+ const rafId = requestAnimationFrame(() => {
81
+ setIsOpen(true);
82
+ });
83
+ return () => {
84
+ cancelAnimationFrame(rafId);
85
+ };
86
+ }, [active, mounted]);
87
+ useEffect(() => {
88
+ if (active) {
89
+ return;
77
90
  }
78
91
  setIsOpen(false);
79
92
  let cancelWait;
@@ -1 +1 @@
1
- {"version":3,"file":"useOverlayPresence.js","sources":["../../src/presence/useOverlayPresence.ts"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from 'react';\n\n/** Fallback when computed transition duration is unavailable (see `--transition-overlay` in common-css). */\nconst FALLBACK_OVERLAY_MS = 200;\n\nfunction parseCssTime(value: string): number {\n const trimmed = value.trim();\n if (!trimmed || trimmed === '0') {\n return 0;\n }\n if (trimmed.endsWith('ms')) {\n return Number.parseFloat(trimmed);\n }\n if (trimmed.endsWith('s')) {\n return Number.parseFloat(trimmed) * 1000;\n }\n return Number.parseFloat(trimmed) || 0;\n}\n\nfunction getMaxTransitionMs(el: HTMLElement): number {\n const style = getComputedStyle(el);\n const durations = style.transitionDuration.split(',').map(parseCssTime);\n const delays = style.transitionDelay.split(',').map(parseCssTime);\n let max = 0;\n const count = Math.max(durations.length, delays.length, 1);\n\n for (let i = 0; i < count; i += 1) {\n const duration = durations[i % durations.length] ?? durations[0] ?? 0;\n const delay = delays[i % delays.length] ?? delays[0] ?? 0;\n max = Math.max(max, duration + delay);\n }\n\n return max > 0 ? max : FALLBACK_OVERLAY_MS;\n}\n\nfunction waitForSubtreeTransitions(el: HTMLElement, onDone: () => void): () => void {\n let settled = false;\n let debounceId = 0;\n let safetyId = 0;\n\n const cleanup = () => {\n el.removeEventListener('transitionend', onTransitionEnd);\n window.clearTimeout(debounceId);\n window.clearTimeout(safetyId);\n };\n\n const finish = () => {\n if (settled) {\n return;\n }\n settled = true;\n cleanup();\n onDone();\n };\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target instanceof Node && !el.contains(event.target)) {\n return;\n }\n window.clearTimeout(debounceId);\n debounceId = window.setTimeout(finish, 0);\n };\n\n safetyId = window.setTimeout(finish, getMaxTransitionMs(el) + 50);\n el.addEventListener('transitionend', onTransitionEnd);\n\n return () => {\n if (!settled) {\n settled = true;\n cleanup();\n }\n };\n}\n\nexport function useOverlayPresence<T extends HTMLElement = HTMLElement>(active: boolean) {\n const overlayRef = useRef<T | null>(null);\n const [mounted, setMounted] = useState(active);\n const [isOpen, setIsOpen] = useState(false);\n\n useEffect(() => {\n if (active) {\n setMounted(true);\n let raf2 = 0;\n const raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(() => setIsOpen(true));\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n }\n\n setIsOpen(false);\n\n let cancelWait: (() => void) | undefined;\n let exitRaf = 0;\n\n exitRaf = requestAnimationFrame(() => {\n const el = overlayRef.current;\n if (!el) {\n setMounted(false);\n return;\n }\n\n cancelWait = waitForSubtreeTransitions(el, () => setMounted(false));\n });\n\n return () => {\n cancelAnimationFrame(exitRaf);\n cancelWait?.();\n };\n }, [active]);\n\n return { mounted, isOpen, overlayRef };\n}\n\nexport function withOverlayOpen(className: string, isOpen: boolean) {\n return isOpen ? `${className} is-open` : className;\n}\n\n/** Attach the same DOM node to multiple refs (e.g. layout measure + transition root). */\nexport function mergeOverlayRefs<T extends HTMLElement>(\n overlayRef: RefObject<T | null>,\n ...refs: Array<RefObject<T | null> | ((node: T | null) => void) | null | undefined>\n) {\n return (node: T | null) => {\n overlayRef.current = node;\n for (const ref of refs) {\n if (!ref) {\n continue;\n }\n if (typeof ref === 'function') {\n ref(node);\n continue;\n }\n ref.current = node;\n }\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useOverlayPresence.js","sources":["../../src/presence/useOverlayPresence.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from 'react';\n\n/** Fallback when computed transition duration is unavailable (see `--transition-overlay` in common-css). */\nconst FALLBACK_OVERLAY_MS = 200;\n\nfunction parseCssTime(value: string): number {\n const trimmed = value.trim();\n if (!trimmed || trimmed === '0') {\n return 0;\n }\n if (trimmed.endsWith('ms')) {\n return Number.parseFloat(trimmed);\n }\n if (trimmed.endsWith('s')) {\n return Number.parseFloat(trimmed) * 1000;\n }\n return Number.parseFloat(trimmed) || 0;\n}\n\nfunction getMaxTransitionMs(el: HTMLElement): number {\n const style = getComputedStyle(el);\n const durations = style.transitionDuration.split(',').map(parseCssTime);\n const delays = style.transitionDelay.split(',').map(parseCssTime);\n let max = 0;\n const count = Math.max(durations.length, delays.length, 1);\n\n for (let i = 0; i < count; i += 1) {\n const duration = durations[i % durations.length] ?? durations[0] ?? 0;\n const delay = delays[i % delays.length] ?? delays[0] ?? 0;\n max = Math.max(max, duration + delay);\n }\n\n return max > 0 ? max : FALLBACK_OVERLAY_MS;\n}\n\nfunction waitForSubtreeTransitions(el: HTMLElement, onDone: () => void): () => void {\n let settled = false;\n let debounceId = 0;\n let safetyId = 0;\n\n const cleanup = () => {\n el.removeEventListener('transitionend', onTransitionEnd);\n window.clearTimeout(debounceId);\n window.clearTimeout(safetyId);\n };\n\n const finish = () => {\n if (settled) {\n return;\n }\n settled = true;\n cleanup();\n onDone();\n };\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target instanceof Node && !el.contains(event.target)) {\n return;\n }\n window.clearTimeout(debounceId);\n debounceId = window.setTimeout(finish, 0);\n };\n\n safetyId = window.setTimeout(finish, getMaxTransitionMs(el) + 50);\n el.addEventListener('transitionend', onTransitionEnd);\n\n return () => {\n if (!settled) {\n settled = true;\n cleanup();\n }\n };\n}\n\nexport function useOverlayPresence<T extends HTMLElement = HTMLElement>(active: boolean) {\n const overlayRef = useRef<T | null>(null);\n const [mounted, setMounted] = useState(active);\n const [isOpen, setIsOpen] = useState(false);\n\n useLayoutEffect(() => {\n if (active) {\n setMounted(true);\n }\n }, [active]);\n\n useLayoutEffect(() => {\n if (!active || !mounted) {\n return;\n }\n\n const el = overlayRef.current;\n if (!el) {\n return;\n }\n\n // Commit closed styles before toggling is-open so CSS transitions always have a start state.\n void el.getBoundingClientRect();\n\n const rafId = requestAnimationFrame(() => {\n setIsOpen(true);\n });\n\n return () => {\n cancelAnimationFrame(rafId);\n };\n }, [active, mounted]);\n\n useEffect(() => {\n if (active) {\n return;\n }\n\n setIsOpen(false);\n\n let cancelWait: (() => void) | undefined;\n let exitRaf = 0;\n\n exitRaf = requestAnimationFrame(() => {\n const el = overlayRef.current;\n if (!el) {\n setMounted(false);\n return;\n }\n\n cancelWait = waitForSubtreeTransitions(el, () => setMounted(false));\n });\n\n return () => {\n cancelAnimationFrame(exitRaf);\n cancelWait?.();\n };\n }, [active]);\n\n return { mounted, isOpen, overlayRef };\n}\n\nexport function withOverlayOpen(className: string, isOpen: boolean) {\n return isOpen ? `${className} is-open` : className;\n}\n\n/** Attach the same DOM node to multiple refs (e.g. layout measure + transition root). */\nexport function mergeOverlayRefs<T extends HTMLElement>(\n overlayRef: RefObject<T | null>,\n ...refs: Array<RefObject<T | null> | ((node: T | null) => void) | null | undefined>\n) {\n return (node: T | null) => {\n overlayRef.current = node;\n for (const ref of refs) {\n if (!ref) {\n continue;\n }\n if (typeof ref === 'function') {\n ref(node);\n continue;\n }\n ref.current = node;\n }\n };\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,81 +1,81 @@
1
- {
2
- "name": "@umamichi-ui/common-components",
3
- "version": "0.2.0",
4
- "description": "Reusable React UI components for Umamichi sites (overlay stack, dialogs, menus)",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/umamichi-ui/common-components.git"
10
- },
11
- "homepage": "https://github.com/umamichi-ui/common-components#readme",
12
- "bugs": {
13
- "url": "https://github.com/umamichi-ui/common-components/issues"
14
- },
15
- "publishConfig": {
16
- "access": "public"
17
- },
18
- "keywords": [
19
- "react",
20
- "ui",
21
- "overlay",
22
- "modal",
23
- "dialog",
24
- "umamichi"
25
- ],
26
- "files": [
27
- "dist",
28
- "src/styles"
29
- ],
30
- "sideEffects": [
31
- "**/*.css"
32
- ],
33
- "exports": {
34
- ".": {
35
- "types": "./dist/index.d.ts",
36
- "import": "./dist/index.js"
37
- },
38
- "./presence": {
39
- "types": "./dist/presence/index.d.ts",
40
- "import": "./dist/presence/index.js"
41
- },
42
- "./overlay": {
43
- "types": "./dist/overlay/index.d.ts",
44
- "import": "./dist/overlay/index.js"
45
- },
46
- "./icons": {
47
- "types": "./dist/icons/index.d.ts",
48
- "import": "./dist/icons/index.js"
49
- },
50
- "./menu": {
51
- "types": "./dist/menu/index.d.ts",
52
- "import": "./dist/menu/index.js"
53
- },
54
- "./dialog": {
55
- "types": "./dist/dialog/index.d.ts",
56
- "import": "./dist/dialog/index.js"
57
- },
58
- "./styles.css": "./src/styles/index.css"
59
- },
60
- "scripts": {
61
- "build": "vite build && tsc -p tsconfig.build.json",
62
- "dev": "vite build --watch",
63
- "prepublishOnly": "npm run build"
64
- },
65
- "peerDependencies": {
66
- "@umamichi-ui/common-css": ">=0.16.0",
67
- "react": "^18.0.0 || ^19.0.0",
68
- "react-dom": "^18.0.0 || ^19.0.0"
69
- },
70
- "devDependencies": {
71
- "@types/react": "^19.2.2",
72
- "@types/react-dom": "^19.2.2",
73
- "@umamichi-ui/common-css": "^0.16.0",
74
- "@vitejs/plugin-react": "^5.1.0",
75
- "react": "^19.2.0",
76
- "react-dom": "^19.2.0",
77
- "typescript": "^5.9.3",
78
- "vite": "^7.1.12",
79
- "vite-plugin-dts": "^4.5.4"
80
- }
81
- }
1
+ {
2
+ "name": "@umamichi-ui/common-components",
3
+ "version": "0.3.1",
4
+ "description": "Reusable React UI components for Umamichi sites (overlay stack, dialogs, menus)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/umamichi-ui/common-components.git"
10
+ },
11
+ "homepage": "https://github.com/umamichi-ui/common-components#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/umamichi-ui/common-components/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "keywords": [
19
+ "react",
20
+ "ui",
21
+ "overlay",
22
+ "modal",
23
+ "dialog",
24
+ "umamichi"
25
+ ],
26
+ "files": [
27
+ "dist",
28
+ "src/styles"
29
+ ],
30
+ "sideEffects": [
31
+ "**/*.css"
32
+ ],
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./presence": {
39
+ "types": "./dist/presence/index.d.ts",
40
+ "import": "./dist/presence/index.js"
41
+ },
42
+ "./overlay": {
43
+ "types": "./dist/overlay/index.d.ts",
44
+ "import": "./dist/overlay/index.js"
45
+ },
46
+ "./icons": {
47
+ "types": "./dist/icons/index.d.ts",
48
+ "import": "./dist/icons/index.js"
49
+ },
50
+ "./menu": {
51
+ "types": "./dist/menu/index.d.ts",
52
+ "import": "./dist/menu/index.js"
53
+ },
54
+ "./dialog": {
55
+ "types": "./dist/dialog/index.d.ts",
56
+ "import": "./dist/dialog/index.js"
57
+ },
58
+ "./styles.css": "./src/styles/index.css"
59
+ },
60
+ "scripts": {
61
+ "build": "vite build && tsc -p tsconfig.build.json",
62
+ "dev": "vite build --watch",
63
+ "prepublishOnly": "npm run build"
64
+ },
65
+ "peerDependencies": {
66
+ "@umamichi-ui/common-css": ">=0.16.0",
67
+ "react": "^18.0.0 || ^19.0.0",
68
+ "react-dom": "^18.0.0 || ^19.0.0"
69
+ },
70
+ "devDependencies": {
71
+ "@types/react": "^19.2.2",
72
+ "@types/react-dom": "^19.2.2",
73
+ "@umamichi-ui/common-css": "^0.16.0",
74
+ "@vitejs/plugin-react": "^5.1.0",
75
+ "react": "^19.2.0",
76
+ "react-dom": "^19.2.0",
77
+ "typescript": "^5.9.3",
78
+ "vite": "^7.1.12",
79
+ "vite-plugin-dts": "^4.5.4"
80
+ }
81
+ }