@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,137 @@
1
+ /**
2
+ * createDescription - Creates a hidden element for dynamic aria-describedby content.
3
+ *
4
+ * This utility creates a visually hidden element containing description text and
5
+ * returns an aria-describedby prop pointing to it. Multiple components using the
6
+ * same description will share the same element (reference counted).
7
+ *
8
+ * Port of @react-aria/utils/useDescription.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * function SortableColumn(props) {
13
+ * const descriptionProps = createDescription(
14
+ * () => props.sortDirection ? `Sorted ${props.sortDirection}` : undefined
15
+ * );
16
+ *
17
+ * return (
18
+ * <th {...descriptionProps}>
19
+ * {props.children}
20
+ * </th>
21
+ * );
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ import { createSignal, createEffect, onCleanup, type Accessor } from 'solid-js';
27
+ import { isServer } from 'solid-js/web';
28
+
29
+ // ============================================
30
+ // TYPES
31
+ // ============================================
32
+
33
+ export interface DescriptionProps {
34
+ 'aria-describedby'?: string;
35
+ }
36
+
37
+ // ============================================
38
+ // STATE
39
+ // ============================================
40
+
41
+ let descriptionId = 0;
42
+ const descriptionNodes = new Map<string, { refCount: number; element: Element }>();
43
+
44
+ // ============================================
45
+ // IMPLEMENTATION
46
+ // ============================================
47
+
48
+ /**
49
+ * Creates an invisible description element and returns aria-describedby props.
50
+ *
51
+ * The element is created in the DOM and reference counted - multiple uses of
52
+ * the same description text will share the same element. When all references
53
+ * are removed, the element is cleaned up.
54
+ *
55
+ * @param description - Accessor that returns the description text, or undefined
56
+ * @returns Object with aria-describedby prop (or empty object if no description)
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * const descProps = createDescription(() => 'Press Enter to submit');
61
+ * return <button {...descProps}>Submit</button>;
62
+ * ```
63
+ */
64
+ export function createDescription(description: Accessor<string | undefined>): DescriptionProps {
65
+ // SSR: return empty object
66
+ if (isServer) {
67
+ return {};
68
+ }
69
+
70
+ const [id, setId] = createSignal<string | undefined>();
71
+
72
+ createEffect(() => {
73
+ const desc = description();
74
+
75
+ if (!desc) {
76
+ setId(undefined);
77
+ return;
78
+ }
79
+
80
+ let node = descriptionNodes.get(desc);
81
+
82
+ if (!node) {
83
+ // Create new description element
84
+ const newId = `solidaria-description-${descriptionId++}`;
85
+ setId(newId);
86
+
87
+ const element = document.createElement('div');
88
+ element.id = newId;
89
+ element.style.display = 'none';
90
+ element.textContent = desc;
91
+ document.body.appendChild(element);
92
+
93
+ node = { refCount: 0, element };
94
+ descriptionNodes.set(desc, node);
95
+ } else {
96
+ // Reuse existing element
97
+ setId(node.element.id);
98
+ }
99
+
100
+ node.refCount++;
101
+
102
+ // Cleanup when description changes or component unmounts
103
+ onCleanup(() => {
104
+ if (node && --node.refCount === 0) {
105
+ node.element.remove();
106
+ descriptionNodes.delete(desc);
107
+ }
108
+ });
109
+ });
110
+
111
+ // Return reactive props object
112
+ return {
113
+ get 'aria-describedby'() {
114
+ const desc = description();
115
+ return desc ? id() : undefined;
116
+ },
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Utility to get all active description nodes (for testing).
122
+ * @internal
123
+ */
124
+ export function getDescriptionNodeCount(): number {
125
+ return descriptionNodes.size;
126
+ }
127
+
128
+ /**
129
+ * Utility to clear all description nodes (for testing cleanup).
130
+ * @internal
131
+ */
132
+ export function clearDescriptionNodes(): void {
133
+ for (const [, node] of descriptionNodes) {
134
+ node.element.remove();
135
+ }
136
+ descriptionNodes.clear();
137
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * DOM utilities for cross-browser compatibility.
3
+ * Based on @react-aria/utils DOM utilities.
4
+ */
5
+
6
+ /**
7
+ * Gets the owner document of an element, or the global document.
8
+ */
9
+ export function getOwnerDocument(el: Element | null | undefined): Document {
10
+ return el?.ownerDocument ?? document;
11
+ }
12
+
13
+ /**
14
+ * Gets the owner window of an element, or the global window.
15
+ */
16
+ export function getOwnerWindow(el: Element | null | undefined): Window & typeof globalThis {
17
+ return getOwnerDocument(el).defaultView ?? window;
18
+ }
19
+
20
+ /**
21
+ * Cross-browser implementation of Node.contains that works with ShadowDOM.
22
+ * In Safari, Node.contains doesn't properly detect elements inside shadow roots.
23
+ */
24
+ export function nodeContains(parent: Node | null, child: Node | null): boolean {
25
+ if (!parent || !child) {
26
+ return false;
27
+ }
28
+
29
+ // Standard contains check
30
+ if (parent.contains(child)) {
31
+ return true;
32
+ }
33
+
34
+ // Check if child is in a shadow root
35
+ let node: Node | null = child;
36
+ while (node) {
37
+ if (node === parent) {
38
+ return true;
39
+ }
40
+
41
+ // Check shadow root host
42
+ if ((node as ShadowRoot).host) {
43
+ node = (node as ShadowRoot).host;
44
+ } else {
45
+ node = node.parentNode;
46
+ }
47
+ }
48
+
49
+ return false;
50
+ }
51
+
52
+ /**
53
+ * Gets the event target, handling composed path for shadow DOM.
54
+ */
55
+ export function getEventTarget<T extends EventTarget>(event: Event): T | null {
56
+ // Use composedPath to get the real target when using Shadow DOM
57
+ if (typeof event.composedPath === 'function') {
58
+ const path = event.composedPath();
59
+ if (path.length > 0) {
60
+ return path[0] as T;
61
+ }
62
+ }
63
+ return event.target as T | null;
64
+ }
65
+
66
+ /**
67
+ * Checks if an element is a valid focusable element.
68
+ */
69
+ export function isFocusable(element: Element): boolean {
70
+ // Check if element is disabled
71
+ if ((element as HTMLInputElement).disabled) {
72
+ return false;
73
+ }
74
+
75
+ // Check native focusable elements
76
+ const tagName = element.tagName.toLowerCase();
77
+ if (['input', 'select', 'textarea', 'button', 'a', 'area'].includes(tagName)) {
78
+ // For anchor elements, they must have href to be focusable
79
+ if (tagName === 'a' || tagName === 'area') {
80
+ return element.hasAttribute('href');
81
+ }
82
+ return true;
83
+ }
84
+
85
+ // Check for tabIndex
86
+ const tabIndex = element.getAttribute('tabindex');
87
+ if (tabIndex != null && tabIndex !== '-1') {
88
+ return true;
89
+ }
90
+
91
+ // Check for contenteditable
92
+ if (element.hasAttribute('contenteditable') && element.getAttribute('contenteditable') !== 'false') {
93
+ return true;
94
+ }
95
+
96
+ return false;
97
+ }
98
+
99
+ /**
100
+ * Checks if a keyboard event should trigger the default action (like clicking).
101
+ */
102
+ export function isValidKeyboardEvent(event: KeyboardEvent, currentTarget: Element): boolean {
103
+ const { key, code } = event;
104
+ const element = currentTarget as HTMLElement;
105
+ const tagName = element.tagName.toLowerCase();
106
+ const role = element.getAttribute('role');
107
+
108
+ // Only accept Enter and Space
109
+ const isActivationKey = key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space';
110
+ if (!isActivationKey) {
111
+ return false;
112
+ }
113
+
114
+ // Text inputs should handle their own keyboard events
115
+ if (tagName === 'textarea') {
116
+ return false;
117
+ }
118
+
119
+ // Content editable elements should handle their own keyboard events
120
+ if (element.isContentEditable) {
121
+ return false;
122
+ }
123
+
124
+ // Links should only respond to Enter, not Space
125
+ const isLink = role === 'link' || (!role && isHTMLAnchorLink(element));
126
+ if (isLink && key !== 'Enter') {
127
+ return false;
128
+ }
129
+
130
+ // Input elements have specific key handling
131
+ if (tagName === 'input') {
132
+ return isValidInputKey(element as HTMLInputElement, key);
133
+ }
134
+
135
+ return true;
136
+ }
137
+
138
+ /**
139
+ * Checks if a key is valid for a specific input type.
140
+ */
141
+ export function isValidInputKey(target: HTMLInputElement, key: string): boolean {
142
+ const type = target.type.toLowerCase();
143
+
144
+ // Checkbox and radio only respond to Space
145
+ if (type === 'checkbox' || type === 'radio') {
146
+ return key === ' ' || key === 'Spacebar';
147
+ }
148
+
149
+ // Text-like inputs handle their own keyboard events
150
+ const textInputTypes = [
151
+ 'text', 'search', 'url', 'tel', 'email', 'password',
152
+ 'date', 'month', 'week', 'time', 'datetime-local', 'number'
153
+ ];
154
+ if (textInputTypes.includes(type)) {
155
+ return false;
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * Checks if an element is an HTML anchor link (has href attribute).
163
+ */
164
+ export function isHTMLAnchorLink(target: Element): boolean {
165
+ return target.tagName === 'A' && target.hasAttribute('href');
166
+ }
167
+
168
+ /**
169
+ * Whether to prevent default on keyboard events for this element.
170
+ */
171
+ export function shouldPreventDefaultKeyboard(target: Element, key: string): boolean {
172
+ const tagName = target.tagName.toLowerCase();
173
+
174
+ // Never prevent default on inputs - they handle their own behavior
175
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
176
+ return false;
177
+ }
178
+
179
+ // Don't prevent default on links for Enter (native navigation)
180
+ if ((tagName === 'a' || target.getAttribute('role') === 'link') && key === 'Enter') {
181
+ return false;
182
+ }
183
+
184
+ // Buttons with submit/reset type should not prevent default
185
+ if (tagName === 'button') {
186
+ const type = (target as HTMLButtonElement).type;
187
+ if (type === 'submit' || type === 'reset') {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ return true;
193
+ }
194
+
195
+ /**
196
+ * Whether to prevent default on pointer up for this element.
197
+ */
198
+ export function shouldPreventDefaultUp(target: Element): boolean {
199
+ const tagName = target.tagName.toLowerCase();
200
+
201
+ // Never prevent default on form elements
202
+ if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
203
+ return false;
204
+ }
205
+
206
+ // Don't prevent default on links
207
+ if (tagName === 'a' || target.getAttribute('role') === 'link') {
208
+ return false;
209
+ }
210
+
211
+ // Buttons with submit/reset type should not prevent default
212
+ if (tagName === 'button') {
213
+ const type = (target as HTMLButtonElement).type;
214
+ if (type === 'submit' || type === 'reset') {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ return true;
220
+ }
221
+
222
+ /**
223
+ * Opens a link, supporting both same-window and new-window navigation.
224
+ * Used for keyboard activation of links with Space key (which doesn't natively open links).
225
+ */
226
+ export function openLink(target: HTMLAnchorElement, event: Event, allowOpener = false): void {
227
+ const { href, target: linkTarget, rel } = target;
228
+ (openLink as { isOpening?: boolean }).isOpening = true;
229
+
230
+ // Handle modifier keys for open-in-new-tab behavior
231
+ const keyEvent = event as KeyboardEvent;
232
+ const shouldOpenInNewTab =
233
+ linkTarget === '_blank' ||
234
+ keyEvent?.metaKey ||
235
+ keyEvent?.ctrlKey ||
236
+ keyEvent?.shiftKey ||
237
+ keyEvent?.altKey;
238
+
239
+ if (shouldOpenInNewTab) {
240
+ const features = !allowOpener && rel?.includes('noopener') ? 'noopener' : undefined;
241
+ window.open(href, linkTarget || '_blank', features);
242
+ } else {
243
+ window.location.href = href;
244
+ }
245
+
246
+ (openLink as { isOpening?: boolean }).isOpening = false;
247
+ }
248
+
249
+ (openLink as { isOpening?: boolean }).isOpening = false;
250
+
251
+ // ============================================
252
+ // Scroll utilities
253
+ // ============================================
254
+
255
+ /**
256
+ * Checks if an element is scrollable based on its overflow style.
257
+ * @param node - The element to check
258
+ * @param checkForOverflow - If true, also check if the element actually overflows
259
+ */
260
+ export function isScrollable(node: Element | null, checkForOverflow?: boolean): boolean {
261
+ if (!node) {
262
+ return false;
263
+ }
264
+
265
+ const style = window.getComputedStyle(node);
266
+ const scrollable = /(auto|scroll)/.test(style.overflow + style.overflowX + style.overflowY);
267
+
268
+ if (scrollable && checkForOverflow) {
269
+ return node.scrollHeight !== node.clientHeight || node.scrollWidth !== node.clientWidth;
270
+ }
271
+
272
+ return scrollable;
273
+ }
274
+
275
+ /**
276
+ * Gets the nearest scrollable parent element.
277
+ * @param node - The starting element
278
+ * @param checkForOverflow - If true, only return parents that actually overflow
279
+ */
280
+ export function getScrollParent(node: Element, checkForOverflow?: boolean): Element {
281
+ let scrollableNode: Element | null = node;
282
+
283
+ if (isScrollable(scrollableNode, checkForOverflow)) {
284
+ scrollableNode = scrollableNode.parentElement;
285
+ }
286
+
287
+ while (scrollableNode && !isScrollable(scrollableNode, checkForOverflow)) {
288
+ scrollableNode = scrollableNode.parentElement;
289
+ }
290
+
291
+ return scrollableNode || document.scrollingElement || document.documentElement;
292
+ }
293
+
294
+ /**
295
+ * Checks if an element will open a virtual keyboard when focused.
296
+ * Used for iOS Safari scroll handling.
297
+ */
298
+ export function willOpenKeyboard(target: Element | null): boolean {
299
+ if (!target) {
300
+ return false;
301
+ }
302
+
303
+ const tagName = target.tagName.toLowerCase();
304
+
305
+ // Inputs that open keyboard (not all input types do)
306
+ if (tagName === 'input') {
307
+ const type = (target as HTMLInputElement).type.toLowerCase();
308
+ // These input types open the keyboard
309
+ const keyboardTypes = [
310
+ 'text', 'search', 'url', 'tel', 'email', 'password',
311
+ 'date', 'month', 'week', 'time', 'datetime-local', 'number'
312
+ ];
313
+ return keyboardTypes.includes(type);
314
+ }
315
+
316
+ // Textareas always open keyboard
317
+ if (tagName === 'textarea') {
318
+ return true;
319
+ }
320
+
321
+ // Contenteditable elements open keyboard
322
+ if (target.hasAttribute('contenteditable') && target.getAttribute('contenteditable') !== 'false') {
323
+ return true;
324
+ }
325
+
326
+ return false;
327
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Environment detection utilities.
3
+ * These avoid direct references to process.env which can cause TypeScript issues in browser environments.
4
+ * Compatible with Node.js, Deno, and Vite environments.
5
+ */
6
+
7
+ // Type-safe access to import.meta.env (Vite) and Deno.env
8
+ declare const Deno: { env?: { get(key: string): string | undefined } } | undefined;
9
+
10
+ function getEnvVar(key: string): string | undefined {
11
+ // Check Vite's import.meta.env
12
+ if (typeof import.meta !== 'undefined' && (import.meta as any).env) {
13
+ return (import.meta as any).env[key];
14
+ }
15
+ // Check Deno
16
+ if (typeof Deno !== 'undefined' && Deno.env) {
17
+ return Deno.env.get(key);
18
+ }
19
+ // Check Node.js process.env via globalThis
20
+ if (typeof globalThis !== 'undefined' && (globalThis as any).process?.env) {
21
+ return (globalThis as any).process.env[key];
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ /**
27
+ * Check if we're running in a test environment.
28
+ */
29
+ export function isTestEnv(): boolean {
30
+ return getEnvVar('NODE_ENV') === 'test';
31
+ }
32
+
33
+ /**
34
+ * Check if we're running in a development environment (not production).
35
+ */
36
+ export function isDevEnv(): boolean {
37
+ // Check Vite's DEV flag
38
+ if (typeof import.meta !== 'undefined' && (import.meta as any).env?.DEV) {
39
+ return true;
40
+ }
41
+ const nodeEnv = getEnvVar('NODE_ENV');
42
+ return nodeEnv !== 'production';
43
+ }
44
+
45
+ /**
46
+ * Check if we're running in production.
47
+ */
48
+ export function isProdEnv(): boolean {
49
+ // Check Vite's PROD flag
50
+ if (typeof import.meta !== 'undefined' && (import.meta as any).env?.PROD) {
51
+ return true;
52
+ }
53
+ return getEnvVar('NODE_ENV') === 'production';
54
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Event utilities for detecting virtual clicks and event handling.
3
+ * Based on @react-aria/utils event utilities.
4
+ */
5
+
6
+ import { isAndroid } from './platform';
7
+
8
+ /**
9
+ * Checks if a click event was generated from a virtual source like a screen reader.
10
+ * Virtual clicks typically have detail of 0 and may have zero coordinates.
11
+ */
12
+ export function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
13
+ // JAWS/NVDA with Firefox.
14
+ if ((event as PointerEvent).pointerType === '' && event.isTrusted) {
15
+ return true;
16
+ }
17
+
18
+ // Android TalkBack's detail value varies depending on the event listener providing the event.
19
+ // If pointerType is defined, event is from a click listener.
20
+ if (isAndroid() && (event as PointerEvent).pointerType) {
21
+ return event.type === 'click' && (event as MouseEvent).buttons === 1;
22
+ }
23
+
24
+ return event.detail === 0 && !(event as PointerEvent).pointerType;
25
+ }
26
+
27
+ /**
28
+ * Checks if a pointer event was generated by a virtual source.
29
+ * iOS VoiceOver fires pointer events with incorrect coordinates.
30
+ * These events have zero width/height.
31
+ */
32
+ export function isVirtualPointerEvent(event: PointerEvent): boolean {
33
+ // If the pointer size is zero, then we assume it's from a screen reader.
34
+ // Android TalkBack double tap will sometimes return a event with width and height of 1
35
+ // and pointerType === 'mouse' so we need to check for a specific combination of event attributes.
36
+ // Cannot use "event.pressure === 0" as the sole check due to Safari pointer events always returning pressure === 0.
37
+ return (
38
+ (!isAndroid() && event.width === 0 && event.height === 0) ||
39
+ (event.width === 1 &&
40
+ event.height === 1 &&
41
+ event.pressure === 0 &&
42
+ event.detail === 0 &&
43
+ event.pointerType === 'mouse')
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Creates a synthetic mouse event for programmatic clicking.
49
+ */
50
+ export function createMouseEvent(type: string, nativeEvent?: Event): MouseEvent {
51
+ const init: MouseEventInit = {
52
+ bubbles: true,
53
+ cancelable: true,
54
+ view: window,
55
+ };
56
+
57
+ // Copy properties from the native event if provided
58
+ if (nativeEvent) {
59
+ const e = nativeEvent as MouseEvent;
60
+ init.screenX = e.screenX;
61
+ init.screenY = e.screenY;
62
+ init.clientX = e.clientX;
63
+ init.clientY = e.clientY;
64
+ init.ctrlKey = e.ctrlKey;
65
+ init.shiftKey = e.shiftKey;
66
+ init.altKey = e.altKey;
67
+ init.metaKey = e.metaKey;
68
+ init.button = e.button;
69
+ init.buttons = e.buttons;
70
+ init.relatedTarget = e.relatedTarget;
71
+ }
72
+
73
+ return new MouseEvent(type, init);
74
+ }
75
+
76
+ /**
77
+ * Creates a chain of event handlers that calls each in sequence.
78
+ */
79
+ export function chain<T extends (...args: any[]) => any>(
80
+ ...callbacks: (T | undefined | null)[]
81
+ ): T {
82
+ return ((...args: Parameters<T>) => {
83
+ for (const callback of callbacks) {
84
+ if (typeof callback === 'function') {
85
+ callback(...args);
86
+ }
87
+ }
88
+ }) as T;
89
+ }
90
+
91
+ /**
92
+ * Sets the target property on an event object.
93
+ * Used for synthetic events where target needs to be modified.
94
+ */
95
+ export function setEventTarget<T extends Event>(event: T, target: EventTarget): void {
96
+ Object.defineProperty(event, 'target', {
97
+ value: target,
98
+ writable: false,
99
+ configurable: true,
100
+ });
101
+ Object.defineProperty(event, 'currentTarget', {
102
+ value: target,
103
+ writable: false,
104
+ configurable: true,
105
+ });
106
+ }