@proyecto-viviana/solidaria 0.2.2 → 0.2.4

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.
Files changed (210) hide show
  1. package/dist/autocomplete/createAutocomplete.d.ts +2 -2
  2. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  3. package/dist/index.js +233 -234
  4. package/dist/index.js.map +2 -2
  5. package/dist/index.ssr.js +233 -234
  6. package/dist/index.ssr.js.map +2 -2
  7. package/dist/interactions/PressEvent.d.ts +13 -10
  8. package/dist/interactions/PressEvent.d.ts.map +1 -1
  9. package/dist/interactions/createPress.d.ts.map +1 -1
  10. package/dist/interactions/index.d.ts +1 -1
  11. package/dist/interactions/index.d.ts.map +1 -1
  12. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  13. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  14. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  15. package/package.json +9 -7
  16. package/src/autocomplete/createAutocomplete.ts +341 -0
  17. package/src/autocomplete/index.ts +9 -0
  18. package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
  19. package/src/breadcrumbs/index.ts +8 -0
  20. package/src/button/createButton.ts +142 -0
  21. package/src/button/createToggleButton.ts +101 -0
  22. package/src/button/index.ts +4 -0
  23. package/src/button/types.ts +78 -0
  24. package/src/calendar/createCalendar.ts +138 -0
  25. package/src/calendar/createCalendarCell.ts +187 -0
  26. package/src/calendar/createCalendarGrid.ts +140 -0
  27. package/src/calendar/createRangeCalendar.ts +136 -0
  28. package/src/calendar/createRangeCalendarCell.ts +186 -0
  29. package/src/calendar/index.ts +34 -0
  30. package/src/checkbox/createCheckbox.ts +135 -0
  31. package/src/checkbox/createCheckboxGroup.ts +137 -0
  32. package/src/checkbox/createCheckboxGroupItem.ts +117 -0
  33. package/src/checkbox/createCheckboxGroupState.ts +193 -0
  34. package/src/checkbox/index.ts +13 -0
  35. package/src/color/createColorArea.ts +314 -0
  36. package/src/color/createColorField.ts +137 -0
  37. package/src/color/createColorSlider.ts +197 -0
  38. package/src/color/createColorSwatch.ts +40 -0
  39. package/src/color/createColorWheel.ts +208 -0
  40. package/src/color/index.ts +24 -0
  41. package/src/color/types.ts +116 -0
  42. package/src/combobox/createComboBox.ts +647 -0
  43. package/src/combobox/index.ts +6 -0
  44. package/src/combobox/intl/en-US.json +7 -0
  45. package/src/combobox/intl/es-ES.json +7 -0
  46. package/src/combobox/intl/index.ts +23 -0
  47. package/src/datepicker/createDateField.ts +154 -0
  48. package/src/datepicker/createDatePicker.ts +206 -0
  49. package/src/datepicker/createDateSegment.ts +229 -0
  50. package/src/datepicker/createTimeField.ts +154 -0
  51. package/src/datepicker/index.ts +28 -0
  52. package/src/dialog/createDialog.ts +120 -0
  53. package/src/dialog/index.ts +2 -0
  54. package/src/dialog/types.ts +19 -0
  55. package/src/disclosure/createDisclosure.ts +131 -0
  56. package/src/disclosure/createDisclosureGroup.ts +62 -0
  57. package/src/disclosure/index.ts +11 -0
  58. package/src/dnd/createDrag.ts +209 -0
  59. package/src/dnd/createDraggableCollection.ts +63 -0
  60. package/src/dnd/createDraggableItem.ts +243 -0
  61. package/src/dnd/createDrop.ts +321 -0
  62. package/src/dnd/createDroppableCollection.ts +293 -0
  63. package/src/dnd/createDroppableItem.ts +213 -0
  64. package/src/dnd/index.ts +47 -0
  65. package/src/dnd/types.ts +89 -0
  66. package/src/dnd/utils.ts +294 -0
  67. package/src/focus/FocusScope.tsx +408 -0
  68. package/src/focus/createAutoFocus.ts +321 -0
  69. package/src/focus/createFocusRestore.ts +313 -0
  70. package/src/focus/createVirtualFocus.ts +396 -0
  71. package/src/focus/index.ts +35 -0
  72. package/src/form/createFormReset.ts +51 -0
  73. package/src/form/createFormValidation.ts +224 -0
  74. package/src/form/index.ts +11 -0
  75. package/src/grid/GridKeyboardDelegate.ts +429 -0
  76. package/src/grid/createGrid.ts +261 -0
  77. package/src/grid/createGridCell.ts +182 -0
  78. package/src/grid/createGridRow.ts +153 -0
  79. package/src/grid/index.ts +18 -0
  80. package/src/grid/types.ts +133 -0
  81. package/src/gridlist/createGridList.ts +185 -0
  82. package/src/gridlist/createGridListItem.ts +180 -0
  83. package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
  84. package/src/gridlist/index.ts +16 -0
  85. package/src/gridlist/types.ts +81 -0
  86. package/src/i18n/NumberFormatter.ts +266 -0
  87. package/src/i18n/createCollator.ts +79 -0
  88. package/src/i18n/createDateFormatter.ts +83 -0
  89. package/src/i18n/createFilter.ts +131 -0
  90. package/src/i18n/createNumberFormatter.ts +52 -0
  91. package/src/i18n/createStringFormatter.ts +87 -0
  92. package/src/i18n/index.ts +40 -0
  93. package/src/i18n/locale.tsx +188 -0
  94. package/src/i18n/utils.ts +99 -0
  95. package/src/index.ts +670 -0
  96. package/src/interactions/FocusableProvider.tsx +44 -0
  97. package/src/interactions/PressEvent.ts +126 -0
  98. package/src/interactions/createFocus.ts +163 -0
  99. package/src/interactions/createFocusRing.ts +89 -0
  100. package/src/interactions/createFocusWithin.ts +206 -0
  101. package/src/interactions/createFocusable.ts +168 -0
  102. package/src/interactions/createHover.ts +254 -0
  103. package/src/interactions/createInteractionModality.ts +424 -0
  104. package/src/interactions/createKeyboard.ts +82 -0
  105. package/src/interactions/createLongPress.ts +174 -0
  106. package/src/interactions/createMove.ts +289 -0
  107. package/src/interactions/createPress.ts +834 -0
  108. package/src/interactions/index.ts +78 -0
  109. package/src/label/createField.ts +145 -0
  110. package/src/label/createLabel.ts +117 -0
  111. package/src/label/createLabels.ts +50 -0
  112. package/src/label/index.ts +19 -0
  113. package/src/landmark/createLandmark.ts +377 -0
  114. package/src/landmark/index.ts +8 -0
  115. package/src/link/createLink.ts +182 -0
  116. package/src/link/index.ts +1 -0
  117. package/src/listbox/createListBox.ts +269 -0
  118. package/src/listbox/createOption.ts +151 -0
  119. package/src/listbox/index.ts +12 -0
  120. package/src/live-announcer/announce.ts +322 -0
  121. package/src/live-announcer/index.ts +9 -0
  122. package/src/menu/createMenu.ts +396 -0
  123. package/src/menu/createMenuItem.ts +149 -0
  124. package/src/menu/createMenuTrigger.ts +88 -0
  125. package/src/menu/index.ts +18 -0
  126. package/src/meter/createMeter.ts +75 -0
  127. package/src/meter/index.ts +1 -0
  128. package/src/numberfield/createNumberField.ts +268 -0
  129. package/src/numberfield/index.ts +5 -0
  130. package/src/overlays/ariaHideOutside.ts +219 -0
  131. package/src/overlays/createInteractOutside.ts +149 -0
  132. package/src/overlays/createModal.tsx +202 -0
  133. package/src/overlays/createOverlay.ts +155 -0
  134. package/src/overlays/createOverlayTrigger.ts +85 -0
  135. package/src/overlays/createPreventScroll.ts +266 -0
  136. package/src/overlays/index.ts +44 -0
  137. package/src/popover/calculatePosition.ts +766 -0
  138. package/src/popover/createOverlayPosition.ts +356 -0
  139. package/src/popover/createPopover.ts +170 -0
  140. package/src/popover/index.ts +24 -0
  141. package/src/progress/createProgressBar.ts +128 -0
  142. package/src/progress/index.ts +5 -0
  143. package/src/radio/createRadio.ts +287 -0
  144. package/src/radio/createRadioGroup.ts +189 -0
  145. package/src/radio/createRadioGroupState.ts +201 -0
  146. package/src/radio/index.ts +23 -0
  147. package/src/searchfield/createSearchField.ts +186 -0
  148. package/src/searchfield/index.ts +2 -0
  149. package/src/select/createHiddenSelect.tsx +236 -0
  150. package/src/select/createSelect.ts +395 -0
  151. package/src/select/index.ts +14 -0
  152. package/src/selection/createTypeSelect.ts +201 -0
  153. package/src/selection/index.ts +6 -0
  154. package/src/separator/createSeparator.ts +82 -0
  155. package/src/separator/index.ts +6 -0
  156. package/src/slider/createSlider.ts +349 -0
  157. package/src/slider/index.ts +2 -0
  158. package/src/ssr/index.tsx +370 -0
  159. package/src/switch/createSwitch.ts +70 -0
  160. package/src/switch/index.ts +1 -0
  161. package/src/table/createTable.ts +526 -0
  162. package/src/table/createTableCell.ts +147 -0
  163. package/src/table/createTableColumnHeader.ts +115 -0
  164. package/src/table/createTableHeaderRow.ts +40 -0
  165. package/src/table/createTableRow.ts +155 -0
  166. package/src/table/createTableRowGroup.ts +32 -0
  167. package/src/table/createTableSelectAllCheckbox.ts +73 -0
  168. package/src/table/createTableSelectionCheckbox.ts +59 -0
  169. package/src/table/index.ts +30 -0
  170. package/src/table/types.ts +165 -0
  171. package/src/tabs/createTabs.ts +472 -0
  172. package/src/tabs/index.ts +14 -0
  173. package/src/tag/createTag.ts +194 -0
  174. package/src/tag/createTagGroup.ts +154 -0
  175. package/src/tag/index.ts +12 -0
  176. package/src/textfield/createTextField.ts +198 -0
  177. package/src/textfield/index.ts +5 -0
  178. package/src/toast/createToast.ts +118 -0
  179. package/src/toast/createToastRegion.ts +100 -0
  180. package/src/toast/index.ts +11 -0
  181. package/src/toggle/createToggle.ts +223 -0
  182. package/src/toggle/createToggleState.ts +94 -0
  183. package/src/toggle/index.ts +7 -0
  184. package/src/toolbar/createToolbar.ts +369 -0
  185. package/src/toolbar/index.ts +6 -0
  186. package/src/tooltip/createTooltip.ts +79 -0
  187. package/src/tooltip/createTooltipTrigger.ts +222 -0
  188. package/src/tooltip/index.ts +6 -0
  189. package/src/tree/createTree.ts +246 -0
  190. package/src/tree/createTreeItem.ts +233 -0
  191. package/src/tree/createTreeSelectionCheckbox.ts +68 -0
  192. package/src/tree/index.ts +16 -0
  193. package/src/tree/types.ts +87 -0
  194. package/src/utils/createDescription.ts +137 -0
  195. package/src/utils/dom.ts +327 -0
  196. package/src/utils/env.ts +54 -0
  197. package/src/utils/events.ts +106 -0
  198. package/src/utils/filterDOMProps.ts +116 -0
  199. package/src/utils/focus.ts +151 -0
  200. package/src/utils/geometry.ts +115 -0
  201. package/src/utils/globalListeners.ts +142 -0
  202. package/src/utils/index.ts +80 -0
  203. package/src/utils/mergeProps.ts +52 -0
  204. package/src/utils/platform.ts +52 -0
  205. package/src/utils/reactivity.ts +36 -0
  206. package/src/utils/textSelection.ts +114 -0
  207. package/src/visually-hidden/createVisuallyHidden.ts +124 -0
  208. package/src/visually-hidden/index.ts +6 -0
  209. package/dist/index.jsx +0 -15845
  210. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Detects interactions outside a given element.
