@trackunit/react-modal 1.21.4 → 1.21.7
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/index.cjs.js +264 -159
- package/index.esm.js +268 -163
- package/package.json +4 -4
- package/src/modal/Modal.d.ts +14 -7
- package/src/modal/Modal.variants.d.ts +24 -2
- package/src/modal/modal-mode-switch-reducer.d.ts +18 -0
- package/src/modal/modalStackRegistry.d.ts +0 -5
- package/src/modal/useModal.d.ts +46 -94
- package/src/modal/useModalFooterBorder.d.ts +3 -10
- package/src/modal/useModalStack.d.ts +4 -5
package/src/modal/Modal.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Size } from "@trackunit/react-components";
|
|
2
|
-
import { PropsWithChildren, ReactElement } from "react";
|
|
3
|
-
import { UseModalReturnValue } from "./useModal";
|
|
1
|
+
import { Size, type SheetDefaultSize } from "@trackunit/react-components";
|
|
2
|
+
import { type PropsWithChildren, type ReactElement } from "react";
|
|
3
|
+
import type { UseModalReturnValue } from "./useModal";
|
|
4
4
|
/**
|
|
5
5
|
* Modal props extend the return type of useModal and add presentational-only props.
|
|
6
6
|
*/
|
|
@@ -17,6 +17,13 @@ export type ModalProps = PropsWithChildren<UseModalReturnValue & {
|
|
|
17
17
|
* The test ID applied to the modal.
|
|
18
18
|
*/
|
|
19
19
|
"data-testid"?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Initial snap size of the sheet when Modal renders in sheet mode.
|
|
22
|
+
* Only relevant on narrow viewports where the modal becomes a bottom sheet.
|
|
23
|
+
*
|
|
24
|
+
* @default "fit"
|
|
25
|
+
*/
|
|
26
|
+
sheetDefaultSize?: SheetDefaultSize;
|
|
20
27
|
/**
|
|
21
28
|
* Determines if focus should be restored to the nearest tabbable element
|
|
22
29
|
* if the currently focused element inside the modal is removed from the DOM.
|
|
@@ -30,6 +37,9 @@ export type ModalProps = PropsWithChildren<UseModalReturnValue & {
|
|
|
30
37
|
* It renders inside a Portal with a backdrop overlay, focus trapping, and proper accessibility roles.
|
|
31
38
|
* Modals must always be used together with the `useModal` hook, which manages open/close state and Floating UI integration.
|
|
32
39
|
*
|
|
40
|
+
* When the container width is below the "sm" breakpoint (480px), the Modal
|
|
41
|
+
* automatically renders as a bottom Sheet with gesture support.
|
|
42
|
+
*
|
|
33
43
|
* Compose the modal body with `ModalHeader`, `ModalBody`, and `ModalFooter` for consistent structure.
|
|
34
44
|
*
|
|
35
45
|
* ### When to use
|
|
@@ -60,7 +70,4 @@ export type ModalProps = PropsWithChildren<UseModalReturnValue & {
|
|
|
60
70
|
* @param {ModalProps} props - The props for the Modal component
|
|
61
71
|
* @returns {ReactElement} Modal component
|
|
62
72
|
*/
|
|
63
|
-
export declare const Modal: {
|
|
64
|
-
({ children, isOpen, role, "data-testid": dataTestId, className, size, floatingUi, ref, restoreFocus, depthFromFront, stackSizeAtOpen, }: ModalProps): ReactElement;
|
|
65
|
-
displayName: string;
|
|
66
|
-
};
|
|
73
|
+
export declare const Modal: ({ children, container, dismiss, isOpen, mode, requestClose, role, "data-testid": dataTestId, className, size, stack, floatingUi, ref, sheetDefaultSize, restoreFocus, onCloseComplete, }: ModalProps) => ReactElement;
|
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import { Size } from "@trackunit/react-components";
|
|
2
2
|
import { CSSProperties } from "react";
|
|
3
3
|
export declare const cvaModalContainer: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
4
|
+
type GetModalCardCSSVariablesOptions = Readonly<{
|
|
5
|
+
/**
|
|
6
|
+
* When true, width math uses container query units (`cqw`) so the card tracks
|
|
7
|
+
* the nearest `container-type` ancestor (e.g. Storybook `ResizableBox`) instead
|
|
8
|
+
* of the viewport. Pass when using `useModal({ container })`.
|
|
9
|
+
*/
|
|
10
|
+
useContainerWidthUnits: boolean;
|
|
11
|
+
}>;
|
|
4
12
|
/**
|
|
5
13
|
* Returns the CSS properties for the modal card based on the size.
|
|
6
14
|
*/
|
|
7
|
-
export declare const getModalCardCSSVariables: (size: Size) => CSSProperties;
|
|
15
|
+
export declare const getModalCardCSSVariables: (size: Size, options?: GetModalCardCSSVariablesOptions) => CSSProperties;
|
|
16
|
+
/**
|
|
17
|
+
* Layout contract that modal children depend on. Applied to the children's
|
|
18
|
+
* immediate container in both card mode (the Card) and sheet mode (the
|
|
19
|
+
* aria-modal wrapper). Ensures ModalHeader/ModalBody/ModalFooter experience
|
|
20
|
+
* an identical flex/container-query environment regardless of rendering mode.
|
|
21
|
+
*
|
|
22
|
+
* Does NOT include overflow-y — scrolling is owned by the parent:
|
|
23
|
+
* in card mode by the Card element (`cvaModalCard`), in sheet mode by the
|
|
24
|
+
* Sheet's scroll area. A nested overflow here would absorb content height,
|
|
25
|
+
* preventing Sheet's fit-height measurement from seeing the true size.
|
|
26
|
+
*/
|
|
27
|
+
export declare const cvaModalContentEnvironment: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
8
28
|
export declare const cvaModalCard: (props?: ({
|
|
9
|
-
animation?: "fade" | "rise" | null | undefined;
|
|
29
|
+
animation?: "fade" | "rise" | "none" | null | undefined;
|
|
10
30
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
11
31
|
export declare const cvaModalBackdrop: (props?: ({
|
|
32
|
+
contained?: boolean | null | undefined;
|
|
12
33
|
isFrontmost?: boolean | null | undefined;
|
|
13
34
|
shouldAnimate?: boolean | null | undefined;
|
|
14
35
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ModalMode } from "./useModal";
|
|
2
|
+
/**
|
|
3
|
+
* Tracks formfactor transitions (card ↔ sheet) while the modal is already
|
|
4
|
+
* open. `isModeSwitching` is true for the committed render after a mode
|
|
5
|
+
* change and resets when the modal closes.
|
|
6
|
+
*/
|
|
7
|
+
export type ModalModeSwitchState = {
|
|
8
|
+
readonly prevMode: ModalMode;
|
|
9
|
+
readonly prevIsOpen: boolean;
|
|
10
|
+
readonly isModeSwitching: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type ModalModeSwitchAction = {
|
|
13
|
+
readonly type: "SYNC";
|
|
14
|
+
readonly mode: ModalMode;
|
|
15
|
+
readonly isOpen: boolean;
|
|
16
|
+
};
|
|
17
|
+
/** Reducer tracking formfactor transitions (card/sheet) while the modal is open. */
|
|
18
|
+
export declare const modalModeSwitchReducer: (state: ModalModeSwitchState, action: ModalModeSwitchAction) => ModalModeSwitchState;
|
|
@@ -66,11 +66,6 @@
|
|
|
66
66
|
* - `stackSize`: Total modals open (local + host + iframe)
|
|
67
67
|
* - `stackSizeAtOpen`: Stack size when this modal opened (for animation decisions)
|
|
68
68
|
*
|
|
69
|
-
* This enables the Modal component to:
|
|
70
|
-
* - Scale down non-frontmost modals (visual stacking effect)
|
|
71
|
-
* - Show backdrop only on the frontmost modal
|
|
72
|
-
* - Choose appropriate animations based on whether it's opening over another modal
|
|
73
|
-
*
|
|
74
69
|
* @module modalStackRegistry
|
|
75
70
|
*/
|
|
76
71
|
/**
|
package/src/modal/useModal.d.ts
CHANGED
|
@@ -1,89 +1,33 @@
|
|
|
1
1
|
import { UseDismissProps, UseFloatingReturn, useInteractions } from "@floating-ui/react";
|
|
2
2
|
import { UseFloatingReturn as UseFloatingReturn_Dom } from "@floating-ui/react-dom";
|
|
3
|
-
import {
|
|
3
|
+
import { type CloseReason, type DismissOptions, type UseOverlayDismissibleProps, type UseOverlayDismissibleReturn } from "@trackunit/react-components";
|
|
4
|
+
import { type AriaRole, type Ref, type RefObject } from "react";
|
|
5
|
+
import { type UseModalStackResult } from "./useModalStack";
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
* - `
|
|
7
|
-
* - `
|
|
8
|
-
* - `programmatic`: The close() function was called (e.g., from a close button)
|
|
7
|
+
* Rendering mode of the Modal. Determined by container/viewport width:
|
|
8
|
+
* - `"card"`: standard centered card overlay (container width >= 480px)
|
|
9
|
+
* - `"sheet"`: bottom sheet with gesture support (container width < 480px)
|
|
9
10
|
*/
|
|
10
|
-
export type
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
* Return `false` (or Promise resolving to `false`) to prevent closing.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* // With confirmation dialog
|
|
17
|
-
* onBeforeClose: async (event, reason) => {
|
|
18
|
-
* if (reason === "escape-key") return true; // Allow ESC to close without confirmation
|
|
19
|
-
* const result = await confirm({ title: "Discard changes?" });
|
|
20
|
-
* return result === "PRIMARY";
|
|
21
|
-
* }
|
|
22
|
-
*/
|
|
23
|
-
type OnBeforeCloseFn = (event?: Event, reason?: ModalCloseReason) => boolean | Promise<boolean>;
|
|
11
|
+
export type ModalMode = "card" | "sheet";
|
|
12
|
+
/** @deprecated Use CloseReason from @trackunit/react-components instead. */
|
|
13
|
+
export type ModalCloseReason = CloseReason;
|
|
24
14
|
/**
|
|
25
15
|
* Options for configuring modal dismissal behavior.
|
|
26
|
-
*
|
|
16
|
+
* Extends shared DismissOptions with FloatingUI's useDismiss options.
|
|
27
17
|
*
|
|
28
18
|
* @see https://floating-ui.com/docs/usedismiss
|
|
29
19
|
*/
|
|
30
|
-
export type ModalDismissOptions = Omit<UseDismissProps, "enabled">;
|
|
31
|
-
export type UseModalProps = {
|
|
32
|
-
/**
|
|
33
|
-
* Whether the modal is open (controlled mode).
|
|
34
|
-
*
|
|
35
|
-
* Prefer uncontrolled mode using `modal.open()` and `modal.close()`.
|
|
36
|
-
*
|
|
37
|
-
* **Controlled mode limitations:**
|
|
38
|
-
* - `onBeforeClose` only fires for ESC, outside click, and `modal.close()` calls
|
|
39
|
-
* - `onBeforeClose` does NOT fire for browser back/forward, link navigation, or external state changes
|
|
40
|
-
*
|
|
41
|
-
* **Alternatives:**
|
|
42
|
-
* - Use `defaultOpen` to start the modal in open state
|
|
43
|
-
* - If you need to manage modal state from a parent, lift the whole `useModal()` instance to the parent
|
|
44
|
-
* - Only use `isOpen` for route-based modals where the modal is always open when the route is active
|
|
45
|
-
*/
|
|
46
|
-
isOpen?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Whether the modal should start open (uncontrolled mode).
|
|
49
|
-
* Unlike `isOpen`, this only sets the initial state and does not trigger `onOpen`.
|
|
50
|
-
*/
|
|
51
|
-
defaultOpen?: boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Callback fired when the modal closes.
|
|
54
|
-
*/
|
|
55
|
-
onClose?: (event?: Event, reason?: ModalCloseReason) => void;
|
|
56
|
-
/**
|
|
57
|
-
* Callback fired when the modal opens.
|
|
58
|
-
*/
|
|
59
|
-
onOpen?: (event?: Event) => void;
|
|
60
|
-
/**
|
|
61
|
-
* Callback fired when the modal open state changes.
|
|
62
|
-
* Alternative to using separate onOpen/onClose callbacks.
|
|
63
|
-
*/
|
|
64
|
-
onOpenChange?: (open: boolean, event?: Event, reason?: ModalCloseReason) => void;
|
|
65
|
-
/**
|
|
66
|
-
* Callback fired BEFORE the modal closes on ESC, outside click, or close().
|
|
67
|
-
* Return `false` (or Promise resolving to `false`) to prevent closing.
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* // With confirmation dialog for unsaved changes
|
|
71
|
-
* onBeforeClose: async () => {
|
|
72
|
-
* if (!isDirty) return true;
|
|
73
|
-
* const result = await confirm({ title: "Discard changes?" });
|
|
74
|
-
* return result === "PRIMARY";
|
|
75
|
-
* }
|
|
76
|
-
*/
|
|
77
|
-
onBeforeClose?: OnBeforeCloseFn;
|
|
20
|
+
export type ModalDismissOptions = DismissOptions & Omit<UseDismissProps, "enabled">;
|
|
21
|
+
export type UseModalProps = UseOverlayDismissibleProps & {
|
|
78
22
|
/**
|
|
79
23
|
* The root element to render the modal portal into.
|
|
80
24
|
*/
|
|
81
25
|
rootElement?: HTMLElement;
|
|
82
26
|
/**
|
|
83
27
|
* Configuration for modal dismissal behavior.
|
|
84
|
-
*
|
|
28
|
+
* Extends shared DismissOptions with FloatingUI options.
|
|
85
29
|
*
|
|
86
|
-
* @default { escapeKey: true, outsidePress: true }
|
|
30
|
+
* @default { escapeKey: true, outsidePress: true, gesture: true }
|
|
87
31
|
* @example
|
|
88
32
|
* // Disable outside click dismissal
|
|
89
33
|
* dismiss: { outsidePress: false }
|
|
@@ -97,6 +41,16 @@ export type UseModalProps = {
|
|
|
97
41
|
* The aria role for the modal.
|
|
98
42
|
*/
|
|
99
43
|
role?: AriaRole;
|
|
44
|
+
/**
|
|
45
|
+
* Scopes the modal to a specific container element. When provided, the modal
|
|
46
|
+
* portals into this container (instead of the document body), uses its width
|
|
47
|
+
* to determine whether to render as a bottom sheet (container width < 480px),
|
|
48
|
+
* and positions the backdrop/overlay relative to the container.
|
|
49
|
+
* Card width uses container query units (`cqw`) so it tracks a `container-type`
|
|
50
|
+
* ancestor of the portal root (ensure one exists, e.g. Tailwind `@container`).
|
|
51
|
+
* When omitted, viewport width is used for breakpoint detection and card sizing.
|
|
52
|
+
*/
|
|
53
|
+
container?: RefObject<HTMLElement | null>;
|
|
100
54
|
};
|
|
101
55
|
type FloatingUiProps = {
|
|
102
56
|
refs: UseFloatingReturn_Dom["refs"];
|
|
@@ -104,27 +58,11 @@ type FloatingUiProps = {
|
|
|
104
58
|
context: UseFloatingReturn["context"];
|
|
105
59
|
getFloatingProps: ReturnType<typeof useInteractions>["getFloatingProps"];
|
|
106
60
|
};
|
|
107
|
-
export type UseModalReturnValue = {
|
|
61
|
+
export type UseModalReturnValue = Omit<UseOverlayDismissibleReturn, "ref"> & {
|
|
108
62
|
/**
|
|
109
|
-
* The ref for the modal.
|
|
63
|
+
* The ref for the modal (RefObject or RefCallback).
|
|
110
64
|
*/
|
|
111
65
|
ref: Ref<HTMLDivElement>;
|
|
112
|
-
/**
|
|
113
|
-
* Whether the modal is open.
|
|
114
|
-
*/
|
|
115
|
-
isOpen: boolean;
|
|
116
|
-
/**
|
|
117
|
-
* A function to call for opening the modal.
|
|
118
|
-
*/
|
|
119
|
-
open: () => void;
|
|
120
|
-
/**
|
|
121
|
-
* A function to call for toggling the modal visibility.
|
|
122
|
-
*/
|
|
123
|
-
toggle: () => void;
|
|
124
|
-
/**
|
|
125
|
-
* A function to call for closing the modal.
|
|
126
|
-
*/
|
|
127
|
-
close: () => void;
|
|
128
66
|
/**
|
|
129
67
|
* The floating UI properties for the modal.
|
|
130
68
|
*/
|
|
@@ -134,15 +72,29 @@ export type UseModalReturnValue = {
|
|
|
134
72
|
*/
|
|
135
73
|
role?: AriaRole;
|
|
136
74
|
/**
|
|
137
|
-
*
|
|
138
|
-
|
|
75
|
+
* Resolved dismiss options (defaults applied). Pass to Sheet when in sheet mode.
|
|
76
|
+
*/
|
|
77
|
+
dismiss: Required<DismissOptions>;
|
|
78
|
+
/** Called after the modal's close animation has finished, allowing the iframe to resize back. */
|
|
79
|
+
onCloseComplete: () => void;
|
|
80
|
+
/** Position and sizing info for this modal within the open-modal stack. */
|
|
81
|
+
stack: UseModalStackResult;
|
|
82
|
+
/**
|
|
83
|
+
* Current rendering mode of the modal.
|
|
84
|
+
* - `"card"`: standard centered card overlay
|
|
85
|
+
* - `"sheet"`: bottom sheet with gesture support
|
|
86
|
+
*/
|
|
87
|
+
mode: ModalMode;
|
|
88
|
+
/**
|
|
89
|
+
* The container ref passed to useModal, if any. Forwarded to Modal for
|
|
90
|
+
* portal and positioning decisions.
|
|
139
91
|
*/
|
|
140
|
-
|
|
92
|
+
container: RefObject<HTMLElement | null> | undefined;
|
|
141
93
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
94
|
+
* Close the modal with a specific reason. Used internally by Modal
|
|
95
|
+
* to preserve the correct CloseReason for gesture-initiated closes.
|
|
144
96
|
*/
|
|
145
|
-
|
|
97
|
+
requestClose: (event: Event | undefined, reason: CloseReason) => void;
|
|
146
98
|
};
|
|
147
99
|
/**
|
|
148
100
|
* A hook to handle the state and configuration of Modal components.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RefObject } from "react";
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
2
|
type Options = {
|
|
3
3
|
footerClass?: string | Array<string>;
|
|
4
4
|
enabled?: boolean;
|
|
@@ -11,18 +11,11 @@ type ElementRefLike = ElementRef | RefObject<ElementRef>;
|
|
|
11
11
|
* Observes the modal body within the given root element and toggles a top border on the footer
|
|
12
12
|
* when the body becomes vertically scrollable.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* - Locates elements via stable data-attributes (by default: [data-modal-body], [data-modal-footer]).
|
|
16
|
-
* - Recomputes on resize, DOM mutations within the body, scroll events, and image load/error events.
|
|
17
|
-
* - Cleans up all observers/listeners on unmount or dependency change.
|
|
18
|
-
*
|
|
19
|
-
* Edge cases:
|
|
20
|
-
* - If either body or footer is missing, the hook does nothing.
|
|
21
|
-
* - No DOM mutations occur when `enabled` is false.
|
|
14
|
+
* Thin wrapper around `useOverflowBorder` with modal-specific defaults.
|
|
22
15
|
*
|
|
23
16
|
* @param rootRef Root element that contains both the modal body and footer.
|
|
24
17
|
* @param options Optional configuration.
|
|
25
|
-
* @param options.footerClass CSS class(es) toggled on the footer when the body is scrollable.
|
|
18
|
+
* @param options.footerClass CSS class(es) toggled on the footer when the body is scrollable. Defaults to "border-t".
|
|
26
19
|
* @param options.enabled Whether the hook is active. Defaults to true.
|
|
27
20
|
* @param options.bodySelector CSS selector to locate the body element. Defaults to "[data-modal-body]".
|
|
28
21
|
* @param options.footerSelector CSS selector to locate the footer element. Defaults to "[data-modal-footer]".
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
type UseModalStackResult = {
|
|
1
|
+
export type UseModalStackResult = {
|
|
2
2
|
/**
|
|
3
3
|
* How many modals are in front of this one (0 = frontmost)
|
|
4
4
|
*/
|
|
5
|
-
depthFromFront: number;
|
|
5
|
+
readonly depthFromFront: number;
|
|
6
6
|
/**
|
|
7
7
|
* Current number of open modals in the stack
|
|
8
8
|
*/
|
|
9
|
-
stackSize: number;
|
|
9
|
+
readonly stackSize: number;
|
|
10
10
|
/**
|
|
11
11
|
* The stack size when this modal opened (stable after open, useful for one-time decisions)
|
|
12
12
|
*/
|
|
13
|
-
stackSizeAtOpen: number;
|
|
13
|
+
readonly stackSizeAtOpen: number;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
16
|
* Hook to track this modal's position in the stack of open modals.
|
|
@@ -22,4 +22,3 @@ type UseModalStackResult = {
|
|
|
22
22
|
* @param isOpen - Whether the modal is currently open
|
|
23
23
|
*/
|
|
24
24
|
export declare const useModalStack: (isOpen: boolean) => UseModalStackResult;
|
|
25
|
-
export {};
|