@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.
Files changed (56) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -3
  4. package/src/button/createButton.ts +135 -0
  5. package/src/button/createToggleButton.ts +101 -0
  6. package/src/button/index.ts +4 -0
  7. package/src/button/types.ts +67 -0
  8. package/src/checkbox/createCheckbox.ts +135 -0
  9. package/src/checkbox/createCheckboxGroup.ts +137 -0
  10. package/src/checkbox/createCheckboxGroupItem.ts +117 -0
  11. package/src/checkbox/createCheckboxGroupState.ts +193 -0
  12. package/src/checkbox/index.ts +13 -0
  13. package/src/index.ts +128 -0
  14. package/src/interactions/FocusableProvider.tsx +44 -0
  15. package/src/interactions/PressEvent.ts +112 -0
  16. package/src/interactions/createFocus.ts +157 -0
  17. package/src/interactions/createFocusRing.ts +142 -0
  18. package/src/interactions/createFocusWithin.ts +141 -0
  19. package/src/interactions/createFocusable.ts +168 -0
  20. package/src/interactions/createHover.ts +214 -0
  21. package/src/interactions/createKeyboard.ts +82 -0
  22. package/src/interactions/createPress.ts +758 -0
  23. package/src/interactions/index.ts +45 -0
  24. package/src/label/createField.ts +145 -0
  25. package/src/label/createLabel.ts +116 -0
  26. package/src/label/createLabels.ts +50 -0
  27. package/src/label/index.ts +19 -0
  28. package/src/link/createLink.ts +176 -0
  29. package/src/link/index.ts +1 -0
  30. package/src/progress/createProgressBar.ts +128 -0
  31. package/src/progress/index.ts +5 -0
  32. package/src/radio/createRadio.ts +286 -0
  33. package/src/radio/createRadioGroup.ts +189 -0
  34. package/src/radio/createRadioGroupState.ts +201 -0
  35. package/src/radio/index.ts +23 -0
  36. package/src/separator/createSeparator.ts +82 -0
  37. package/src/separator/index.ts +6 -0
  38. package/src/ssr/index.ts +36 -0
  39. package/src/switch/createSwitch.ts +70 -0
  40. package/src/switch/index.ts +1 -0
  41. package/src/textfield/createTextField.ts +198 -0
  42. package/src/textfield/index.ts +5 -0
  43. package/src/toggle/createToggle.ts +222 -0
  44. package/src/toggle/createToggleState.ts +94 -0
  45. package/src/toggle/index.ts +7 -0
  46. package/src/utils/dom.ts +244 -0
  47. package/src/utils/events.ts +119 -0
  48. package/src/utils/filterDOMProps.ts +116 -0
  49. package/src/utils/focus.ts +151 -0
  50. package/src/utils/geometry.ts +115 -0
  51. package/src/utils/globalListeners.ts +142 -0
  52. package/src/utils/index.ts +66 -0
  53. package/src/utils/mergeProps.ts +49 -0
  54. package/src/utils/platform.ts +52 -0
  55. package/src/utils/reactivity.ts +36 -0
  56. 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
+ }