@umamichi-ui/common-components 0.2.0 → 0.3.0

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;"}
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.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
+ }