3
+ * Based on @react-aria/interactions useInteractOutside.
4
+ */
5
+
6
+ import { createEffect, onCleanup } from 'solid-js';
7
+ import { getOwnerDocument } from '../utils';
8
+
9
+ export interface InteractOutsideProps {
10
+ /** Reference to the element to detect interactions outside of. */
11
+ ref: () => Element | null;
12
+ /** Handler called when an interaction outside the element completes. */
13
+ onInteractOutside?: (e: PointerEvent) => void;
14
+ /** Handler called when an interaction outside the element starts. */
15
+ onInteractOutsideStart?: (e: PointerEvent) => void;
16
+ /** Whether the interact outside events should be disabled. */
17
+ isDisabled?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Detects interactions outside a given element, used in components like
22
+ * Dialogs and Popovers so they can close when a user clicks outside them.
23
+ */
24
+ export function createInteractOutside(props: InteractOutsideProps): void {
25
+ let isPointerDown = false;
26
+ let ignoreEmulatedMouseEvents = false;
27
+
28
+ createEffect(() => {
29
+ const { ref, onInteractOutside, onInteractOutsideStart, isDisabled } = props;
30
+
31
+ if (isDisabled) {
32
+ return;
33
+ }
34
+
35
+ const element = ref();
36
+ const documentObject = getOwnerDocument(element);
37
+
38
+ const onPointerDown = (e: PointerEvent) => {
39
+ if (onInteractOutside && isValidEvent(e, ref)) {
40
+ if (onInteractOutsideStart) {
41
+ onInteractOutsideStart(e);
42
+ }
43
+ isPointerDown = true;
44
+ }
45
+ };
46
+
47
+ const triggerInteractOutside = (e: PointerEvent) => {
48
+ if (onInteractOutside) {
49
+ onInteractOutside(e);
50
+ }
51
+ };
52
+
53
+ // Use pointer events if available. Otherwise, fall back to mouse and touch events.
54
+ if (typeof PointerEvent !== 'undefined') {
55
+ const onClick = (e: PointerEvent) => {
56
+ if (isPointerDown && isValidEvent(e, ref)) {
57
+ triggerInteractOutside(e);
58
+ }
59
+ isPointerDown = false;
60
+ };
61
+
62
+ // Use click instead of pointerup to avoid Android Chrome issue
63
+ // https://issues.chromium.org/issues/40732224
64
+ documentObject.addEventListener('pointerdown', onPointerDown as EventListener, true);
65
+ documentObject.addEventListener('click', onClick as EventListener, true);
66
+
67
+ onCleanup(() => {
68
+ documentObject.removeEventListener('pointerdown', onPointerDown as EventListener, true);
69
+ documentObject.removeEventListener('click', onClick as EventListener, true);
70
+ });
71
+ } else {
72
+ // Fallback for environments without PointerEvent (mainly tests)
73
+ const onMouseUp = (e: MouseEvent) => {
74
+ if (ignoreEmulatedMouseEvents) {
75
+ ignoreEmulatedMouseEvents = false;
76
+ } else if (isPointerDown && isValidEvent(e as unknown as PointerEvent, ref)) {
77
+ triggerInteractOutside(e as unknown as PointerEvent);
78
+ }
79
+ isPointerDown = false;
80
+ };
81
+
82
+ const onTouchEnd = (e: TouchEvent) => {
83
+ ignoreEmulatedMouseEvents = true;
84
+ if (isPointerDown && isValidEvent(e as unknown as PointerEvent, ref)) {
85
+ triggerInteractOutside(e as unknown as PointerEvent);
86
+ }
87
+ isPointerDown = false;
88
+ };
89
+
90
+ const onMouseDown = (e: MouseEvent) => {
91
+ if (onInteractOutside && isValidEvent(e as unknown as PointerEvent, ref)) {
92
+ if (onInteractOutsideStart) {
93
+ onInteractOutsideStart(e as unknown as PointerEvent);
94
+ }
95
+ isPointerDown = true;
96
+ }
97
+ };
98
+
99
+ const onTouchStart = (e: TouchEvent) => {
100
+ if (onInteractOutside && isValidEvent(e as unknown as PointerEvent, ref)) {
101
+ if (onInteractOutsideStart) {
102
+ onInteractOutsideStart(e as unknown as PointerEvent);
103
+ }
104
+ isPointerDown = true;
105
+ }
106
+ };
107
+
108
+ documentObject.addEventListener('mousedown', onMouseDown as EventListener, true);
109
+ documentObject.addEventListener('mouseup', onMouseUp as EventListener, true);
110
+ documentObject.addEventListener('touchstart', onTouchStart as EventListener, true);
111
+ documentObject.addEventListener('touchend', onTouchEnd as EventListener, true);
112
+
113
+ onCleanup(() => {
114
+ documentObject.removeEventListener('mousedown', onMouseDown as EventListener, true);
115
+ documentObject.removeEventListener('mouseup', onMouseUp as EventListener, true);
116
+ documentObject.removeEventListener('touchstart', onTouchStart as EventListener, true);
117
+ documentObject.removeEventListener('touchend', onTouchEnd as EventListener, true);
118
+ });
119
+ }
120
+ });
121
+ }
122
+
123
+ function isValidEvent(event: PointerEvent | MouseEvent | TouchEvent, ref: () => Element | null): boolean {
124
+ // Only handle primary button clicks
125
+ if ('button' in event && event.button > 0) {
126
+ return false;
127
+ }
128
+
129
+ if (event.target) {
130
+ // If the event target is no longer in the document, ignore
131
+ const ownerDocument = (event.target as Element).ownerDocument;
132
+ if (!ownerDocument || !ownerDocument.documentElement.contains(event.target as Node)) {
133
+ return false;
134
+ }
135
+ // If the target is within a top layer element (e.g. toasts), ignore
136
+ if ((event.target as Element).closest?.('[data-solidaria-top-layer]')) {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ const element = ref();
142
+ if (!element) {
143
+ return false;
144
+ }
145
+
146
+ // When the event source is inside a Shadow DOM, event.target is just the shadow root.
147
+ // Using event.composedPath instead means we can get the actual element inside the shadow root.
148
+ return !event.composedPath().includes(element);
149
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Modal context and hooks for managing modal accessibility.
3
+ * Based on @react-aria/overlays useModal.
4
+ */
5
+
6
+ import {
7
+ createContext,
8
+ createSignal,
9
+ useContext,
10
+ createEffect,
11
+ onCleanup,
12
+ type JSX,
13
+ type Accessor,
14
+ type ParentComponent,
15
+ } from 'solid-js';
16
+ import { Portal } from 'solid-js/web';
17
+ import { isServer } from 'solid-js/web';
18
+
19
+ export interface ModalProviderProps {
20
+ children: JSX.Element;
21
+ }
22
+
23
+ interface ModalContext {
24
+ parent: ModalContext | null;
25
+ modalCount: Accessor<number>;
26
+ addModal: () => void;
27
+ removeModal: () => void;
28
+ }
29
+
30
+ const ModalContext = createContext<ModalContext | null>(null);
31
+
32
+ /**
33
+ * Each ModalProvider tracks how many modals are open in its subtree. On mount, the modals
34
+ * trigger `addModal` to increment the count, and trigger `removeModal` on unmount to decrement it.
35
+ * This is done recursively so that all parent providers are incremented and decremented.
36
+ * If the modal count is greater than zero, we add `aria-hidden` to this provider to hide its
37
+ * subtree from screen readers. This is done using SolidJS context in order to account for things
38
+ * like portals, which can cause the component tree and the DOM tree to differ significantly in structure.
39
+ */
40
+ export const ModalProvider: ParentComponent<ModalProviderProps> = (props) => {
41
+ const parent = useContext(ModalContext);
42
+ const [modalCount, setModalCount] = createSignal(0);
43
+
44
+ const context: ModalContext = {
45
+ parent,
46
+ modalCount,
47
+ addModal() {
48
+ setModalCount((count) => count + 1);
49
+ if (parent) {
50
+ parent.addModal();
51
+ }
52
+ },
53
+ removeModal() {
54
+ setModalCount((count) => count - 1);
55
+ if (parent) {
56
+ parent.removeModal();
57
+ }
58
+ },
59
+ };
60
+
61
+ return (
62
+ <ModalContext.Provider value={context}>
63
+ {props.children}
64
+ </ModalContext.Provider>
65
+ );
66
+ };
67
+
68
+ export interface ModalProviderAria {
69
+ /** Props to be spread on the container element. */
70
+ modalProviderProps: {
71
+ 'aria-hidden'?: true;
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Used to determine if the tree should be aria-hidden based on how many
77
+ * modals are open.
78
+ */
79
+ export function useModalProvider(): ModalProviderAria {
80
+ const context = useContext(ModalContext);
81
+ return {
82
+ modalProviderProps: {
83
+ get 'aria-hidden'() {
84
+ return context && context.modalCount() > 0 ? true : undefined;
85
+ },
86
+ },
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Creates a root node that will be aria-hidden if there are other modals open.
92
+ */
93
+ const OverlayContainerDOM: ParentComponent<ModalProviderProps> = (props) => {
94
+ const { modalProviderProps } = useModalProvider();
95
+ return (
96
+ <div data-overlay-container {...modalProviderProps}>
97
+ {props.children}
98
+ </div>
99
+ );
100
+ };
101
+
102
+ /**
103
+ * An OverlayProvider acts as a container for the top-level application.
104
+ * Any application that uses modal dialogs or other overlays should
105
+ * be wrapped in a `<OverlayProvider>`. This is used to ensure that
106
+ * the main content of the application is hidden from screen readers
107
+ * if a modal or other overlay is opened. Only the top-most modal or
108
+ * overlay should be accessible at once.
109
+ */
110
+ export const OverlayProvider: ParentComponent<ModalProviderProps> = (props) => {
111
+ return (
112
+ <ModalProvider>
113
+ <OverlayContainerDOM>{props.children}</OverlayContainerDOM>
114
+ </ModalProvider>
115
+ );
116
+ };
117
+
118
+ export interface OverlayContainerProps extends ModalProviderProps {
119
+ /**
120
+ * The container element in which the overlay portal will be placed.
121
+ * @default document.body
122
+ */
123
+ portalContainer?: Element;
124
+ }
125
+
126
+ /**
127
+ * A container for overlays like modals and popovers. Renders the overlay
128
+ * into a Portal which is placed at the end of the document body.
129
+ * Also ensures that the overlay is hidden from screen readers if a
130
+ * nested modal is opened. Only the top-most modal or overlay should
131
+ * be accessible at once.
132
+ */
133
+ export const OverlayContainer: ParentComponent<OverlayContainerProps> = (props) => {
134
+ // Don't render portal on server
135
+ if (isServer) {
136
+ return null;
137
+ }
138
+
139
+ const portalContainer = () => props.portalContainer ?? document.body;
140
+
141
+ createEffect(() => {
142
+ const container = portalContainer();
143
+ if (container?.closest('[data-overlay-container]')) {
144
+ throw new Error('An OverlayContainer must not be inside another container. Please change the portalContainer prop.');
145
+ }
146
+ });
147
+
148
+ return (
149
+ <Portal mount={portalContainer()}>
150
+ <OverlayProvider>{props.children}</OverlayProvider>
151
+ </Portal>
152
+ );
153
+ };
154
+
155
+ export interface AriaModalOptions {
156
+ /** Whether the modal is disabled. */
157
+ isDisabled?: boolean;
158
+ }
159
+
160
+ export interface ModalAria {
161
+ /** Props for the modal content element. */
162
+ modalProps: {
163
+ 'data-ismodal': boolean;
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Hides content outside the current `<OverlayContainer>` from screen readers
169
+ * on mount and restores it on unmount. Typically used by modal dialogs and
170
+ * other types of overlays to ensure that only the top-most modal is
171
+ * accessible at once.
172
+ */
173
+ export function createModal(options?: AriaModalOptions): ModalAria {
174
+ // Add aria-hidden to all parent providers on mount, and restore on unmount.
175
+ const context = useContext(ModalContext);
176
+
177
+ if (!context) {
178
+ throw new Error('Modal is not contained within a provider');
179
+ }
180
+
181
+ createEffect(() => {
182
+ if (options?.isDisabled || !context.parent) {
183
+ return;
184
+ }
185
+
186
+ // The immediate context is from the provider containing this modal, so we only
187
+ // want to trigger aria-hidden on its parents not on the modal provider itself.
188
+ context.parent.addModal();
189
+
190
+ onCleanup(() => {
191
+ if (context.parent) {
192
+ context.parent.removeModal();
193
+ }
194
+ });
195
+ });
196
+
197
+ return {
198
+ modalProps: {
199
+ 'data-ismodal': !options?.isDisabled,
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Provides the behavior for overlays such as dialogs, popovers, and menus.
3
+ * Based on @react-aria/overlays useOverlay.
4
+ */
5
+
6
+ import { createEffect, onCleanup, type JSX } from 'solid-js';
7
+ import { createInteractOutside } from './createInteractOutside';
8
+ import { createFocusWithin } from '../interactions/createFocusWithin';
9
+
10
+ export interface AriaOverlayProps {
11
+ /** Whether the overlay is currently open. */
12
+ isOpen?: boolean;
13
+ /** Handler that is called when the overlay should close. */
14
+ onClose?: () => void;
15
+ /**
16
+ * Whether to close the overlay when the user interacts outside it.
17
+ * @default false
18
+ */
19
+ isDismissable?: boolean;
20
+ /** Whether the overlay should close when focus is lost or moves outside it. */
21
+ shouldCloseOnBlur?: boolean;
22
+ /**
23
+ * Whether pressing the escape key to close the overlay should be disabled.
24
+ * @default false
25
+ */
26
+ isKeyboardDismissDisabled?: boolean;
27
+ /**
28
+ * When user interacts with the argument element outside of the overlay ref,
29
+ * return true if onClose should be called. This gives you a chance to filter
30
+ * out interaction with elements that should not dismiss the overlay.
31
+ * By default, onClose will always be called on interaction outside the overlay ref.
32
+ */
33
+ shouldCloseOnInteractOutside?: (element: Element) => boolean;
34
+ }
35
+
36
+ export interface OverlayAria {
37
+ /** Props to apply to the overlay container element. */
38
+ overlayProps: JSX.HTMLAttributes<HTMLElement>;
39
+ /** Props to apply to the underlay element, if any. */
40
+ underlayProps: JSX.HTMLAttributes<HTMLElement>;
41
+ }
42
+
43
+ // Stack of visible overlays, used to ensure only topmost overlay closes
44
+ const visibleOverlays: Array<() => Element | null> = [];
45
+
46
+ /**
47
+ * Provides the behavior for overlays such as dialogs, popovers, and menus.
48
+ * Hides the overlay when the user interacts outside it, when the Escape key is pressed,
49
+ * or optionally, on blur. Only the top-most overlay will close at once.
50
+ */
51
+ export function createOverlay(
52
+ props: AriaOverlayProps,
53
+ ref: () => Element | null
54
+ ): OverlayAria {
55
+ const {
56
+ onClose,
57
+ shouldCloseOnBlur,
58
+ isOpen,
59
+ isDismissable = false,
60
+ isKeyboardDismissDisabled = false,
61
+ shouldCloseOnInteractOutside,
62
+ } = props;
63
+
64
+ // Add the overlay ref to the stack of visible overlays on mount, and remove on unmount.
65
+ createEffect(() => {
66
+ if (isOpen && !visibleOverlays.includes(ref)) {
67
+ visibleOverlays.push(ref);
68
+ }
69
+
70
+ onCleanup(() => {
71
+ const index = visibleOverlays.indexOf(ref);
72
+ if (index >= 0) {
73
+ visibleOverlays.splice(index, 1);
74
+ }
75
+ });
76
+ });
77
+
78
+ // Only hide the overlay when it is the topmost visible overlay in the stack
79
+ const onHide = () => {
80
+ if (visibleOverlays[visibleOverlays.length - 1] === ref && onClose) {
81
+ onClose();
82
+ }
83
+ };
84
+
85
+ const onInteractOutsideStart = (e: PointerEvent) => {
86
+ if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) {
87
+ if (visibleOverlays[visibleOverlays.length - 1] === ref) {
88
+ e.stopPropagation();
89
+ e.preventDefault();
90
+ }
91
+ }
92
+ };
93
+
94
+ const onInteractOutside = (e: PointerEvent) => {
95
+ if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.target as Element)) {
96
+ if (visibleOverlays[visibleOverlays.length - 1] === ref) {
97
+ e.stopPropagation();
98
+ e.preventDefault();
99
+ }
100
+ onHide();
101
+ }
102
+ };
103
+
104
+ // Handle clicking outside the overlay to close it
105
+ createInteractOutside({
106
+ ref,
107
+ onInteractOutside: isDismissable && isOpen ? onInteractOutside : undefined,
108
+ onInteractOutsideStart,
109
+ isDisabled: !isDismissable || !isOpen,
110
+ });
111
+
112
+ // Handle focus within for blur detection
113
+ const { focusWithinProps } = createFocusWithin({
114
+ isDisabled: !shouldCloseOnBlur,
115
+ onBlurWithin: (e) => {
116
+ // Do not close if relatedTarget is null, which means focus is lost to the body.
117
+ // That can happen when switching tabs, or due to a browser bug.
118
+ // Clicking on the body to close the overlay should already be handled by createInteractOutside.
119
+ if (!e.relatedTarget) {
120
+ return;
121
+ }
122
+
123
+ if (!shouldCloseOnInteractOutside || shouldCloseOnInteractOutside(e.relatedTarget as Element)) {
124
+ onClose?.();
125
+ }
126
+ },
127
+ });
128
+
129
+ // Handle the escape key
130
+ const onKeyDown: JSX.EventHandler<HTMLElement, KeyboardEvent> = (e) => {
131
+ if (e.key === 'Escape' && !isKeyboardDismissDisabled && !(e as any).nativeEvent?.isComposing) {
132
+ e.stopPropagation();
133
+ e.preventDefault();
134
+ onHide();
135
+ }
136
+ };
137
+
138
+ const onPointerDownUnderlay: JSX.EventHandler<HTMLElement, PointerEvent> = (e) => {
139
+ // Fixes a Firefox issue that starts text selection
140
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1675846
141
+ if (e.target === e.currentTarget) {
142
+ e.preventDefault();
143
+ }
144
+ };
145
+
146
+ return {
147
+ overlayProps: {
148
+ onKeyDown,
149
+ ...focusWithinProps,
150
+ },
151
+ underlayProps: {
152
+ onPointerDown: onPointerDownUnderlay,
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Handles the behavior and accessibility for an overlay trigger.
3
+ * Based on @react-aria/overlays useOverlayTrigger.
4
+ */
5
+
6
+ import { createEffect } from 'solid-js';
7
+ import type { OverlayTriggerState } from '@proyecto-viviana/solid-stately';
8
+ import { createId } from '../ssr';
9
+ import { access, type MaybeAccessor } from '../utils';
10
+
11
+ export interface OverlayTriggerProps {
12
+ /** Type of overlay that is opened by the trigger. */
13
+ type: 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid';
14
+ }
15
+
16
+ export interface OverlayTriggerAria {
17
+ /** Props for the trigger element. */
18
+ triggerProps: {
19
+ 'aria-haspopup'?: boolean | 'listbox';
20
+ 'aria-expanded': boolean;
21
+ 'aria-controls'?: string;
22
+ onPress: () => void;
23
+ };
24
+ /** Props for the overlay container element. */
25
+ overlayProps: {
26
+ id: string;
27
+ };
28
+ }
29
+
30
+ // Map for storing close functions, used by useCloseOnScroll
31
+ export const onCloseMap = new WeakMap<Element, () => void>();
32
+
33
+ /**
34
+ * Handles the behavior and accessibility for an overlay trigger, e.g. a button
35
+ * that opens a popover, menu, or other overlay that is positioned relative to the trigger.
36
+ */
37
+ export function createOverlayTrigger(
38
+ props: MaybeAccessor<OverlayTriggerProps>,
39
+ state: OverlayTriggerState,
40
+ ref?: () => Element | null
41
+ ): OverlayTriggerAria {
42
+ const propsAccessor = () => access(props);
43
+ const overlayId = createId();
44
+
45
+ // Backward compatibility. Share state close function with useOverlayPosition so it can close on scroll
46
+ // without forcing users to pass onClose.
47
+ createEffect(() => {
48
+ const element = ref?.();
49
+ if (element) {
50
+ onCloseMap.set(element, state.close);
51
+ }
52
+ });
53
+
54
+ // Aria 1.1 supports multiple values for aria-haspopup other than just menus.
55
+ // https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup
56
+ // However, we only add it for menus for now because screen readers often
57
+ // announce it as a menu even for other values.
58
+ const getAriaHasPopup = (): boolean | 'listbox' | undefined => {
59
+ const type = propsAccessor().type;
60
+ if (type === 'menu') {
61
+ return true;
62
+ } else if (type === 'listbox') {
63
+ return 'listbox';
64
+ }
65
+ return undefined;
66
+ };
67
+
68
+ return {
69
+ triggerProps: {
70
+ get 'aria-haspopup'() {
71
+ return getAriaHasPopup();
72
+ },
73
+ get 'aria-expanded'() {
74
+ return state.isOpen();
75
+ },
76
+ get 'aria-controls'() {
77
+ return state.isOpen() ? overlayId : undefined;
78
+ },
79
+ onPress: state.toggle,
80
+ },
81
+ overlayProps: {
82
+ id: overlayId,
83
+ },
84
+ };
85
+ }