@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.
- package/dist/autocomplete/createAutocomplete.d.ts +2 -2
- package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
- package/dist/index.js +233 -234
- package/dist/index.js.map +2 -2
- package/dist/index.ssr.js +233 -234
- package/dist/index.ssr.js.map +2 -2
- package/dist/interactions/PressEvent.d.ts +13 -10
- package/dist/interactions/PressEvent.d.ts.map +1 -1
- package/dist/interactions/createPress.d.ts.map +1 -1
- package/dist/interactions/index.d.ts +1 -1
- package/dist/interactions/index.d.ts.map +1 -1
- package/dist/select/createHiddenSelect.d.ts.map +1 -1
- package/dist/toolbar/createToolbar.d.ts.map +1 -1
- package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
- package/package.json +9 -7
- package/src/autocomplete/createAutocomplete.ts +341 -0
- package/src/autocomplete/index.ts +9 -0
- package/src/breadcrumbs/createBreadcrumbs.ts +196 -0
- package/src/breadcrumbs/index.ts +8 -0
- package/src/button/createButton.ts +142 -0
- package/src/button/createToggleButton.ts +101 -0
- package/src/button/index.ts +4 -0
- package/src/button/types.ts +78 -0
- package/src/calendar/createCalendar.ts +138 -0
- package/src/calendar/createCalendarCell.ts +187 -0
- package/src/calendar/createCalendarGrid.ts +140 -0
- package/src/calendar/createRangeCalendar.ts +136 -0
- package/src/calendar/createRangeCalendarCell.ts +186 -0
- package/src/calendar/index.ts +34 -0
- package/src/checkbox/createCheckbox.ts +135 -0
- package/src/checkbox/createCheckboxGroup.ts +137 -0
- package/src/checkbox/createCheckboxGroupItem.ts +117 -0
- package/src/checkbox/createCheckboxGroupState.ts +193 -0
- package/src/checkbox/index.ts +13 -0
- package/src/color/createColorArea.ts +314 -0
- package/src/color/createColorField.ts +137 -0
- package/src/color/createColorSlider.ts +197 -0
- package/src/color/createColorSwatch.ts +40 -0
- package/src/color/createColorWheel.ts +208 -0
- package/src/color/index.ts +24 -0
- package/src/color/types.ts +116 -0
- package/src/combobox/createComboBox.ts +647 -0
- package/src/combobox/index.ts +6 -0
- package/src/combobox/intl/en-US.json +7 -0
- package/src/combobox/intl/es-ES.json +7 -0
- package/src/combobox/intl/index.ts +23 -0
- package/src/datepicker/createDateField.ts +154 -0
- package/src/datepicker/createDatePicker.ts +206 -0
- package/src/datepicker/createDateSegment.ts +229 -0
- package/src/datepicker/createTimeField.ts +154 -0
- package/src/datepicker/index.ts +28 -0
- package/src/dialog/createDialog.ts +120 -0
- package/src/dialog/index.ts +2 -0
- package/src/dialog/types.ts +19 -0
- package/src/disclosure/createDisclosure.ts +131 -0
- package/src/disclosure/createDisclosureGroup.ts +62 -0
- package/src/disclosure/index.ts +11 -0
- package/src/dnd/createDrag.ts +209 -0
- package/src/dnd/createDraggableCollection.ts +63 -0
- package/src/dnd/createDraggableItem.ts +243 -0
- package/src/dnd/createDrop.ts +321 -0
- package/src/dnd/createDroppableCollection.ts +293 -0
- package/src/dnd/createDroppableItem.ts +213 -0
- package/src/dnd/index.ts +47 -0
- package/src/dnd/types.ts +89 -0
- package/src/dnd/utils.ts +294 -0
- package/src/focus/FocusScope.tsx +408 -0
- package/src/focus/createAutoFocus.ts +321 -0
- package/src/focus/createFocusRestore.ts +313 -0
- package/src/focus/createVirtualFocus.ts +396 -0
- package/src/focus/index.ts +35 -0
- package/src/form/createFormReset.ts +51 -0
- package/src/form/createFormValidation.ts +224 -0
- package/src/form/index.ts +11 -0
- package/src/grid/GridKeyboardDelegate.ts +429 -0
- package/src/grid/createGrid.ts +261 -0
- package/src/grid/createGridCell.ts +182 -0
- package/src/grid/createGridRow.ts +153 -0
- package/src/grid/index.ts +18 -0
- package/src/grid/types.ts +133 -0
- package/src/gridlist/createGridList.ts +185 -0
- package/src/gridlist/createGridListItem.ts +180 -0
- package/src/gridlist/createGridListSelectionCheckbox.ts +59 -0
- package/src/gridlist/index.ts +16 -0
- package/src/gridlist/types.ts +81 -0
- package/src/i18n/NumberFormatter.ts +266 -0
- package/src/i18n/createCollator.ts +79 -0
- package/src/i18n/createDateFormatter.ts +83 -0
- package/src/i18n/createFilter.ts +131 -0
- package/src/i18n/createNumberFormatter.ts +52 -0
- package/src/i18n/createStringFormatter.ts +87 -0
- package/src/i18n/index.ts +40 -0
- package/src/i18n/locale.tsx +188 -0
- package/src/i18n/utils.ts +99 -0
- package/src/index.ts +670 -0
- package/src/interactions/FocusableProvider.tsx +44 -0
- package/src/interactions/PressEvent.ts +126 -0
- package/src/interactions/createFocus.ts +163 -0
- package/src/interactions/createFocusRing.ts +89 -0
- package/src/interactions/createFocusWithin.ts +206 -0
- package/src/interactions/createFocusable.ts +168 -0
- package/src/interactions/createHover.ts +254 -0
- package/src/interactions/createInteractionModality.ts +424 -0
- package/src/interactions/createKeyboard.ts +82 -0
- package/src/interactions/createLongPress.ts +174 -0
- package/src/interactions/createMove.ts +289 -0
- package/src/interactions/createPress.ts +834 -0
- package/src/interactions/index.ts +78 -0
- package/src/label/createField.ts +145 -0
- package/src/label/createLabel.ts +117 -0
- package/src/label/createLabels.ts +50 -0
- package/src/label/index.ts +19 -0
- package/src/landmark/createLandmark.ts +377 -0
- package/src/landmark/index.ts +8 -0
- package/src/link/createLink.ts +182 -0
- package/src/link/index.ts +1 -0
- package/src/listbox/createListBox.ts +269 -0
- package/src/listbox/createOption.ts +151 -0
- package/src/listbox/index.ts +12 -0
- package/src/live-announcer/announce.ts +322 -0
- package/src/live-announcer/index.ts +9 -0
- package/src/menu/createMenu.ts +396 -0
- package/src/menu/createMenuItem.ts +149 -0
- package/src/menu/createMenuTrigger.ts +88 -0
- package/src/menu/index.ts +18 -0
- package/src/meter/createMeter.ts +75 -0
- package/src/meter/index.ts +1 -0
- package/src/numberfield/createNumberField.ts +268 -0
- package/src/numberfield/index.ts +5 -0
- package/src/overlays/ariaHideOutside.ts +219 -0
- package/src/overlays/createInteractOutside.ts +149 -0
- package/src/overlays/createModal.tsx +202 -0
- package/src/overlays/createOverlay.ts +155 -0
- package/src/overlays/createOverlayTrigger.ts +85 -0
- package/src/overlays/createPreventScroll.ts +266 -0
- package/src/overlays/index.ts +44 -0
- package/src/popover/calculatePosition.ts +766 -0
- package/src/popover/createOverlayPosition.ts +356 -0
- package/src/popover/createPopover.ts +170 -0
- package/src/popover/index.ts +24 -0
- package/src/progress/createProgressBar.ts +128 -0
- package/src/progress/index.ts +5 -0
- package/src/radio/createRadio.ts +287 -0
- package/src/radio/createRadioGroup.ts +189 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.ts +23 -0
- package/src/searchfield/createSearchField.ts +186 -0
- package/src/searchfield/index.ts +2 -0
- package/src/select/createHiddenSelect.tsx +236 -0
- package/src/select/createSelect.ts +395 -0
- package/src/select/index.ts +14 -0
- package/src/selection/createTypeSelect.ts +201 -0
- package/src/selection/index.ts +6 -0
- package/src/separator/createSeparator.ts +82 -0
- package/src/separator/index.ts +6 -0
- package/src/slider/createSlider.ts +349 -0
- package/src/slider/index.ts +2 -0
- package/src/ssr/index.tsx +370 -0
- package/src/switch/createSwitch.ts +70 -0
- package/src/switch/index.ts +1 -0
- package/src/table/createTable.ts +526 -0
- package/src/table/createTableCell.ts +147 -0
- package/src/table/createTableColumnHeader.ts +115 -0
- package/src/table/createTableHeaderRow.ts +40 -0
- package/src/table/createTableRow.ts +155 -0
- package/src/table/createTableRowGroup.ts +32 -0
- package/src/table/createTableSelectAllCheckbox.ts +73 -0
- package/src/table/createTableSelectionCheckbox.ts +59 -0
- package/src/table/index.ts +30 -0
- package/src/table/types.ts +165 -0
- package/src/tabs/createTabs.ts +472 -0
- package/src/tabs/index.ts +14 -0
- package/src/tag/createTag.ts +194 -0
- package/src/tag/createTagGroup.ts +154 -0
- package/src/tag/index.ts +12 -0
- package/src/textfield/createTextField.ts +198 -0
- package/src/textfield/index.ts +5 -0
- package/src/toast/createToast.ts +118 -0
- package/src/toast/createToastRegion.ts +100 -0
- package/src/toast/index.ts +11 -0
- package/src/toggle/createToggle.ts +223 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.ts +7 -0
- package/src/toolbar/createToolbar.ts +369 -0
- package/src/toolbar/index.ts +6 -0
- package/src/tooltip/createTooltip.ts +79 -0
- package/src/tooltip/createTooltipTrigger.ts +222 -0
- package/src/tooltip/index.ts +6 -0
- package/src/tree/createTree.ts +246 -0
- package/src/tree/createTreeItem.ts +233 -0
- package/src/tree/createTreeSelectionCheckbox.ts +68 -0
- package/src/tree/index.ts +16 -0
- package/src/tree/types.ts +87 -0
- package/src/utils/createDescription.ts +137 -0
- package/src/utils/dom.ts +327 -0
- package/src/utils/env.ts +54 -0
- package/src/utils/events.ts +106 -0
- package/src/utils/filterDOMProps.ts +116 -0
- package/src/utils/focus.ts +151 -0
- package/src/utils/geometry.ts +115 -0
- package/src/utils/globalListeners.ts +142 -0
- package/src/utils/index.ts +80 -0
- package/src/utils/mergeProps.ts +52 -0
- package/src/utils/platform.ts +52 -0
- package/src/utils/reactivity.ts +36 -0
- package/src/utils/textSelection.ts +114 -0
- package/src/visually-hidden/createVisuallyHidden.ts +124 -0
- package/src/visually-hidden/index.ts +6 -0
- package/dist/index.jsx +0 -15845
- 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
|
+
}
|
package/src/utils/dom.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -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
|
+
}
|