@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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus management utilities.
|
|
3
|
+
* Based on @react-aria/utils focus utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getOwnerDocument } from './dom';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Focuses an element without scrolling the page.
|
|
10
|
+
* Uses preventScroll option with fallback for older browsers.
|
|
11
|
+
*/
|
|
12
|
+
export function focusWithoutScrolling(element: HTMLElement | null): void {
|
|
13
|
+
if (!element) return;
|
|
14
|
+
|
|
15
|
+
// Try using the modern preventScroll option
|
|
16
|
+
try {
|
|
17
|
+
element.focus({ preventScroll: true });
|
|
18
|
+
} catch {
|
|
19
|
+
// Fallback for browsers that don't support preventScroll
|
|
20
|
+
// Save scroll positions and restore after focus
|
|
21
|
+
const scrollableElements = getScrollableAncestors(element);
|
|
22
|
+
const scrollPositions = scrollableElements.map((el) => ({
|
|
23
|
+
element: el,
|
|
24
|
+
scrollTop: el.scrollTop,
|
|
25
|
+
scrollLeft: el.scrollLeft,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
element.focus();
|
|
29
|
+
|
|
30
|
+
// Restore scroll positions
|
|
31
|
+
for (const { element: el, scrollTop, scrollLeft } of scrollPositions) {
|
|
32
|
+
el.scrollTop = scrollTop;
|
|
33
|
+
el.scrollLeft = scrollLeft;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets all scrollable ancestors of an element.
|
|
40
|
+
*/
|
|
41
|
+
function getScrollableAncestors(element: Element): Element[] {
|
|
42
|
+
const ancestors: Element[] = [];
|
|
43
|
+
let parent = element.parentElement;
|
|
44
|
+
|
|
45
|
+
while (parent) {
|
|
46
|
+
const style = getComputedStyle(parent);
|
|
47
|
+
const overflowY = style.overflowY;
|
|
48
|
+
const overflowX = style.overflowX;
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
overflowY === 'auto' ||
|
|
52
|
+
overflowY === 'scroll' ||
|
|
53
|
+
overflowX === 'auto' ||
|
|
54
|
+
overflowX === 'scroll'
|
|
55
|
+
) {
|
|
56
|
+
ancestors.push(parent);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
parent = parent.parentElement;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Also include the document scrolling element
|
|
63
|
+
const doc = getOwnerDocument(element);
|
|
64
|
+
ancestors.push(doc.documentElement);
|
|
65
|
+
|
|
66
|
+
return ancestors;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// State for preventFocus
|
|
70
|
+
let ignoreFocus = false;
|
|
71
|
+
let preventFocusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Prevents focus from moving to a new element temporarily.
|
|
75
|
+
* Used when clicking on a button that shouldn't steal focus.
|
|
76
|
+
*/
|
|
77
|
+
export function preventFocus(target: Element): void {
|
|
78
|
+
// Find the closest focusable ancestor
|
|
79
|
+
const focusableAncestor = findFocusableAncestor(target);
|
|
80
|
+
if (!focusableAncestor) return;
|
|
81
|
+
|
|
82
|
+
const document = getOwnerDocument(target);
|
|
83
|
+
const activeElement = document.activeElement;
|
|
84
|
+
|
|
85
|
+
// Set flag to ignore next focus event
|
|
86
|
+
ignoreFocus = true;
|
|
87
|
+
|
|
88
|
+
// Capture focus events and prevent them from changing focus
|
|
89
|
+
const onFocus = (e: Event) => {
|
|
90
|
+
if (ignoreFocus) {
|
|
91
|
+
e.stopImmediatePropagation();
|
|
92
|
+
// Refocus the original element if focus moved
|
|
93
|
+
if (activeElement && activeElement !== document.body) {
|
|
94
|
+
(activeElement as HTMLElement).focus();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onBlur = (e: Event) => {
|
|
100
|
+
if (ignoreFocus) {
|
|
101
|
+
e.stopImmediatePropagation();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Use capturing to intercept focus before it reaches elements
|
|
106
|
+
// Cast to HTMLElement to access focus event listeners
|
|
107
|
+
const el = focusableAncestor as HTMLElement;
|
|
108
|
+
el.addEventListener('focus', onFocus, true);
|
|
109
|
+
el.addEventListener('blur', onBlur, true);
|
|
110
|
+
el.addEventListener('focusin', onFocus, true);
|
|
111
|
+
el.addEventListener('focusout', onBlur, true);
|
|
112
|
+
|
|
113
|
+
// Clean up after the current event cycle
|
|
114
|
+
if (preventFocusTimeout != null) {
|
|
115
|
+
clearTimeout(preventFocusTimeout);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
preventFocusTimeout = setTimeout(() => {
|
|
119
|
+
ignoreFocus = false;
|
|
120
|
+
el.removeEventListener('focus', onFocus, true);
|
|
121
|
+
el.removeEventListener('blur', onBlur, true);
|
|
122
|
+
el.removeEventListener('focusin', onFocus, true);
|
|
123
|
+
el.removeEventListener('focusout', onBlur, true);
|
|
124
|
+
preventFocusTimeout = null;
|
|
125
|
+
}, 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Finds the closest focusable ancestor or the element itself.
|
|
130
|
+
*/
|
|
131
|
+
function findFocusableAncestor(element: Element): Element | null {
|
|
132
|
+
let current: Element | null = element;
|
|
133
|
+
|
|
134
|
+
while (current) {
|
|
135
|
+
if (
|
|
136
|
+
current.hasAttribute('tabindex') ||
|
|
137
|
+
['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'A'].includes(current.tagName)
|
|
138
|
+
) {
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
current = current.parentElement;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return element;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Safely focuses an element, alias for focusWithoutScrolling.
|
|
149
|
+
* This matches the react-aria focusSafely function name.
|
|
150
|
+
*/
|
|
151
|
+
export const focusSafely = focusWithoutScrolling;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geometry utilities for pointer/touch hit testing.
|
|
3
|
+
* Based on @react-aria/interactions geometry utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Rect {
|
|
7
|
+
top: number;
|
|
8
|
+
right: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
left: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EventPoint {
|
|
14
|
+
clientX: number;
|
|
15
|
+
clientY: number;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
radiusX?: number;
|
|
19
|
+
radiusY?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Checks if two rectangles overlap.
|
|
24
|
+
*/
|
|
25
|
+
export function areRectanglesOverlapping(a: Rect, b: Rect): boolean {
|
|
26
|
+
// Check if one rectangle is to the left of the other
|
|
27
|
+
if (a.left > b.right || b.left > a.right) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if one rectangle is above the other
|
|
32
|
+
if (a.top > b.bottom || b.top > a.bottom) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the bounding rectangle for an event point (touch/pointer).
|
|
41
|
+
* Takes into account the size of the touch point.
|
|
42
|
+
*/
|
|
43
|
+
export function getPointClientRect(point: EventPoint): Rect {
|
|
44
|
+
let offsetX = 0;
|
|
45
|
+
let offsetY = 0;
|
|
46
|
+
|
|
47
|
+
// Use width/height if available (PointerEvent)
|
|
48
|
+
if (point.width !== undefined && point.width > 0) {
|
|
49
|
+
offsetX = point.width / 2;
|
|
50
|
+
} else if (point.radiusX !== undefined && point.radiusX > 0) {
|
|
51
|
+
// Fallback to radiusX/radiusY (Touch)
|
|
52
|
+
offsetX = point.radiusX;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (point.height !== undefined && point.height > 0) {
|
|
56
|
+
offsetY = point.height / 2;
|
|
57
|
+
} else if (point.radiusY !== undefined && point.radiusY > 0) {
|
|
58
|
+
offsetY = point.radiusY;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
top: point.clientY - offsetY,
|
|
63
|
+
right: point.clientX + offsetX,
|
|
64
|
+
bottom: point.clientY + offsetY,
|
|
65
|
+
left: point.clientX - offsetX,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if a pointer/touch point is over an element.
|
|
71
|
+
*/
|
|
72
|
+
export function isPointOverTarget(point: EventPoint, target: Element): boolean {
|
|
73
|
+
const rect = target.getBoundingClientRect();
|
|
74
|
+
const pointRect = getPointClientRect(point);
|
|
75
|
+
|
|
76
|
+
return areRectanglesOverlapping(
|
|
77
|
+
{
|
|
78
|
+
top: rect.top,
|
|
79
|
+
right: rect.right,
|
|
80
|
+
bottom: rect.bottom,
|
|
81
|
+
left: rect.left,
|
|
82
|
+
},
|
|
83
|
+
pointRect
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets the first touch from a TouchEvent's targetTouches.
|
|
89
|
+
*/
|
|
90
|
+
export function getTouchFromEvent(event: TouchEvent): Touch | null {
|
|
91
|
+
const { targetTouches } = event;
|
|
92
|
+
if (targetTouches.length > 0) {
|
|
93
|
+
return targetTouches[0];
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Finds a touch by its identifier in changedTouches.
|
|
100
|
+
*/
|
|
101
|
+
export function getTouchById(event: TouchEvent, pointerId: number | null): Touch | null {
|
|
102
|
+
if (pointerId == null) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { changedTouches } = event;
|
|
107
|
+
for (let i = 0; i < changedTouches.length; i++) {
|
|
108
|
+
const touch = changedTouches[i];
|
|
109
|
+
if (touch.identifier === pointerId) {
|
|
110
|
+
return touch;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global listener management utility.
|
|
3
|
+
* Based on @react-aria/utils useGlobalListeners hook, adapted for SolidJS.
|
|
4
|
+
*
|
|
5
|
+
* In SolidJS, we use onCleanup for automatic cleanup instead of useEffect return.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { onCleanup } from 'solid-js';
|
|
9
|
+
|
|
10
|
+
export interface GlobalListenerOptions extends AddEventListenerOptions {
|
|
11
|
+
/** Whether to add the listener to the window instead of document */
|
|
12
|
+
isWindow?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a manager for global event listeners that automatically cleans up.
|
|
17
|
+
* Use this in a component to register document/window level listeners
|
|
18
|
+
* that will be removed when the component unmounts.
|
|
19
|
+
*/
|
|
20
|
+
export function createGlobalListeners() {
|
|
21
|
+
const listeners: Array<{
|
|
22
|
+
target: EventTarget;
|
|
23
|
+
type: string;
|
|
24
|
+
handler: EventListener;
|
|
25
|
+
options?: AddEventListenerOptions;
|
|
26
|
+
}> = [];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Adds a global event listener.
|
|
30
|
+
*/
|
|
31
|
+
function addGlobalListener<K extends keyof DocumentEventMap>(
|
|
32
|
+
type: K,
|
|
33
|
+
handler: (ev: DocumentEventMap[K]) => void,
|
|
34
|
+
options?: GlobalListenerOptions
|
|
35
|
+
): void;
|
|
36
|
+
function addGlobalListener<K extends keyof WindowEventMap>(
|
|
37
|
+
type: K,
|
|
38
|
+
handler: (ev: WindowEventMap[K]) => void,
|
|
39
|
+
options?: GlobalListenerOptions & { isWindow: true }
|
|
40
|
+
): void;
|
|
41
|
+
function addGlobalListener(
|
|
42
|
+
type: string,
|
|
43
|
+
handler: EventListener,
|
|
44
|
+
options?: GlobalListenerOptions
|
|
45
|
+
): void {
|
|
46
|
+
const target = options?.isWindow ? window : document;
|
|
47
|
+
const listenerOptions = options
|
|
48
|
+
? {
|
|
49
|
+
capture: options.capture,
|
|
50
|
+
passive: options.passive,
|
|
51
|
+
once: options.once,
|
|
52
|
+
}
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
target.addEventListener(type, handler, listenerOptions);
|
|
56
|
+
listeners.push({ target, type, handler, options: listenerOptions });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Removes a specific global event listener.
|
|
61
|
+
*/
|
|
62
|
+
function removeGlobalListener<K extends keyof DocumentEventMap>(
|
|
63
|
+
type: K,
|
|
64
|
+
handler: (ev: DocumentEventMap[K]) => void,
|
|
65
|
+
options?: AddEventListenerOptions
|
|
66
|
+
): void;
|
|
67
|
+
function removeGlobalListener<K extends keyof WindowEventMap>(
|
|
68
|
+
type: K,
|
|
69
|
+
handler: (ev: WindowEventMap[K]) => void,
|
|
70
|
+
options?: AddEventListenerOptions & { isWindow: true }
|
|
71
|
+
): void;
|
|
72
|
+
function removeGlobalListener(
|
|
73
|
+
type: string,
|
|
74
|
+
handler: EventListener,
|
|
75
|
+
options?: AddEventListenerOptions & { isWindow?: boolean }
|
|
76
|
+
): void {
|
|
77
|
+
const target = options?.isWindow ? window : document;
|
|
78
|
+
const listenerOptions = options
|
|
79
|
+
? {
|
|
80
|
+
capture: options.capture,
|
|
81
|
+
}
|
|
82
|
+
: undefined;
|
|
83
|
+
|
|
84
|
+
target.removeEventListener(type, handler, listenerOptions);
|
|
85
|
+
|
|
86
|
+
// Remove from tracked listeners
|
|
87
|
+
const index = listeners.findIndex(
|
|
88
|
+
(l) =>
|
|
89
|
+
l.target === target &&
|
|
90
|
+
l.type === type &&
|
|
91
|
+
l.handler === handler &&
|
|
92
|
+
l.options?.capture === listenerOptions?.capture
|
|
93
|
+
);
|
|
94
|
+
if (index !== -1) {
|
|
95
|
+
listeners.splice(index, 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Removes all registered global listeners.
|
|
101
|
+
*/
|
|
102
|
+
function removeAllGlobalListeners(): void {
|
|
103
|
+
for (const { target, type, handler, options } of listeners) {
|
|
104
|
+
target.removeEventListener(type, handler, options);
|
|
105
|
+
}
|
|
106
|
+
listeners.length = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Automatically clean up when the component/scope is disposed
|
|
110
|
+
onCleanup(removeAllGlobalListeners);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
addGlobalListener,
|
|
114
|
+
removeGlobalListener,
|
|
115
|
+
removeAllGlobalListeners,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Simple utility to add a single global listener with automatic cleanup.
|
|
121
|
+
* For one-off listeners where the full manager isn't needed.
|
|
122
|
+
*/
|
|
123
|
+
export function addGlobalListenerOnce<K extends keyof DocumentEventMap>(
|
|
124
|
+
type: K,
|
|
125
|
+
handler: (ev: DocumentEventMap[K]) => void,
|
|
126
|
+
options?: GlobalListenerOptions
|
|
127
|
+
): () => void {
|
|
128
|
+
const target = options?.isWindow ? window : document;
|
|
129
|
+
const listenerOptions = options
|
|
130
|
+
? {
|
|
131
|
+
capture: options.capture,
|
|
132
|
+
passive: options.passive,
|
|
133
|
+
once: options.once,
|
|
134
|
+
}
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
137
|
+
target.addEventListener(type, handler as EventListener, listenerOptions);
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
target.removeEventListener(type, handler as EventListener, listenerOptions);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export { mergeProps } from './mergeProps';
|
|
2
|
+
export { filterDOMProps, type FilterDOMPropsOptions } from './filterDOMProps';
|
|
3
|
+
|
|
4
|
+
// Reactivity utilities
|
|
5
|
+
export { access, isAccessor, type MaybeAccessor, type MaybeAccessorValue } from './reactivity';
|
|
6
|
+
|
|
7
|
+
// Platform detection
|
|
8
|
+
export {
|
|
9
|
+
isMac,
|
|
10
|
+
isIPhone,
|
|
11
|
+
isIPad,
|
|
12
|
+
isIOS,
|
|
13
|
+
isAppleDevice,
|
|
14
|
+
isWebKit,
|
|
15
|
+
isChrome,
|
|
16
|
+
isAndroid,
|
|
17
|
+
isFirefox,
|
|
18
|
+
} from './platform';
|
|
19
|
+
|
|
20
|
+
// DOM utilities
|
|
21
|
+
export {
|
|
22
|
+
getOwnerDocument,
|
|
23
|
+
getOwnerWindow,
|
|
24
|
+
nodeContains,
|
|
25
|
+
getEventTarget,
|
|
26
|
+
isFocusable,
|
|
27
|
+
isValidKeyboardEvent,
|
|
28
|
+
isValidInputKey,
|
|
29
|
+
isHTMLAnchorLink,
|
|
30
|
+
shouldPreventDefaultKeyboard,
|
|
31
|
+
shouldPreventDefaultUp,
|
|
32
|
+
openLink,
|
|
33
|
+
} from './dom';
|
|
34
|
+
|
|
35
|
+
// Geometry utilities
|
|
36
|
+
export {
|
|
37
|
+
areRectanglesOverlapping,
|
|
38
|
+
getPointClientRect,
|
|
39
|
+
isPointOverTarget,
|
|
40
|
+
getTouchFromEvent,
|
|
41
|
+
getTouchById,
|
|
42
|
+
type Rect,
|
|
43
|
+
type EventPoint,
|
|
44
|
+
} from './geometry';
|
|
45
|
+
|
|
46
|
+
// Event utilities
|
|
47
|
+
export {
|
|
48
|
+
isVirtualClick,
|
|
49
|
+
isVirtualPointerEvent,
|
|
50
|
+
createMouseEvent,
|
|
51
|
+
chain,
|
|
52
|
+
setEventTarget,
|
|
53
|
+
} from './events';
|
|
54
|
+
|
|
55
|
+
// Text selection management
|
|
56
|
+
export { disableTextSelection, restoreTextSelection } from './textSelection';
|
|
57
|
+
|
|
58
|
+
// Focus utilities
|
|
59
|
+
export { focusWithoutScrolling, focusSafely, preventFocus } from './focus';
|
|
60
|
+
|
|
61
|
+
// Global listener management
|
|
62
|
+
export {
|
|
63
|
+
createGlobalListeners,
|
|
64
|
+
addGlobalListenerOnce,
|
|
65
|
+
type GlobalListenerOptions,
|
|
66
|
+
} from './globalListeners';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type Props = { [key: string]: unknown };
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges multiple props objects together, handling event handlers specially
|
|
5
|
+
* by chaining them rather than replacing.
|
|
6
|
+
*
|
|
7
|
+
* Based on react-aria's mergeProps but adapted for SolidJS.
|
|
8
|
+
*/
|
|
9
|
+
export function mergeProps<T extends object>(...args: T[]): Record<string, unknown> {
|
|
10
|
+
const result: Props = {};
|
|
11
|
+
|
|
12
|
+
for (const props of args) {
|
|
13
|
+
for (const key in props) {
|
|
14
|
+
const value = props[key];
|
|
15
|
+
const existingValue = result[key];
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
typeof existingValue === 'function' &&
|
|
19
|
+
typeof value === 'function' &&
|
|
20
|
+
key.startsWith('on') &&
|
|
21
|
+
key[2] === key[2]?.toUpperCase()
|
|
22
|
+
) {
|
|
23
|
+
// Chain event handlers
|
|
24
|
+
result[key] = chainHandlers(existingValue as Function, value as Function);
|
|
25
|
+
} else if (key === 'class' || key === 'className') {
|
|
26
|
+
// Merge class names
|
|
27
|
+
result[key] = mergeClassNames(existingValue, value);
|
|
28
|
+
} else if (key === 'style' && typeof existingValue === 'object' && typeof value === 'object') {
|
|
29
|
+
// Merge style objects
|
|
30
|
+
result[key] = { ...(existingValue as object), ...(value as object) };
|
|
31
|
+
} else if (value !== undefined) {
|
|
32
|
+
result[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function chainHandlers(existingHandler: Function, newHandler: Function) {
|
|
41
|
+
return (...args: unknown[]) => {
|
|
42
|
+
existingHandler(...args);
|
|
43
|
+
newHandler(...args);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeClassNames(...classes: unknown[]): string {
|
|
48
|
+
return classes.filter(Boolean).join(' ');
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection utilities.
|
|
3
|
+
* Based on @react-aria/utils platform detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function testPlatform(re: RegExp): boolean {
|
|
7
|
+
return typeof window !== 'undefined' && window.navigator != null
|
|
8
|
+
? re.test(window.navigator.platform || (window.navigator as any).userAgentData?.platform || '')
|
|
9
|
+
: false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function testUserAgent(re: RegExp): boolean {
|
|
13
|
+
return typeof window !== 'undefined' && window.navigator != null
|
|
14
|
+
? re.test(window.navigator.userAgent)
|
|
15
|
+
: false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isMac(): boolean {
|
|
19
|
+
return testPlatform(/^Mac/i);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isIPhone(): boolean {
|
|
23
|
+
return testPlatform(/^iPhone/i);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isIPad(): boolean {
|
|
27
|
+
return testPlatform(/^iPad/i) || (isMac() && navigator.maxTouchPoints > 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isIOS(): boolean {
|
|
31
|
+
return isIPhone() || isIPad();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isAppleDevice(): boolean {
|
|
35
|
+
return isMac() || isIOS();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isWebKit(): boolean {
|
|
39
|
+
return testUserAgent(/AppleWebKit/i) && !isChrome();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isChrome(): boolean {
|
|
43
|
+
return testUserAgent(/Chrome/i);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isAndroid(): boolean {
|
|
47
|
+
return testUserAgent(/Android/i);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isFirefox(): boolean {
|
|
51
|
+
return testUserAgent(/Firefox/i);
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity utilities for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides type-safe utilities for working with SolidJS reactivity patterns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Accessor } from 'solid-js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A value that may be either a raw value or an accessor function.
|
|
11
|
+
* This is a common pattern in SolidJS for props that may be reactive.
|
|
12
|
+
*/
|
|
13
|
+
export type MaybeAccessor<T> = T | Accessor<T>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Unwraps a MaybeAccessor to get the underlying value.
|
|
17
|
+
* If the input is a function, it calls it to get the value.
|
|
18
|
+
* Otherwise, it returns the value directly.
|
|
19
|
+
*
|
|
20
|
+
* @param value - The value or accessor to unwrap.
|
|
21
|
+
*/
|
|
22
|
+
export function access<T>(value: MaybeAccessor<T>): T {
|
|
23
|
+
return typeof value === 'function' ? (value as Accessor<T>)() : value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A value that may be undefined or an accessor that returns the value or undefined.
|
|
28
|
+
*/
|
|
29
|
+
export type MaybeAccessorValue<T> = T | undefined | Accessor<T | undefined>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a value is an accessor function.
|
|
33
|
+
*/
|
|
34
|
+
export function isAccessor<T>(value: MaybeAccessor<T>): value is Accessor<T> {
|
|
35
|
+
return typeof value === 'function';
|
|
36
|
+
}
|