@proyecto-viviana/solidaria-components 0.2.5 → 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.
- package/LICENSE +21 -0
- package/README.md +39 -272
- package/dist/ActionBar.d.ts +79 -0
- package/dist/ActionBar.d.ts.map +1 -0
- package/dist/ActionGroup.d.ts +74 -0
- package/dist/ActionGroup.d.ts.map +1 -0
- package/dist/Alert.d.ts +70 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Autocomplete.d.ts +5 -5
- package/dist/Autocomplete.d.ts.map +1 -1
- package/dist/Breadcrumbs.d.ts +27 -8
- package/dist/Breadcrumbs.d.ts.map +1 -1
- package/dist/Button.d.ts +28 -5
- package/dist/Button.d.ts.map +1 -1
- package/dist/Calendar.d.ts +51 -7
- package/dist/Calendar.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +33 -8
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/Collection.d.ts +130 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Color.d.ts +210 -9
- package/dist/Color.d.ts.map +1 -1
- package/dist/ColorEditor.d.ts +42 -0
- package/dist/ColorEditor.d.ts.map +1 -0
- package/dist/ComboBox.d.ts +146 -16
- package/dist/ComboBox.d.ts.map +1 -1
- package/dist/ContextualHelpTrigger.d.ts +40 -0
- package/dist/ContextualHelpTrigger.d.ts.map +1 -0
- package/dist/DateField.d.ts +35 -8
- package/dist/DateField.d.ts.map +1 -1
- package/dist/DatePicker.d.ts +101 -5
- package/dist/DatePicker.d.ts.map +1 -1
- package/dist/DateRangePickerContext.d.ts +30 -0
- package/dist/DateRangePickerContext.d.ts.map +1 -0
- package/dist/Dialog.d.ts +5 -5
- package/dist/Dialog.d.ts.map +1 -1
- package/dist/Disclosure.d.ts +25 -5
- package/dist/Disclosure.d.ts.map +1 -1
- package/dist/DragAndDrop.d.ts +80 -0
- package/dist/DragAndDrop.d.ts.map +1 -0
- package/dist/DragPreview.d.ts +14 -0
- package/dist/DragPreview.d.ts.map +1 -0
- package/dist/DropZone.d.ts +27 -0
- package/dist/DropZone.d.ts.map +1 -0
- package/dist/FieldError.d.ts +27 -0
- package/dist/FieldError.d.ts.map +1 -0
- package/dist/FileTrigger.d.ts +26 -0
- package/dist/FileTrigger.d.ts.map +1 -0
- package/dist/Focusable.d.ts +27 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Form.d.ts +41 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/GridList.d.ts +69 -10
- package/dist/GridList.d.ts.map +1 -1
- package/dist/HiddenDateInput.d.ts +26 -0
- package/dist/HiddenDateInput.d.ts.map +1 -0
- package/dist/HiddenTimeInput.d.ts +25 -0
- package/dist/HiddenTimeInput.d.ts.map +1 -0
- package/dist/Icon.d.ts +57 -0
- package/dist/Icon.d.ts.map +1 -0
- package/dist/Keyboard.d.ts +13 -0
- package/dist/Keyboard.d.ts.map +1 -0
- package/dist/Landmark.d.ts +3 -3
- package/dist/Landmark.d.ts.map +1 -1
- package/dist/Link.d.ts +10 -4
- package/dist/Link.d.ts.map +1 -1
- package/dist/ListBox.d.ts +73 -11
- package/dist/ListBox.d.ts.map +1 -1
- package/dist/ListDropTargetDelegate.d.ts +38 -0
- package/dist/ListDropTargetDelegate.d.ts.map +1 -0
- package/dist/Menu.d.ts +79 -10
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Meter.d.ts +4 -4
- package/dist/Meter.d.ts.map +1 -1
- package/dist/Modal.d.ts +6 -4
- package/dist/Modal.d.ts.map +1 -1
- package/dist/NumberField.d.ts +10 -12
- package/dist/NumberField.d.ts.map +1 -1
- package/dist/Popover.d.ts +32 -7
- package/dist/Popover.d.ts.map +1 -1
- package/dist/Pressable.d.ts +27 -0
- package/dist/Pressable.d.ts.map +1 -0
- package/dist/ProgressBar.d.ts +6 -4
- package/dist/ProgressBar.d.ts.map +1 -1
- package/dist/RadioGroup.d.ts +43 -9
- package/dist/RadioGroup.d.ts.map +1 -1
- package/dist/RangeCalendar.d.ts +39 -7
- package/dist/RangeCalendar.d.ts.map +1 -1
- package/dist/RouterProvider.d.ts +75 -0
- package/dist/RouterProvider.d.ts.map +1 -0
- package/dist/SearchField.d.ts +23 -21
- package/dist/SearchField.d.ts.map +1 -1
- package/dist/Select.d.ts +48 -7
- package/dist/Select.d.ts.map +1 -1
- package/dist/SelectionIndicator.d.ts +30 -0
- package/dist/SelectionIndicator.d.ts.map +1 -0
- package/dist/Separator.d.ts +9 -3
- package/dist/Separator.d.ts.map +1 -1
- package/dist/SharedElementTransition.d.ts +41 -0
- package/dist/SharedElementTransition.d.ts.map +1 -0
- package/dist/Slider.d.ts +15 -8
- package/dist/Slider.d.ts.map +1 -1
- package/dist/StepList.d.ts +90 -0
- package/dist/StepList.d.ts.map +1 -0
- package/dist/Switch.d.ts +11 -5
- package/dist/Switch.d.ts.map +1 -1
- package/dist/Table.d.ts +222 -19
- package/dist/Table.d.ts.map +1 -1
- package/dist/Tabs.d.ts +47 -10
- package/dist/Tabs.d.ts.map +1 -1
- package/dist/TagGroup.d.ts +22 -10
- package/dist/TagGroup.d.ts.map +1 -1
- package/dist/Text.d.ts +10 -0
- package/dist/Text.d.ts.map +1 -0
- package/dist/TextField.d.ts +19 -11
- package/dist/TextField.d.ts.map +1 -1
- package/dist/TimeField.d.ts +32 -7
- package/dist/TimeField.d.ts.map +1 -1
- package/dist/Toast.d.ts +29 -14
- package/dist/Toast.d.ts.map +1 -1
- package/dist/ToggleButton.d.ts +36 -0
- package/dist/ToggleButton.d.ts.map +1 -0
- package/dist/ToggleButtonGroup.d.ts +33 -0
- package/dist/ToggleButtonGroup.d.ts.map +1 -0
- package/dist/Toolbar.d.ts +7 -3
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Tooltip.d.ts +58 -7
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/Tree.d.ts +102 -11
- package/dist/Tree.d.ts.map +1 -1
- package/dist/Virtualizer.d.ts +61 -0
- package/dist/Virtualizer.d.ts.map +1 -0
- package/dist/VirtualizerLayouts.d.ts +82 -0
- package/dist/VirtualizerLayouts.d.ts.map +1 -0
- package/dist/VisuallyHidden.d.ts +4 -2
- package/dist/VisuallyHidden.d.ts.map +1 -1
- package/dist/contexts.d.ts +6 -1
- package/dist/contexts.d.ts.map +1 -1
- package/dist/index.d.ts +73 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23342 -10644
- package/dist/index.js.map +1 -7
- package/dist/index.jsx +18110 -0
- package/dist/index.jsx.map +1 -0
- package/dist/useDragAndDrop.d.ts +93 -0
- package/dist/useDragAndDrop.d.ts.map +1 -0
- package/dist/utils.d.ts +8 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/virtualizer/Layout.d.ts +79 -0
- package/dist/virtualizer/Layout.d.ts.map +1 -0
- package/package.json +33 -32
- package/src/ActionBar.tsx +251 -0
- package/src/ActionGroup.tsx +277 -0
- package/src/Alert.tsx +152 -0
- package/src/Autocomplete.tsx +39 -44
- package/src/Breadcrumbs.tsx +227 -72
- package/src/Button.tsx +315 -74
- package/src/Calendar.tsx +347 -141
- package/src/Checkbox.tsx +414 -123
- package/src/Collection.tsx +350 -0
- package/src/Color.tsx +1325 -284
- package/src/ColorEditor.tsx +213 -0
- package/src/ComboBox.tsx +644 -245
- package/src/ContextualHelpTrigger.tsx +195 -0
- package/src/DateField.tsx +274 -106
- package/src/DatePicker.tsx +892 -111
- package/src/DateRangePickerContext.tsx +44 -0
- package/src/Dialog.tsx +173 -104
- package/src/Disclosure.tsx +158 -105
- package/src/DragAndDrop.tsx +340 -0
- package/src/DragPreview.tsx +47 -0
- package/src/DropZone.tsx +233 -0
- package/src/FieldError.tsx +89 -0
- package/src/FileTrigger.tsx +83 -0
- package/src/Focusable.tsx +103 -0
- package/src/Form.tsx +140 -0
- package/src/GridList.tsx +542 -128
- package/src/HiddenDateInput.tsx +153 -0
- package/src/HiddenTimeInput.tsx +133 -0
- package/src/Icon.tsx +133 -0
- package/src/Keyboard.tsx +26 -0
- package/src/Landmark.tsx +37 -63
- package/src/Link.tsx +132 -69
- package/src/ListBox.tsx +656 -106
- package/src/ListDropTargetDelegate.ts +283 -0
- package/src/Menu.tsx +1234 -132
- package/src/Meter.tsx +44 -58
- package/src/Modal.tsx +262 -166
- package/src/NumberField.tsx +267 -151
- package/src/Popover.tsx +452 -343
- package/src/Pressable.tsx +108 -0
- package/src/ProgressBar.tsx +54 -59
- package/src/RadioGroup.tsx +533 -121
- package/src/RangeCalendar.tsx +249 -150
- package/src/RouterProvider.tsx +223 -0
- package/src/SearchField.tsx +460 -133
- package/src/Select.tsx +804 -233
- package/src/SelectionIndicator.tsx +108 -0
- package/src/Separator.tsx +47 -49
- package/src/SharedElementTransition.tsx +264 -0
- package/src/Slider.tsx +148 -98
- package/src/StepList.tsx +272 -0
- package/src/Switch.tsx +93 -46
- package/src/Table.tsx +1551 -225
- package/src/Tabs.tsx +377 -123
- package/src/TagGroup.tsx +233 -135
- package/src/Text.tsx +18 -0
- package/src/TextField.tsx +413 -86
- package/src/TimeField.tsx +232 -222
- package/src/Toast.tsx +306 -160
- package/src/ToggleButton.tsx +169 -0
- package/src/ToggleButtonGroup.tsx +141 -0
- package/src/Toolbar.tsx +61 -70
- package/src/Tooltip.tsx +473 -116
- package/src/Tree.tsx +1514 -175
- package/src/Virtualizer.tsx +730 -0
- package/src/VirtualizerLayouts.ts +280 -0
- package/src/VisuallyHidden.tsx +32 -38
- package/src/contexts.ts +29 -36
- package/src/index.ts +972 -620
- package/src/useDragAndDrop.ts +367 -0
- package/src/utils.tsx +69 -50
- package/src/virtualizer/Layout.ts +192 -0
- package/dist/index.ssr.js +0 -9785
- package/dist/index.ssr.js.map +0 -7
package/src/Modal.tsx
CHANGED
|
@@ -15,13 +15,14 @@ import {
|
|
|
15
15
|
splitProps,
|
|
16
16
|
Show,
|
|
17
17
|
useContext,
|
|
18
|
-
} from
|
|
19
|
-
import { Portal, isServer } from
|
|
18
|
+
} from "solid-js";
|
|
19
|
+
import { Portal, isServer } from "solid-js/web";
|
|
20
20
|
import {
|
|
21
21
|
createInteractOutside,
|
|
22
22
|
ariaHideOutside,
|
|
23
23
|
FocusScope,
|
|
24
|
-
|
|
24
|
+
useUNSAFE_PortalContext,
|
|
25
|
+
} from "@proyecto-viviana/solidaria";
|
|
25
26
|
import {
|
|
26
27
|
type RenderChildren,
|
|
27
28
|
type ClassNameOrFunction,
|
|
@@ -29,197 +30,234 @@ import {
|
|
|
29
30
|
useRenderProps,
|
|
30
31
|
filterDOMProps,
|
|
31
32
|
dataAttr,
|
|
32
|
-
|
|
33
|
+
useIsHydrated,
|
|
34
|
+
} from "./utils";
|
|
33
35
|
import {
|
|
34
36
|
DialogTriggerContext,
|
|
35
37
|
OverlayTriggerStateContext,
|
|
36
38
|
type OverlayTriggerState,
|
|
37
|
-
} from
|
|
38
|
-
|
|
39
|
-
// ============================================
|
|
40
|
-
// INTERNAL CONTEXT
|
|
41
|
-
// ============================================
|
|
39
|
+
} from "./contexts";
|
|
42
40
|
|
|
43
41
|
/**
|
|
44
42
|
* Internal context to signal that Modal is wrapped in ModalOverlay.
|
|
45
43
|
* When present, Modal should not create its own Portal.
|
|
46
44
|
*/
|
|
47
45
|
interface InternalModalContextValue {
|
|
48
|
-
isDismissable?: boolean
|
|
49
|
-
isKeyboardDismissDisabled?: boolean
|
|
46
|
+
isDismissable?: boolean;
|
|
47
|
+
isKeyboardDismissDisabled?: boolean;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
const InternalModalContext = createContext<InternalModalContextValue | null>(null)
|
|
50
|
+
const InternalModalContext = createContext<InternalModalContextValue | null>(null);
|
|
51
|
+
|
|
52
|
+
// Stack of visible modals, used to ensure only the top-most modal dismisses on Escape/outside interaction.
|
|
53
|
+
const visibleModals: Array<() => Element | null> = [];
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
function pruneDisconnectedModals() {
|
|
56
|
+
for (let index = visibleModals.length - 1; index >= 0; index -= 1) {
|
|
57
|
+
const element = visibleModals[index]?.();
|
|
58
|
+
if (!element?.isConnected) {
|
|
59
|
+
visibleModals.splice(index, 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
export interface ModalRenderProps {
|
|
59
65
|
/** Whether the modal is currently entering (for animations). */
|
|
60
|
-
isEntering: boolean
|
|
66
|
+
isEntering: boolean;
|
|
61
67
|
/** Whether the modal is currently exiting (for animations). */
|
|
62
|
-
isExiting: boolean
|
|
68
|
+
isExiting: boolean;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export interface ModalOverlayProps {
|
|
66
72
|
/** The children of the component - can be JSX or render function. */
|
|
67
|
-
children?: RenderChildren<ModalRenderProps
|
|
73
|
+
children?: RenderChildren<ModalRenderProps>;
|
|
68
74
|
/** The CSS className for the element. */
|
|
69
|
-
class?: ClassNameOrFunction<ModalRenderProps
|
|
75
|
+
class?: ClassNameOrFunction<ModalRenderProps>;
|
|
70
76
|
/** The inline style for the element. */
|
|
71
|
-
style?: StyleOrFunction<ModalRenderProps
|
|
77
|
+
style?: StyleOrFunction<ModalRenderProps>;
|
|
72
78
|
/** Whether the modal is open (controlled). */
|
|
73
|
-
isOpen?: boolean
|
|
79
|
+
isOpen?: boolean;
|
|
74
80
|
/** Whether the modal opens by default (uncontrolled). */
|
|
75
|
-
defaultOpen?: boolean
|
|
81
|
+
defaultOpen?: boolean;
|
|
76
82
|
/** Handler called when the modal's open state changes. */
|
|
77
|
-
onOpenChange?: (isOpen: boolean) => void
|
|
83
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
78
84
|
/** Whether clicking outside the modal closes it. */
|
|
79
|
-
isDismissable?: boolean
|
|
85
|
+
isDismissable?: boolean;
|
|
80
86
|
/** Whether pressing Escape closes the modal. */
|
|
81
|
-
isKeyboardDismissDisabled?: boolean
|
|
87
|
+
isKeyboardDismissDisabled?: boolean;
|
|
82
88
|
/** Whether the modal is entering (for animations). */
|
|
83
|
-
isEntering?: boolean
|
|
89
|
+
isEntering?: boolean;
|
|
84
90
|
/** Whether the modal is exiting (for animations). */
|
|
85
|
-
isExiting?: boolean
|
|
91
|
+
isExiting?: boolean;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export interface ModalProps extends ModalOverlayProps {}
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
export {
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
// ============================================
|
|
95
|
-
// MODAL OVERLAY COMPONENT
|
|
96
|
-
// ============================================
|
|
96
|
+
export { OverlayTriggerStateContext, type OverlayTriggerState } from "./contexts";
|
|
97
|
+
export { useOverlayTriggerState } from "./contexts";
|
|
98
|
+
export const ModalContext = OverlayTriggerStateContext;
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
101
|
* ModalOverlay is the backdrop/underlay behind a modal.
|
|
100
102
|
* It handles click-outside dismissal and provides styling hooks.
|
|
101
103
|
*/
|
|
102
104
|
export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
// Do NOT early-return on the server: rendering children bare on the server and a
|
|
106
|
+
// <Show>/<Portal> overlay on the client desyncs hydration. Run the same structure
|
|
107
|
+
// on both and gate the Portal on useIsHydrated() (see Popover for the rationale).
|
|
108
|
+
const isHydrated = useIsHydrated();
|
|
106
109
|
|
|
107
110
|
// IMPORTANT: Don't destructure or access props.children early!
|
|
108
111
|
// In SolidJS, children are lazily evaluated. Accessing them before
|
|
109
112
|
// the context provider renders causes them to evaluate outside the context.
|
|
110
113
|
// See: https://github.com/solidjs/solid/issues/182
|
|
111
114
|
const [local, rest] = splitProps(props, [
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
])
|
|
115
|
+
"class",
|
|
116
|
+
"style",
|
|
117
|
+
"isOpen",
|
|
118
|
+
"defaultOpen",
|
|
119
|
+
"onOpenChange",
|
|
120
|
+
"isDismissable",
|
|
121
|
+
"isKeyboardDismissDisabled",
|
|
122
|
+
"isEntering",
|
|
123
|
+
"isExiting",
|
|
124
|
+
]);
|
|
122
125
|
|
|
123
126
|
// Get state from DialogTrigger context if available
|
|
124
|
-
const dialogTriggerContext = useContext(DialogTriggerContext)
|
|
127
|
+
const dialogTriggerContext = useContext(DialogTriggerContext);
|
|
125
128
|
|
|
126
129
|
// Internal state for uncontrolled mode
|
|
127
|
-
const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false)
|
|
130
|
+
const [internalOpen, setInternalOpen] = createSignal(local.defaultOpen ?? false);
|
|
128
131
|
|
|
129
132
|
// Determine if open (controlled > DialogTrigger context > uncontrolled)
|
|
130
133
|
const isOpen = (): boolean => {
|
|
131
|
-
if (local.isOpen !== undefined) return local.isOpen
|
|
132
|
-
if (dialogTriggerContext) return dialogTriggerContext.state.isOpen()
|
|
133
|
-
return internalOpen()
|
|
134
|
-
}
|
|
134
|
+
if (local.isOpen !== undefined) return local.isOpen;
|
|
135
|
+
if (dialogTriggerContext) return dialogTriggerContext.state.isOpen();
|
|
136
|
+
return internalOpen();
|
|
137
|
+
};
|
|
135
138
|
|
|
136
139
|
const close = () => {
|
|
137
140
|
if (local.isOpen !== undefined) {
|
|
138
|
-
local.onOpenChange?.(false)
|
|
141
|
+
local.onOpenChange?.(false);
|
|
139
142
|
} else if (dialogTriggerContext) {
|
|
140
|
-
dialogTriggerContext.state.close()
|
|
141
|
-
local.onOpenChange?.(false)
|
|
143
|
+
dialogTriggerContext.state.close();
|
|
144
|
+
local.onOpenChange?.(false);
|
|
142
145
|
} else {
|
|
143
|
-
setInternalOpen(false)
|
|
144
|
-
local.onOpenChange?.(false)
|
|
146
|
+
setInternalOpen(false);
|
|
147
|
+
local.onOpenChange?.(false);
|
|
145
148
|
}
|
|
146
|
-
}
|
|
149
|
+
};
|
|
147
150
|
|
|
148
151
|
const open = () => {
|
|
149
152
|
if (local.isOpen !== undefined) {
|
|
150
|
-
local.onOpenChange?.(true)
|
|
153
|
+
local.onOpenChange?.(true);
|
|
151
154
|
} else if (dialogTriggerContext) {
|
|
152
|
-
dialogTriggerContext.state.open()
|
|
153
|
-
local.onOpenChange?.(true)
|
|
155
|
+
dialogTriggerContext.state.open();
|
|
156
|
+
local.onOpenChange?.(true);
|
|
154
157
|
} else {
|
|
155
|
-
setInternalOpen(true)
|
|
156
|
-
local.onOpenChange?.(true)
|
|
158
|
+
setInternalOpen(true);
|
|
159
|
+
local.onOpenChange?.(true);
|
|
157
160
|
}
|
|
158
|
-
}
|
|
161
|
+
};
|
|
159
162
|
|
|
160
163
|
const toggle = () => {
|
|
161
164
|
if (isOpen()) {
|
|
162
|
-
close()
|
|
165
|
+
close();
|
|
163
166
|
} else {
|
|
164
|
-
open()
|
|
167
|
+
open();
|
|
165
168
|
}
|
|
166
|
-
}
|
|
169
|
+
};
|
|
167
170
|
|
|
168
|
-
// Create overlay trigger state for context
|
|
169
171
|
const state: OverlayTriggerState = {
|
|
170
|
-
get isOpen() {
|
|
172
|
+
get isOpen() {
|
|
173
|
+
return isOpen();
|
|
174
|
+
},
|
|
171
175
|
open,
|
|
172
176
|
close,
|
|
173
177
|
toggle,
|
|
174
|
-
}
|
|
178
|
+
};
|
|
175
179
|
|
|
176
|
-
// Render props values
|
|
177
180
|
const renderValues = createMemo<ModalRenderProps>(() => ({
|
|
178
181
|
isEntering: local.isEntering ?? false,
|
|
179
182
|
isExiting: local.isExiting ?? false,
|
|
180
|
-
}))
|
|
183
|
+
}));
|
|
181
184
|
|
|
182
185
|
// Resolve render props - don't pass children, we'll render props.children directly
|
|
183
186
|
const renderProps = useRenderProps(
|
|
184
187
|
{
|
|
185
188
|
class: local.class,
|
|
186
189
|
style: local.style,
|
|
187
|
-
defaultClassName:
|
|
190
|
+
defaultClassName: "solidaria-ModalOverlay",
|
|
188
191
|
},
|
|
189
|
-
renderValues
|
|
190
|
-
)
|
|
192
|
+
renderValues,
|
|
193
|
+
);
|
|
191
194
|
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
const domProps = createMemo(() =>
|
|
196
|
+
filterDOMProps(rest as Record<string, unknown>, { global: true }),
|
|
197
|
+
);
|
|
194
198
|
|
|
195
199
|
// Internal context value to signal Modal that it's wrapped
|
|
196
200
|
const internalModalContext: InternalModalContextValue = {
|
|
197
201
|
isDismissable: local.isDismissable,
|
|
198
202
|
isKeyboardDismissDisabled: local.isKeyboardDismissDisabled,
|
|
199
|
-
}
|
|
203
|
+
};
|
|
204
|
+
const portalContext = useUNSAFE_PortalContext();
|
|
205
|
+
const portalContainer = () => portalContext.getContainer?.() ?? undefined;
|
|
206
|
+
let overlayRef!: HTMLDivElement;
|
|
207
|
+
|
|
208
|
+
const isTopMostModalInOverlay = () => {
|
|
209
|
+
pruneDisconnectedModals();
|
|
210
|
+
const topMostModal = visibleModals[visibleModals.length - 1]?.();
|
|
211
|
+
return !topMostModal || overlayRef?.contains(topMostModal);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleOverlayPointerDown: JSX.EventHandler<HTMLDivElement, PointerEvent> = (event) => {
|
|
215
|
+
if (local.isDismissable && event.target === event.currentTarget && isTopMostModalInOverlay()) {
|
|
216
|
+
close();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
createEffect(() => {
|
|
221
|
+
if (!isOpen() || local.isKeyboardDismissDisabled) return;
|
|
222
|
+
|
|
223
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
224
|
+
if (event.key === "Escape" && !event.isComposing && isTopMostModalInOverlay()) {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
event.stopPropagation();
|
|
227
|
+
close();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
232
|
+
onCleanup(() => {
|
|
233
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
200
236
|
|
|
201
237
|
// Resolve children - handle both static JSX and render functions
|
|
202
238
|
// IMPORTANT: We access props.children directly (not local.children) to preserve
|
|
203
239
|
// lazy evaluation inside context providers
|
|
204
240
|
const resolveChildren = () => {
|
|
205
|
-
const children = props.children
|
|
206
|
-
if (typeof children ===
|
|
207
|
-
return (children as (props: ModalRenderProps) => JSX.Element)(renderValues())
|
|
241
|
+
const children = props.children;
|
|
242
|
+
if (typeof children === "function") {
|
|
243
|
+
return (children as (props: ModalRenderProps) => JSX.Element)(renderValues());
|
|
208
244
|
}
|
|
209
|
-
return children
|
|
210
|
-
}
|
|
245
|
+
return children;
|
|
246
|
+
};
|
|
211
247
|
|
|
212
248
|
return (
|
|
213
|
-
<Show when={isOpen() || local.isExiting}>
|
|
214
|
-
<Portal>
|
|
249
|
+
<Show when={isHydrated() && (isOpen() || local.isExiting)}>
|
|
250
|
+
<Portal mount={portalContainer()}>
|
|
215
251
|
<OverlayTriggerStateContext.Provider value={state}>
|
|
216
252
|
<InternalModalContext.Provider value={internalModalContext}>
|
|
217
253
|
<div
|
|
218
254
|
{...domProps()}
|
|
255
|
+
ref={overlayRef}
|
|
219
256
|
class={renderProps.class()}
|
|
220
257
|
style={renderProps.style()}
|
|
221
258
|
data-entering={dataAttr(local.isEntering)}
|
|
222
259
|
data-exiting={dataAttr(local.isExiting)}
|
|
260
|
+
onPointerDown={handleOverlayPointerDown}
|
|
223
261
|
>
|
|
224
262
|
{resolveChildren()}
|
|
225
263
|
</div>
|
|
@@ -227,13 +265,9 @@ export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
|
|
|
227
265
|
</OverlayTriggerStateContext.Provider>
|
|
228
266
|
</Portal>
|
|
229
267
|
</Show>
|
|
230
|
-
)
|
|
268
|
+
);
|
|
231
269
|
}
|
|
232
270
|
|
|
233
|
-
// ============================================
|
|
234
|
-
// MODAL COMPONENT
|
|
235
|
-
// ============================================
|
|
236
|
-
|
|
237
271
|
/**
|
|
238
272
|
* Modal is a dialog container that manages focus trapping, scroll prevention,
|
|
239
273
|
* aria-hiding of content outside, and dismissal.
|
|
@@ -249,7 +283,7 @@ export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
|
|
|
249
283
|
export function Modal(props: ModalProps): JSX.Element {
|
|
250
284
|
// Check for InternalModalContext which signals we're inside a rendered ModalOverlay
|
|
251
285
|
// This works because ModalContent is rendered INSIDE ModalOverlay's Show/Portal
|
|
252
|
-
return <ModalContentWithAutoOverlay {...props}
|
|
286
|
+
return <ModalContentWithAutoOverlay {...props} />;
|
|
253
287
|
}
|
|
254
288
|
|
|
255
289
|
/**
|
|
@@ -259,17 +293,17 @@ export function Modal(props: ModalProps): JSX.Element {
|
|
|
259
293
|
*/
|
|
260
294
|
function ModalContentWithAutoOverlay(props: ModalProps): JSX.Element {
|
|
261
295
|
const [overlayProps, modalProps] = splitProps(props, [
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
])
|
|
296
|
+
"isOpen",
|
|
297
|
+
"defaultOpen",
|
|
298
|
+
"onOpenChange",
|
|
299
|
+
"isDismissable",
|
|
300
|
+
"isKeyboardDismissDisabled",
|
|
301
|
+
"isEntering",
|
|
302
|
+
"isExiting",
|
|
303
|
+
]);
|
|
270
304
|
|
|
271
305
|
// Check for InternalModalContext - if present, we're inside a ModalOverlay
|
|
272
|
-
const internalContext = useContext(InternalModalContext)
|
|
306
|
+
const internalContext = useContext(InternalModalContext);
|
|
273
307
|
|
|
274
308
|
// If wrapped in ModalOverlay, just render the content
|
|
275
309
|
if (internalContext) {
|
|
@@ -277,14 +311,14 @@ function ModalContentWithAutoOverlay(props: ModalProps): JSX.Element {
|
|
|
277
311
|
<ModalContent {...modalProps} internalContext={internalContext}>
|
|
278
312
|
{props.children}
|
|
279
313
|
</ModalContent>
|
|
280
|
-
)
|
|
314
|
+
);
|
|
281
315
|
}
|
|
282
316
|
|
|
283
317
|
// For standalone usage, wrap in ModalOverlay
|
|
284
318
|
const standaloneContext: InternalModalContextValue = {
|
|
285
319
|
isDismissable: overlayProps.isDismissable,
|
|
286
320
|
isKeyboardDismissDisabled: overlayProps.isKeyboardDismissDisabled,
|
|
287
|
-
}
|
|
321
|
+
};
|
|
288
322
|
|
|
289
323
|
return (
|
|
290
324
|
<ModalOverlay {...overlayProps}>
|
|
@@ -292,127 +326,170 @@ function ModalContentWithAutoOverlay(props: ModalProps): JSX.Element {
|
|
|
292
326
|
{props.children}
|
|
293
327
|
</ModalContent>
|
|
294
328
|
</ModalOverlay>
|
|
295
|
-
)
|
|
329
|
+
);
|
|
296
330
|
}
|
|
297
331
|
|
|
298
332
|
/**
|
|
299
333
|
* Internal component that renders the actual modal content.
|
|
300
334
|
* Used by both standalone Modal and Modal wrapped in ModalOverlay.
|
|
301
335
|
*/
|
|
302
|
-
function ModalContent(
|
|
336
|
+
function ModalContent(
|
|
337
|
+
props: ModalProps & { internalContext: InternalModalContextValue },
|
|
338
|
+
): JSX.Element {
|
|
303
339
|
if (isServer) {
|
|
304
|
-
return <>{props.children}
|
|
340
|
+
return <>{props.children}</>;
|
|
305
341
|
}
|
|
306
342
|
|
|
307
343
|
const [local, rest] = splitProps(props, [
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
])
|
|
320
|
-
|
|
321
|
-
let modalRef!: HTMLDivElement
|
|
344
|
+
"children",
|
|
345
|
+
"class",
|
|
346
|
+
"style",
|
|
347
|
+
"isOpen",
|
|
348
|
+
"defaultOpen",
|
|
349
|
+
"onOpenChange",
|
|
350
|
+
"isDismissable",
|
|
351
|
+
"isKeyboardDismissDisabled",
|
|
352
|
+
"isEntering",
|
|
353
|
+
"isExiting",
|
|
354
|
+
"internalContext",
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
let modalRef!: HTMLDivElement;
|
|
358
|
+
const modalRefAccessor = () => modalRef ?? null;
|
|
322
359
|
|
|
323
360
|
// Get state from parent OverlayTriggerStateContext (provided by ModalOverlay)
|
|
324
|
-
const parentState = useContext(OverlayTriggerStateContext)
|
|
361
|
+
const parentState = useContext(OverlayTriggerStateContext);
|
|
325
362
|
|
|
326
363
|
// Get dismissable settings from internal context (set by ModalOverlay)
|
|
327
|
-
const isDismissable = () => local.internalContext?.isDismissable ?? local.isDismissable
|
|
328
|
-
const isKeyboardDismissDisabled = () =>
|
|
364
|
+
const isDismissable = () => local.internalContext?.isDismissable ?? local.isDismissable;
|
|
365
|
+
const isKeyboardDismissDisabled = () =>
|
|
366
|
+
local.internalContext?.isKeyboardDismissDisabled ?? local.isKeyboardDismissDisabled;
|
|
329
367
|
|
|
330
368
|
// Determine if open from parent state
|
|
331
369
|
const isOpen = (): boolean => {
|
|
332
|
-
if (local.isOpen !== undefined) return local.isOpen
|
|
333
|
-
return parentState?.isOpen ?? false
|
|
334
|
-
}
|
|
370
|
+
if (local.isOpen !== undefined) return local.isOpen;
|
|
371
|
+
return parentState?.isOpen ?? false;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Keep this modal in a global stack so nested modals dismiss in top-down order.
|
|
375
|
+
createEffect(() => {
|
|
376
|
+
if (!isOpen()) return;
|
|
377
|
+
|
|
378
|
+
pruneDisconnectedModals();
|
|
379
|
+
if (!visibleModals.includes(modalRefAccessor)) {
|
|
380
|
+
visibleModals.push(modalRefAccessor);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
onCleanup(() => {
|
|
384
|
+
const index = visibleModals.indexOf(modalRefAccessor);
|
|
385
|
+
if (index >= 0) {
|
|
386
|
+
visibleModals.splice(index, 1);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const isTopMostModal = () => {
|
|
392
|
+
pruneDisconnectedModals();
|
|
393
|
+
return visibleModals[visibleModals.length - 1] === modalRefAccessor;
|
|
394
|
+
};
|
|
335
395
|
|
|
336
396
|
const close = () => {
|
|
337
397
|
if (local.isOpen !== undefined) {
|
|
338
|
-
local.onOpenChange?.(false)
|
|
398
|
+
local.onOpenChange?.(false);
|
|
339
399
|
} else {
|
|
340
|
-
parentState?.close()
|
|
400
|
+
parentState?.close();
|
|
341
401
|
}
|
|
342
|
-
}
|
|
402
|
+
};
|
|
343
403
|
|
|
344
404
|
// Prevent scroll when modal is open
|
|
345
405
|
createEffect(() => {
|
|
346
|
-
if (!isOpen()) return
|
|
406
|
+
if (!isOpen()) return;
|
|
347
407
|
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
html.style.overflow = 'hidden'
|
|
408
|
+
const html = document.documentElement;
|
|
409
|
+
const prevOverflow = html.style.overflow;
|
|
410
|
+
html.style.overflow = "hidden";
|
|
352
411
|
|
|
353
412
|
onCleanup(() => {
|
|
354
|
-
html.style.overflow = prevOverflow
|
|
355
|
-
})
|
|
356
|
-
})
|
|
413
|
+
html.style.overflow = prevOverflow;
|
|
414
|
+
});
|
|
415
|
+
});
|
|
357
416
|
|
|
358
417
|
// Click outside to close (if dismissable)
|
|
359
418
|
createEffect(() => {
|
|
360
|
-
if (!isOpen() || !isDismissable()) return
|
|
419
|
+
if (!isOpen() || !isDismissable()) return;
|
|
361
420
|
|
|
362
421
|
createInteractOutside({
|
|
363
|
-
ref:
|
|
422
|
+
ref: modalRefAccessor,
|
|
364
423
|
onInteractOutside: () => {
|
|
365
|
-
|
|
424
|
+
if (isTopMostModal()) {
|
|
425
|
+
close();
|
|
426
|
+
}
|
|
366
427
|
},
|
|
367
428
|
isDisabled: false,
|
|
368
|
-
})
|
|
369
|
-
})
|
|
429
|
+
});
|
|
430
|
+
});
|
|
370
431
|
|
|
371
432
|
// Escape key to close
|
|
372
433
|
createEffect(() => {
|
|
373
|
-
if (!isOpen() || isKeyboardDismissDisabled()) return
|
|
434
|
+
if (!isOpen() || isKeyboardDismissDisabled()) return;
|
|
374
435
|
|
|
375
436
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
376
|
-
if (e.key ===
|
|
377
|
-
e.preventDefault()
|
|
378
|
-
e.stopPropagation()
|
|
379
|
-
close()
|
|
437
|
+
if (e.key === "Escape" && !e.isComposing && isTopMostModal()) {
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
e.stopPropagation();
|
|
440
|
+
close();
|
|
380
441
|
}
|
|
381
|
-
}
|
|
442
|
+
};
|
|
382
443
|
|
|
383
|
-
document.addEventListener(
|
|
444
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
384
445
|
onCleanup(() => {
|
|
385
|
-
document.removeEventListener(
|
|
386
|
-
})
|
|
387
|
-
})
|
|
446
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
388
449
|
|
|
389
450
|
// Aria-hide outside content
|
|
390
451
|
createEffect(() => {
|
|
391
|
-
if (!isOpen() || !modalRef) return
|
|
452
|
+
if (!isOpen() || !modalRef) return;
|
|
453
|
+
|
|
454
|
+
let cleanup: (() => void) | undefined;
|
|
455
|
+
let cancelled = false;
|
|
456
|
+
const ownerWindow = modalRef.ownerDocument.defaultView ?? window;
|
|
392
457
|
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
458
|
+
const hideOutside = () => {
|
|
459
|
+
if (cancelled || !modalRef?.isConnected) return;
|
|
460
|
+
cleanup = ariaHideOutside([modalRef]);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
if (modalRef.isConnected) {
|
|
464
|
+
hideOutside();
|
|
465
|
+
} else {
|
|
466
|
+
ownerWindow.queueMicrotask(hideOutside);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
onCleanup(() => {
|
|
470
|
+
cancelled = true;
|
|
471
|
+
cleanup?.();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
396
474
|
|
|
397
|
-
// Render props values
|
|
398
475
|
const renderValues = createMemo<ModalRenderProps>(() => ({
|
|
399
476
|
isEntering: local.isEntering ?? false,
|
|
400
477
|
isExiting: local.isExiting ?? false,
|
|
401
|
-
}))
|
|
478
|
+
}));
|
|
402
479
|
|
|
403
|
-
// Resolve render props
|
|
404
480
|
const renderProps = useRenderProps(
|
|
405
481
|
{
|
|
406
482
|
children: props.children,
|
|
407
483
|
class: local.class,
|
|
408
484
|
style: local.style,
|
|
409
|
-
defaultClassName:
|
|
485
|
+
defaultClassName: "solidaria-Modal",
|
|
410
486
|
},
|
|
411
|
-
renderValues
|
|
412
|
-
)
|
|
487
|
+
renderValues,
|
|
488
|
+
);
|
|
413
489
|
|
|
414
|
-
|
|
415
|
-
|
|
490
|
+
const domProps = createMemo(() =>
|
|
491
|
+
filterDOMProps(rest as Record<string, unknown>, { global: true }),
|
|
492
|
+
);
|
|
416
493
|
|
|
417
494
|
return (
|
|
418
495
|
<FocusScope contain restoreFocus autoFocus>
|
|
@@ -424,10 +501,29 @@ function ModalContent(props: ModalProps & { internalContext: InternalModalContex
|
|
|
424
501
|
data-entering={dataAttr(local.isEntering)}
|
|
425
502
|
data-exiting={dataAttr(local.isExiting)}
|
|
426
503
|
>
|
|
504
|
+
<Show when={isDismissable()}>
|
|
505
|
+
<button
|
|
506
|
+
type="button"
|
|
507
|
+
aria-label="Dismiss"
|
|
508
|
+
tabIndex={-1}
|
|
509
|
+
onClick={close}
|
|
510
|
+
style={{
|
|
511
|
+
position: "absolute",
|
|
512
|
+
width: "1px",
|
|
513
|
+
height: "1px",
|
|
514
|
+
padding: 0,
|
|
515
|
+
margin: "-1px",
|
|
516
|
+
overflow: "hidden",
|
|
517
|
+
clip: "rect(0, 0, 0, 0)",
|
|
518
|
+
"white-space": "nowrap",
|
|
519
|
+
border: 0,
|
|
520
|
+
}}
|
|
521
|
+
/>
|
|
522
|
+
</Show>
|
|
427
523
|
{renderProps.renderChildren()}
|
|
428
524
|
</div>
|
|
429
525
|
</FocusScope>
|
|
430
|
-
)
|
|
526
|
+
);
|
|
431
527
|
}
|
|
432
528
|
|
|
433
|
-
export default Modal
|
|
529
|
+
export default Modal;
|