@proyecto-viviana/solidaria 0.0.1 → 0.0.2
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/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/button/createButton.ts +135 -0
- package/src/button/createToggleButton.ts +101 -0
- package/src/button/index.ts +4 -0
- package/src/button/types.ts +67 -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/index.ts +128 -0
- package/src/interactions/FocusableProvider.tsx +44 -0
- package/src/interactions/PressEvent.ts +112 -0
- package/src/interactions/createFocus.ts +157 -0
- package/src/interactions/createFocusRing.ts +142 -0
- package/src/interactions/createFocusWithin.ts +141 -0
- package/src/interactions/createFocusable.ts +168 -0
- package/src/interactions/createHover.ts +214 -0
- package/src/interactions/createKeyboard.ts +82 -0
- package/src/interactions/createPress.ts +758 -0
- package/src/interactions/index.ts +45 -0
- package/src/label/createField.ts +145 -0
- package/src/label/createLabel.ts +116 -0
- package/src/label/createLabels.ts +50 -0
- package/src/label/index.ts +19 -0
- package/src/link/createLink.ts +176 -0
- package/src/link/index.ts +1 -0
- package/src/progress/createProgressBar.ts +128 -0
- package/src/progress/index.ts +5 -0
- package/src/radio/createRadio.ts +286 -0
- package/src/radio/createRadioGroup.ts +189 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.ts +23 -0
- package/src/separator/createSeparator.ts +82 -0
- package/src/separator/index.ts +6 -0
- package/src/ssr/index.ts +36 -0
- package/src/switch/createSwitch.ts +70 -0
- package/src/switch/index.ts +1 -0
- package/src/textfield/createTextField.ts +198 -0
- package/src/textfield/index.ts +5 -0
- package/src/toggle/createToggle.ts +222 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.ts +7 -0
- package/src/utils/dom.ts +244 -0
- package/src/utils/events.ts +119 -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 +66 -0
- package/src/utils/mergeProps.ts +49 -0
- package/src/utils/platform.ts +52 -0
- package/src/utils/reactivity.ts +36 -0
- package/src/utils/textSelection.ts +114 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle state for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides state management for toggle components like checkboxes and switches.
|
|
5
|
+
*
|
|
6
|
+
* This is a 1:1 port of @react-stately/toggle's useToggleState.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createSignal, Accessor } from 'solid-js';
|
|
10
|
+
import { type MaybeAccessor, access } from '../utils/reactivity';
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// TYPES
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
export interface ToggleStateOptions {
|
|
17
|
+
/** Whether the element should be selected (controlled). */
|
|
18
|
+
isSelected?: boolean;
|
|
19
|
+
/** Whether the element should be selected by default (uncontrolled). */
|
|
20
|
+
defaultSelected?: boolean;
|
|
21
|
+
/** Handler that is called when the element's selection state changes. */
|
|
22
|
+
onChange?: (isSelected: boolean) => void;
|
|
23
|
+
/** Whether the element is read only. */
|
|
24
|
+
isReadOnly?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToggleState {
|
|
28
|
+
/** Whether the toggle is selected. */
|
|
29
|
+
readonly isSelected: Accessor<boolean>;
|
|
30
|
+
/** Whether the toggle is selected by default. */
|
|
31
|
+
readonly defaultSelected: boolean;
|
|
32
|
+
/** Updates selection state. */
|
|
33
|
+
setSelected(isSelected: boolean): void;
|
|
34
|
+
/** Toggle the selection state. */
|
|
35
|
+
toggle(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// IMPLEMENTATION
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Provides state management for toggle components like checkboxes and switches.
|
|
44
|
+
*/
|
|
45
|
+
export function createToggleState(props: MaybeAccessor<ToggleStateOptions> = {}): ToggleState {
|
|
46
|
+
const getProps = () => access(props);
|
|
47
|
+
|
|
48
|
+
// Get initial values
|
|
49
|
+
const initialProps = getProps();
|
|
50
|
+
const initialSelected = initialProps.isSelected ?? initialProps.defaultSelected ?? false;
|
|
51
|
+
|
|
52
|
+
// Create internal signal for uncontrolled mode
|
|
53
|
+
const [internalSelected, setInternalSelected] = createSignal(initialSelected);
|
|
54
|
+
|
|
55
|
+
// Determine if controlled
|
|
56
|
+
const isControlled = () => getProps().isSelected !== undefined;
|
|
57
|
+
|
|
58
|
+
// Get current selection state
|
|
59
|
+
const isSelected: Accessor<boolean> = () => {
|
|
60
|
+
const p = getProps();
|
|
61
|
+
return isControlled() ? (p.isSelected ?? false) : internalSelected();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Update selection state
|
|
65
|
+
function setSelected(value: boolean): void {
|
|
66
|
+
const p = getProps();
|
|
67
|
+
if (p.isReadOnly) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isControlled()) {
|
|
72
|
+
setInternalSelected(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
p.onChange?.(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Toggle selection state
|
|
79
|
+
function toggle(): void {
|
|
80
|
+
const p = getProps();
|
|
81
|
+
if (p.isReadOnly) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setSelected(!isSelected());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
isSelected,
|
|
90
|
+
defaultSelected: initialProps.defaultSelected ?? initialSelected,
|
|
91
|
+
setSelected,
|
|
92
|
+
toggle,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Re-export state from solid-stately
|
|
2
|
+
export { createToggleState } from '@proyecto-viviana/solid-stately';
|
|
3
|
+
export type { ToggleStateOptions, ToggleState } from '@proyecto-viviana/solid-stately';
|
|
4
|
+
|
|
5
|
+
// ARIA hook (solidaria-specific)
|
|
6
|
+
export { createToggle } from './createToggle';
|
|
7
|
+
export type { AriaToggleProps, ToggleAria } from './createToggle';
|
package/src/utils/dom.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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 = tagName === 'a' || role === 'link';
|
|
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
|
+
|
|
229
|
+
// Handle modifier keys for open-in-new-tab behavior
|
|
230
|
+
const keyEvent = event as KeyboardEvent;
|
|
231
|
+
const shouldOpenInNewTab =
|
|
232
|
+
linkTarget === '_blank' ||
|
|
233
|
+
keyEvent?.metaKey ||
|
|
234
|
+
keyEvent?.ctrlKey ||
|
|
235
|
+
keyEvent?.shiftKey ||
|
|
236
|
+
keyEvent?.altKey;
|
|
237
|
+
|
|
238
|
+
if (shouldOpenInNewTab) {
|
|
239
|
+
const features = !allowOpener && rel?.includes('noopener') ? 'noopener' : undefined;
|
|
240
|
+
window.open(href, linkTarget || '_blank', features);
|
|
241
|
+
} else {
|
|
242
|
+
window.location.href = href;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event utilities for detecting virtual clicks and event handling.
|
|
3
|
+
* Based on @react-aria/utils event utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a click event was generated from a virtual source like a screen reader.
|
|
8
|
+
* Virtual clicks typically have detail of 0 and may have zero coordinates.
|
|
9
|
+
*/
|
|
10
|
+
export function isVirtualClick(event: MouseEvent | PointerEvent): boolean {
|
|
11
|
+
// The detail property is 0 for non-mouse clicks (keyboard, screen reader)
|
|
12
|
+
// However, we need to be careful about actual clicks at (0,0) or events
|
|
13
|
+
// that have been re-dispatched
|
|
14
|
+
if ((event as any).mozInputSource === 0 && event.isTrusted) {
|
|
15
|
+
// Firefox bug: clicks from screen readers have mozInputSource = 0
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Android TalkBack fires click events with detail=1 but offsetX/offsetY = 0
|
|
20
|
+
if (isAndroidTalkBackClick(event)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Screen readers on Safari/Chrome have detail = 0
|
|
25
|
+
return event.detail === 0 && !(event as PointerEvent).pointerType;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detects Android TalkBack clicks which have detail=1 but zero offsets.
|
|
30
|
+
*/
|
|
31
|
+
function isAndroidTalkBackClick(event: MouseEvent | PointerEvent): boolean {
|
|
32
|
+
// TalkBack on Android sends click events with detail=1 but offsetX/Y = 0
|
|
33
|
+
return (
|
|
34
|
+
event.detail === 1 &&
|
|
35
|
+
(event as MouseEvent).offsetX === 0 &&
|
|
36
|
+
(event as MouseEvent).offsetY === 0
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a pointer event was generated by a virtual source.
|
|
42
|
+
* iOS VoiceOver fires pointer events with incorrect coordinates.
|
|
43
|
+
* These events have zero width/height.
|
|
44
|
+
*/
|
|
45
|
+
export function isVirtualPointerEvent(event: PointerEvent): boolean {
|
|
46
|
+
// Virtual events typically have no dimensions
|
|
47
|
+
// iOS VoiceOver fires pointer events with width=0, height=0
|
|
48
|
+
return (
|
|
49
|
+
(event.width === 0 && event.height === 0) ||
|
|
50
|
+
// Some screen readers send events with pointerType but no actual pointer
|
|
51
|
+
(event.pointerType === 'mouse' &&
|
|
52
|
+
event.width === 1 &&
|
|
53
|
+
event.height === 1 &&
|
|
54
|
+
event.pressure === 0 &&
|
|
55
|
+
event.detail === 0 &&
|
|
56
|
+
event.buttons === 0)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates a synthetic mouse event for programmatic clicking.
|
|
62
|
+
*/
|
|
63
|
+
export function createMouseEvent(type: string, nativeEvent?: Event): MouseEvent {
|
|
64
|
+
const init: MouseEventInit = {
|
|
65
|
+
bubbles: true,
|
|
66
|
+
cancelable: true,
|
|
67
|
+
view: window,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Copy properties from the native event if provided
|
|
71
|
+
if (nativeEvent) {
|
|
72
|
+
const e = nativeEvent as MouseEvent;
|
|
73
|
+
init.screenX = e.screenX;
|
|
74
|
+
init.screenY = e.screenY;
|
|
75
|
+
init.clientX = e.clientX;
|
|
76
|
+
init.clientY = e.clientY;
|
|
77
|
+
init.ctrlKey = e.ctrlKey;
|
|
78
|
+
init.shiftKey = e.shiftKey;
|
|
79
|
+
init.altKey = e.altKey;
|
|
80
|
+
init.metaKey = e.metaKey;
|
|
81
|
+
init.button = e.button;
|
|
82
|
+
init.buttons = e.buttons;
|
|
83
|
+
init.relatedTarget = e.relatedTarget;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new MouseEvent(type, init);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates a chain of event handlers that calls each in sequence.
|
|
91
|
+
*/
|
|
92
|
+
export function chain<T extends (...args: any[]) => any>(
|
|
93
|
+
...callbacks: (T | undefined | null)[]
|
|
94
|
+
): T {
|
|
95
|
+
return ((...args: Parameters<T>) => {
|
|
96
|
+
for (const callback of callbacks) {
|
|
97
|
+
if (typeof callback === 'function') {
|
|
98
|
+
callback(...args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}) as T;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sets the target property on an event object.
|
|
106
|
+
* Used for synthetic events where target needs to be modified.
|
|
107
|
+
*/
|
|
108
|
+
export function setEventTarget<T extends Event>(event: T, target: EventTarget): void {
|
|
109
|
+
Object.defineProperty(event, 'target', {
|
|
110
|
+
value: target,
|
|
111
|
+
writable: false,
|
|
112
|
+
configurable: true,
|
|
113
|
+
});
|
|
114
|
+
Object.defineProperty(event, 'currentTarget', {
|
|
115
|
+
value: target,
|
|
116
|
+
writable: false,
|
|
117
|
+
configurable: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* filterDOMProps for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Filters out all props that aren't valid DOM props.
|
|
5
|
+
* This is a 1:1 port of @react-aria/utils filterDOMProps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DOMPropNames = new Set(['id']);
|
|
9
|
+
|
|
10
|
+
const labelablePropNames = new Set([
|
|
11
|
+
'aria-label',
|
|
12
|
+
'aria-labelledby',
|
|
13
|
+
'aria-describedby',
|
|
14
|
+
'aria-details',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
// See LinkDOMProps in dom.d.ts.
|
|
18
|
+
const linkPropNames = new Set([
|
|
19
|
+
'href',
|
|
20
|
+
'hrefLang',
|
|
21
|
+
'target',
|
|
22
|
+
'rel',
|
|
23
|
+
'download',
|
|
24
|
+
'ping',
|
|
25
|
+
'referrerPolicy',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const globalAttrs = new Set(['dir', 'lang', 'hidden', 'inert', 'translate']);
|
|
29
|
+
|
|
30
|
+
const globalEvents = new Set([
|
|
31
|
+
'onClick',
|
|
32
|
+
'onAuxClick',
|
|
33
|
+
'onContextMenu',
|
|
34
|
+
'onDoubleClick',
|
|
35
|
+
'onMouseDown',
|
|
36
|
+
'onMouseEnter',
|
|
37
|
+
'onMouseLeave',
|
|
38
|
+
'onMouseMove',
|
|
39
|
+
'onMouseOut',
|
|
40
|
+
'onMouseOver',
|
|
41
|
+
'onMouseUp',
|
|
42
|
+
'onTouchCancel',
|
|
43
|
+
'onTouchEnd',
|
|
44
|
+
'onTouchMove',
|
|
45
|
+
'onTouchStart',
|
|
46
|
+
'onPointerDown',
|
|
47
|
+
'onPointerMove',
|
|
48
|
+
'onPointerUp',
|
|
49
|
+
'onPointerCancel',
|
|
50
|
+
'onPointerEnter',
|
|
51
|
+
'onPointerLeave',
|
|
52
|
+
'onPointerOver',
|
|
53
|
+
'onPointerOut',
|
|
54
|
+
'onGotPointerCapture',
|
|
55
|
+
'onLostPointerCapture',
|
|
56
|
+
'onScroll',
|
|
57
|
+
'onWheel',
|
|
58
|
+
'onAnimationStart',
|
|
59
|
+
'onAnimationEnd',
|
|
60
|
+
'onAnimationIteration',
|
|
61
|
+
'onTransitionCancel',
|
|
62
|
+
'onTransitionEnd',
|
|
63
|
+
'onTransitionRun',
|
|
64
|
+
'onTransitionStart',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const propRe = /^(data-.*)$/;
|
|
68
|
+
|
|
69
|
+
export interface FilterDOMPropsOptions {
|
|
70
|
+
/**
|
|
71
|
+
* If labelling associated aria properties should be included in the filter.
|
|
72
|
+
*/
|
|
73
|
+
labelable?: boolean;
|
|
74
|
+
/** Whether the element is a link and should include DOM props for <a> elements. */
|
|
75
|
+
isLink?: boolean;
|
|
76
|
+
/** Whether to include global DOM attributes. */
|
|
77
|
+
global?: boolean;
|
|
78
|
+
/** Whether to include DOM events. */
|
|
79
|
+
events?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* A Set of other property names that should be included in the filter.
|
|
82
|
+
*/
|
|
83
|
+
propNames?: Set<string>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Filters out all props that aren't valid DOM props or defined via override prop obj.
|
|
88
|
+
* @param props - The component props to be filtered.
|
|
89
|
+
* @param opts - Props to override.
|
|
90
|
+
*/
|
|
91
|
+
export function filterDOMProps(
|
|
92
|
+
props: Record<string, unknown>,
|
|
93
|
+
opts: FilterDOMPropsOptions = {}
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
const { labelable, isLink, global, events = global, propNames } = opts;
|
|
96
|
+
const filteredProps: Record<string, unknown> = {};
|
|
97
|
+
|
|
98
|
+
for (const prop in props) {
|
|
99
|
+
if (
|
|
100
|
+
Object.prototype.hasOwnProperty.call(props, prop) &&
|
|
101
|
+
(DOMPropNames.has(prop) ||
|
|
102
|
+
(labelable && labelablePropNames.has(prop)) ||
|
|
103
|
+
(isLink && linkPropNames.has(prop)) ||
|
|
104
|
+
(global && globalAttrs.has(prop)) ||
|
|
105
|
+
(events &&
|
|
106
|
+
(globalEvents.has(prop) ||
|
|
107
|
+
(prop.endsWith('Capture') && globalEvents.has(prop.slice(0, -7))))) ||
|
|
108
|
+
propNames?.has(prop) ||
|
|
109
|
+
propRe.test(prop))
|
|
110
|
+
) {
|
|
111
|
+
filteredProps[prop] = props[prop];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return filteredProps;
|
|
116
|
+
}
|