@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,157 @@
1
+ /**
2
+ * createFocus - Handles focus events for the immediate target.
3
+ *
4
+ * This is a 1-1 port of React-Aria's useFocus hook adapted for SolidJS.
5
+ * Focus events on child elements will be ignored.
6
+ */
7
+
8
+ import { JSX, onCleanup } from 'solid-js';
9
+ import { getOwnerDocument, getEventTarget } from '../utils';
10
+
11
+ export interface FocusEvents {
12
+ /** Handler that is called when the element receives focus. */
13
+ onFocus?: (e: FocusEvent) => void;
14
+ /** Handler that is called when the element loses focus. */
15
+ onBlur?: (e: FocusEvent) => void;
16
+ /** Handler that is called when the element's focus status changes. */
17
+ onFocusChange?: (isFocused: boolean) => void;
18
+ }
19
+
20
+ export interface CreateFocusProps extends FocusEvents {
21
+ /** Whether the focus events should be disabled. */
22
+ isDisabled?: boolean;
23
+ }
24
+
25
+ export interface FocusResult {
26
+ /** Props to spread onto the target element. */
27
+ focusProps: JSX.HTMLAttributes<HTMLElement>;
28
+ }
29
+
30
+ /**
31
+ * Synthetic blur event handler for Firefox bug workaround.
32
+ * React (and we) don't fire onBlur when an element is disabled.
33
+ * Most browsers fire a native focusout event in this case, except for Firefox.
34
+ * We use a MutationObserver to watch for the disabled attribute.
35
+ */
36
+ function createSyntheticBlurHandler(
37
+ onBlur: ((e: FocusEvent) => void) | undefined
38
+ ): (_e: FocusEvent, target: Element) => (() => void) | undefined {
39
+ let isFocused = false;
40
+ let observer: MutationObserver | null = null;
41
+
42
+ return (_e: FocusEvent, target: Element) => {
43
+ if (
44
+ target instanceof HTMLButtonElement ||
45
+ target instanceof HTMLInputElement ||
46
+ target instanceof HTMLTextAreaElement ||
47
+ target instanceof HTMLSelectElement
48
+ ) {
49
+ isFocused = true;
50
+
51
+ const onBlurHandler = (blurEvent: Event) => {
52
+ isFocused = false;
53
+
54
+ if ((target as HTMLButtonElement).disabled && onBlur) {
55
+ onBlur(blurEvent as FocusEvent);
56
+ }
57
+
58
+ if (observer) {
59
+ observer.disconnect();
60
+ observer = null;
61
+ }
62
+ };
63
+
64
+ target.addEventListener('focusout', onBlurHandler, { once: true });
65
+
66
+ observer = new MutationObserver(() => {
67
+ if (isFocused && (target as HTMLButtonElement).disabled) {
68
+ observer?.disconnect();
69
+ const relatedTarget = target === document.activeElement ? null : document.activeElement;
70
+ target.dispatchEvent(new FocusEvent('blur', { relatedTarget }));
71
+ target.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget }));
72
+ }
73
+ });
74
+
75
+ observer.observe(target, { attributes: true, attributeFilter: ['disabled'] });
76
+
77
+ // Return cleanup function
78
+ return () => {
79
+ if (observer) {
80
+ observer.disconnect();
81
+ observer = null;
82
+ }
83
+ };
84
+ }
85
+
86
+ return undefined;
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Handles focus events for the immediate target.
92
+ * Focus events on child elements will be ignored.
93
+ *
94
+ * Based on react-aria's useFocus but adapted for SolidJS.
95
+ */
96
+ export function createFocus(props: CreateFocusProps = {}): FocusResult {
97
+ const { isDisabled, onFocus: onFocusProp, onBlur: onBlurProp, onFocusChange } = props;
98
+
99
+ let cleanupRef: (() => void) | undefined;
100
+ const syntheticBlurHandler = createSyntheticBlurHandler(onBlurProp);
101
+
102
+ // Cleanup on unmount
103
+ onCleanup(() => {
104
+ if (cleanupRef) {
105
+ cleanupRef();
106
+ }
107
+ });
108
+
109
+ const onBlur: JSX.EventHandler<HTMLElement, FocusEvent> = (e) => {
110
+ // Only handle if target is the currentTarget (not bubbled from children)
111
+ if (e.target === e.currentTarget) {
112
+ if (onBlurProp) {
113
+ onBlurProp(e);
114
+ }
115
+
116
+ if (onFocusChange) {
117
+ onFocusChange(false);
118
+ }
119
+ }
120
+ };
121
+
122
+ const onFocus: JSX.EventHandler<HTMLElement, FocusEvent> = (e) => {
123
+ // Double check that document.activeElement actually matches e.target
124
+ // in case a previously chained focus handler already moved focus somewhere else.
125
+ const ownerDocument = getOwnerDocument(e.target);
126
+ const activeElement = ownerDocument?.activeElement;
127
+
128
+ if (e.target === e.currentTarget && activeElement === getEventTarget(e)) {
129
+ if (onFocusProp) {
130
+ onFocusProp(e);
131
+ }
132
+
133
+ if (onFocusChange) {
134
+ onFocusChange(true);
135
+ }
136
+
137
+ // Set up synthetic blur handler for Firefox bug
138
+ cleanupRef = syntheticBlurHandler(e, e.target);
139
+ }
140
+ };
141
+
142
+ // If disabled or no handlers, return empty props
143
+ if (isDisabled) {
144
+ return {
145
+ focusProps: {},
146
+ };
147
+ }
148
+
149
+ const hasHandlers = onFocusProp || onFocusChange || onBlurProp;
150
+
151
+ return {
152
+ focusProps: {
153
+ onFocus: hasHandlers ? onFocus : undefined,
154
+ onBlur: onBlurProp || onFocusChange ? onBlur : undefined,
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * createFocusRing hook for Solidaria
3
+ *
4
+ * Determines whether a focus ring should be visible for a given element.
5
+ * Focus rings are visible when the user navigates with keyboard, but hidden
6
+ * when using mouse/touch.
7
+ *
8
+ * Port of @react-aria/focus useFocusRing.
9
+ */
10
+
11
+ import { type JSX, type Accessor, createSignal, createEffect, onCleanup } from 'solid-js';
12
+ import { createFocus } from './createFocus';
13
+
14
+ // ============================================
15
+ // TYPES
16
+ // ============================================
17
+
18
+ export interface FocusRingProps {
19
+ /** Whether the element is a text input. */
20
+ isTextInput?: boolean;
21
+ /** Whether the element will be auto focused. */
22
+ autoFocus?: boolean;
23
+ /** Whether focus should be tracked within the element. */
24
+ within?: boolean;
25
+ }
26
+
27
+ export interface FocusRingResult {
28
+ /** Whether the element is currently focused. */
29
+ isFocused: Accessor<boolean>;
30
+ /** Whether the focus ring should be visible. */
31
+ isFocusVisible: Accessor<boolean>;
32
+ /** Props to spread on the element to track focus. */
33
+ focusProps: JSX.HTMLAttributes<HTMLElement>;
34
+ }
35
+
36
+ // ============================================
37
+ // GLOBAL STATE
38
+ // ============================================
39
+
40
+ type Modality = 'keyboard' | 'pointer' | 'virtual';
41
+
42
+ let currentModality: Modality | null = null;
43
+ let hasSetupGlobalListeners = false;
44
+ let changeHandlers = new Set<(modality: Modality) => void>();
45
+
46
+ function triggerChangeHandlers(modality: Modality) {
47
+ currentModality = modality;
48
+ for (const handler of changeHandlers) {
49
+ handler(modality);
50
+ }
51
+ }
52
+
53
+ function handleKeyboardEvent(e: KeyboardEvent) {
54
+ // Ignore modifier keys
55
+ if (e.metaKey || e.altKey || e.ctrlKey || e.key === 'Control' || e.key === 'Shift' || e.key === 'Meta') {
56
+ return;
57
+ }
58
+ currentModality = 'keyboard';
59
+ triggerChangeHandlers('keyboard');
60
+ }
61
+
62
+ function handlePointerEvent(e: PointerEvent | MouseEvent) {
63
+ currentModality = 'pointer';
64
+ if (e.type === 'mousedown' || e.type === 'pointerdown') {
65
+ triggerChangeHandlers('pointer');
66
+ }
67
+ }
68
+
69
+ function setupGlobalFocusListeners() {
70
+ if (typeof document === 'undefined' || hasSetupGlobalListeners) {
71
+ return;
72
+ }
73
+
74
+ hasSetupGlobalListeners = true;
75
+
76
+ // Track keyboard vs pointer modality
77
+ document.addEventListener('keydown', handleKeyboardEvent, true);
78
+ document.addEventListener('keyup', handleKeyboardEvent, true);
79
+ document.addEventListener('mousedown', handlePointerEvent, true);
80
+ document.addEventListener('pointerdown', handlePointerEvent, true);
81
+ }
82
+
83
+ // ============================================
84
+ // IMPLEMENTATION
85
+ // ============================================
86
+
87
+ /**
88
+ * Determines whether a focus ring should be visible for a given element.
89
+ *
90
+ * Focus rings are visible when:
91
+ * - The element is focused AND
92
+ * - The user is navigating with keyboard (not mouse/touch)
93
+ *
94
+ * For text inputs, focus rings are always visible when focused.
95
+ */
96
+ export function createFocusRing(props: FocusRingProps = {}): FocusRingResult {
97
+ const { isTextInput = false, autoFocus = false } = props;
98
+
99
+ const [isFocused, setIsFocused] = createSignal(false);
100
+ const [isFocusVisible, setIsFocusVisible] = createSignal(autoFocus);
101
+ const [modality, setModality] = createSignal<Modality | null>(currentModality);
102
+
103
+ // Setup global listeners
104
+ createEffect(() => {
105
+ setupGlobalFocusListeners();
106
+
107
+ const handler = (newModality: Modality) => {
108
+ setModality(newModality);
109
+ };
110
+
111
+ changeHandlers.add(handler);
112
+ onCleanup(() => {
113
+ changeHandlers.delete(handler);
114
+ });
115
+ });
116
+
117
+ // Update focus visible based on modality and focus state
118
+ createEffect(() => {
119
+ const focused = isFocused();
120
+ const mod = modality();
121
+
122
+ if (focused) {
123
+ // Text inputs always show focus ring when focused
124
+ // Otherwise, only show if last interaction was keyboard
125
+ setIsFocusVisible(isTextInput || mod === 'keyboard');
126
+ } else {
127
+ setIsFocusVisible(false);
128
+ }
129
+ });
130
+
131
+ // Use createFocus to track focus state
132
+ const focusResult = createFocus({
133
+ onFocus: () => setIsFocused(true),
134
+ onBlur: () => setIsFocused(false),
135
+ });
136
+
137
+ return {
138
+ isFocused,
139
+ isFocusVisible,
140
+ focusProps: focusResult.focusProps as JSX.HTMLAttributes<HTMLElement>,
141
+ };
142
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * createFocusWithin - Handles focus events for the target and its descendants.
3
+ *
4
+ * This is a 1-1 port of React-Aria's useFocusWithin hook adapted for SolidJS.
5
+ */
6
+
7
+ import { JSX, onCleanup } from 'solid-js';
8
+ import { getOwnerDocument, getEventTarget, nodeContains, createGlobalListeners } from '../utils';
9
+ import { setEventTarget } from '../utils/events';
10
+
11
+ export interface FocusWithinProps {
12
+ /** Whether the focus within events should be disabled. */
13
+ isDisabled?: boolean;
14
+ /** Handler that is called when the target element or a descendant receives focus. */
15
+ onFocusWithin?: (e: FocusEvent) => void;
16
+ /** Handler that is called when the target element and all descendants lose focus. */
17
+ onBlurWithin?: (e: FocusEvent) => void;
18
+ /** Handler that is called when the focus within state changes. */
19
+ onFocusWithinChange?: (isFocusWithin: boolean) => void;
20
+ }
21
+
22
+ export interface FocusWithinResult {
23
+ /** Props to spread onto the target element. */
24
+ focusWithinProps: JSX.HTMLAttributes<HTMLElement>;
25
+ }
26
+
27
+ /**
28
+ * Handles focus events for the target and its descendants.
29
+ *
30
+ * Based on react-aria's useFocusWithin but adapted for SolidJS.
31
+ */
32
+ export function createFocusWithin(props: FocusWithinProps = {}): FocusWithinResult {
33
+ const { isDisabled, onBlurWithin, onFocusWithin, onFocusWithinChange } = props;
34
+
35
+ // State tracking
36
+ let isFocusWithin = false;
37
+
38
+ // Global listeners manager
39
+ const { addGlobalListener, removeAllGlobalListeners } = createGlobalListeners();
40
+
41
+ // Cleanup on unmount
42
+ onCleanup(() => {
43
+ removeAllGlobalListeners();
44
+ });
45
+
46
+ const onBlur: JSX.EventHandler<HTMLElement, FocusEvent> = (e) => {
47
+ // Ignore events bubbling through portals
48
+ if (!e.currentTarget.contains(e.target as Node)) {
49
+ return;
50
+ }
51
+
52
+ // We don't want to trigger onBlurWithin and then immediately onFocusWithin again
53
+ // when moving focus inside the element. Only trigger if the currentTarget doesn't
54
+ // include the relatedTarget (where focus is moving).
55
+ if (isFocusWithin && !e.currentTarget.contains(e.relatedTarget as Node)) {
56
+ isFocusWithin = false;
57
+ removeAllGlobalListeners();
58
+
59
+ if (onBlurWithin) {
60
+ onBlurWithin(e);
61
+ }
62
+
63
+ if (onFocusWithinChange) {
64
+ onFocusWithinChange(false);
65
+ }
66
+ }
67
+ };
68
+
69
+ const onFocus: JSX.EventHandler<HTMLElement, FocusEvent> = (e) => {
70
+ // Ignore events bubbling through portals
71
+ if (!e.currentTarget.contains(e.target as Node)) {
72
+ return;
73
+ }
74
+
75
+ // Double check that document.activeElement actually matches e.target
76
+ // in case a previously chained focus handler already moved focus somewhere else.
77
+ const ownerDocument = getOwnerDocument(e.target);
78
+ const activeElement = ownerDocument?.activeElement;
79
+
80
+ if (!isFocusWithin && activeElement === getEventTarget(e)) {
81
+ if (onFocusWithin) {
82
+ onFocusWithin(e);
83
+ }
84
+
85
+ if (onFocusWithinChange) {
86
+ onFocusWithinChange(true);
87
+ }
88
+
89
+ isFocusWithin = true;
90
+
91
+ // Browsers don't fire blur events when elements are removed from the DOM.
92
+ // However, if a focus event occurs outside the element we're tracking, we
93
+ // can manually fire onBlur.
94
+ const currentTarget = e.currentTarget;
95
+
96
+ addGlobalListener(
97
+ 'focus',
98
+ (focusEvent: Event) => {
99
+ if (isFocusWithin && !nodeContains(currentTarget, (focusEvent as FocusEvent).target as Element)) {
100
+ // Create a synthetic blur event
101
+ const window = ownerDocument?.defaultView;
102
+ if (window) {
103
+ const nativeEvent = new window.FocusEvent('blur', {
104
+ relatedTarget: (focusEvent as FocusEvent).target as Element,
105
+ });
106
+ setEventTarget(nativeEvent, currentTarget);
107
+
108
+ isFocusWithin = false;
109
+ removeAllGlobalListeners();
110
+
111
+ if (onBlurWithin) {
112
+ onBlurWithin(nativeEvent);
113
+ }
114
+
115
+ if (onFocusWithinChange) {
116
+ onFocusWithinChange(false);
117
+ }
118
+ }
119
+ }
120
+ },
121
+ { capture: true }
122
+ );
123
+ }
124
+ };
125
+
126
+ if (isDisabled) {
127
+ return {
128
+ focusWithinProps: {
129
+ onFocus: undefined,
130
+ onBlur: undefined,
131
+ },
132
+ };
133
+ }
134
+
135
+ return {
136
+ focusWithinProps: {
137
+ onFocus,
138
+ onBlur,
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * createFocusable - Makes an element focusable and capable of auto focus.
3
+ *
4
+ * This is a 1-1 port of React-Aria's useFocusable hook adapted for SolidJS.
5
+ */
6
+
7
+ import { JSX, Accessor, createContext, useContext, onMount } from 'solid-js';
8
+ import { createFocus, type FocusEvents } from './createFocus';
9
+ import { createKeyboard, type KeyboardEvents } from './createKeyboard';
10
+ import { mergeProps, focusSafely } from '../utils';
11
+
12
+ export interface FocusableDOMProps {
13
+ /** Whether to exclude the element from the sequential tab order. */
14
+ excludeFromTabOrder?: boolean;
15
+ }
16
+
17
+ export interface FocusableProps extends FocusEvents, KeyboardEvents {
18
+ /** Whether the element should receive focus on mount. */
19
+ autoFocus?: boolean;
20
+ }
21
+
22
+ export interface CreateFocusableProps extends FocusableProps, FocusableDOMProps {
23
+ /** Whether focus should be disabled. */
24
+ isDisabled?: Accessor<boolean> | boolean;
25
+ }
26
+
27
+ export interface FocusableResult {
28
+ /** Props to spread on the focusable element. */
29
+ focusableProps: JSX.HTMLAttributes<HTMLElement>;
30
+ }
31
+
32
+ // --- FocusableContext ---
33
+
34
+ export interface FocusableContextValue {
35
+ ref?: (el: HTMLElement) => void;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ /**
40
+ * Context for passing focusable props to nested focusable children.
41
+ * Used by FocusableProvider to pass DOM props to the nearest focusable child.
42
+ */
43
+ export const FocusableContext = createContext<FocusableContextValue | null>(null);
44
+
45
+ /**
46
+ * Hook to consume the FocusableContext and sync the ref.
47
+ */
48
+ function useFocusableContext(
49
+ setRef: (el: HTMLElement) => void
50
+ ): Omit<FocusableContextValue, 'ref'> {
51
+ const context = useContext(FocusableContext) || {};
52
+
53
+ // If context has a ref, sync our ref to it
54
+ if (context.ref) {
55
+ const contextRef = context.ref;
56
+ // Create a combined ref that calls both
57
+ const originalSetRef = setRef;
58
+ setRef = (el: HTMLElement) => {
59
+ originalSetRef(el);
60
+ contextRef(el);
61
+ };
62
+ }
63
+
64
+ // Return context without the ref
65
+ const { ref: _, ...otherProps } = context;
66
+ return otherProps;
67
+ }
68
+
69
+ export interface FocusableProviderProps {
70
+ /** The child element to provide DOM props to. */
71
+ children?: JSX.Element;
72
+ }
73
+
74
+ function isDisabledValue(isDisabled: Accessor<boolean> | boolean | undefined): boolean {
75
+ if (typeof isDisabled === 'function') {
76
+ return isDisabled();
77
+ }
78
+ return isDisabled ?? false;
79
+ }
80
+
81
+ /**
82
+ * Makes an element focusable, handling disabled state and tab order.
83
+ * Provides focus state tracking and autoFocus support.
84
+ *
85
+ * Based on react-aria's useFocusable but adapted for SolidJS.
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * import { createFocusable } from 'solidaria';
90
+ *
91
+ * function FocusableInput(props) {
92
+ * let ref;
93
+ * const { focusableProps } = createFocusable({
94
+ * autoFocus: props.autoFocus,
95
+ * onFocusChange: (focused) => console.log('Focus:', focused),
96
+ * });
97
+ *
98
+ * return (
99
+ * <input
100
+ * {...focusableProps}
101
+ * ref={(el) => { ref = el; focusableProps.ref?.(el); }}
102
+ * />
103
+ * );
104
+ * }
105
+ * ```
106
+ */
107
+ export function createFocusable(
108
+ props: CreateFocusableProps = {},
109
+ ref?: (el: HTMLElement) => void
110
+ ): FocusableResult {
111
+ let elementRef: HTMLElement | null = null;
112
+ let autoFocusDone = false;
113
+
114
+ // Set up ref handler
115
+ const setRef = (el: HTMLElement) => {
116
+ elementRef = el;
117
+ ref?.(el);
118
+ };
119
+
120
+ // Get focus and keyboard props from the respective hooks
121
+ const { focusProps } = createFocus({
122
+ isDisabled: isDisabledValue(props.isDisabled),
123
+ onFocus: props.onFocus,
124
+ onBlur: props.onBlur,
125
+ onFocusChange: props.onFocusChange,
126
+ });
127
+
128
+ const { keyboardProps } = createKeyboard({
129
+ isDisabled: isDisabledValue(props.isDisabled),
130
+ onKeyDown: props.onKeyDown,
131
+ onKeyUp: props.onKeyUp,
132
+ });
133
+
134
+ // Merge focus and keyboard interactions
135
+ const interactions = mergeProps(focusProps, keyboardProps);
136
+
137
+ // Get context props (from FocusableProvider if present)
138
+ const contextProps = useFocusableContext(setRef);
139
+ const interactionProps = isDisabledValue(props.isDisabled) ? {} : contextProps;
140
+
141
+ // Handle autoFocus
142
+ onMount(() => {
143
+ if (props.autoFocus && elementRef && !autoFocusDone) {
144
+ focusSafely(elementRef);
145
+ autoFocusDone = true;
146
+ }
147
+ });
148
+
149
+ // Always set a tabIndex so that Safari allows focusing native buttons and inputs.
150
+ let tabIndex: number | undefined = props.excludeFromTabOrder ? -1 : 0;
151
+ if (isDisabledValue(props.isDisabled)) {
152
+ tabIndex = undefined;
153
+ }
154
+
155
+ // Build final focusable props
156
+ const focusableProps = mergeProps(
157
+ {
158
+ ...interactions,
159
+ tabIndex,
160
+ ref: setRef,
161
+ },
162
+ interactionProps
163
+ ) as JSX.HTMLAttributes<HTMLElement>;
164
+
165
+ return {
166
+ focusableProps,
167
+ };
168
+ }