@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,82 @@
1
+ /**
2
+ * createSeparator - SolidJS implementation of React Aria's useSeparator
3
+ *
4
+ * A separator is a visual divider between two groups of content,
5
+ * e.g. groups of menu items or sections of a page.
6
+ */
7
+
8
+ import type { JSX } from 'solid-js';
9
+ import { access, type MaybeAccessor } from '../utils';
10
+ import { filterDOMProps } from '../utils';
11
+
12
+ // ============================================
13
+ // TYPES
14
+ // ============================================
15
+
16
+ export type Orientation = 'horizontal' | 'vertical';
17
+
18
+ export interface AriaSeparatorProps {
19
+ /**
20
+ * The orientation of the separator.
21
+ * @default 'horizontal'
22
+ */
23
+ orientation?: Orientation;
24
+ /**
25
+ * The HTML element type that will be used to render the separator.
26
+ * @default 'hr'
27
+ */
28
+ elementType?: string;
29
+ /** An accessibility label for the separator. */
30
+ 'aria-label'?: string;
31
+ /** Identifies the element(s) that labels the separator. */
32
+ 'aria-labelledby'?: string;
33
+ /** The element's unique identifier. */
34
+ id?: string;
35
+ }
36
+
37
+ export interface SeparatorAria {
38
+ /** Props for the separator element. */
39
+ separatorProps: JSX.HTMLAttributes<HTMLElement>;
40
+ }
41
+
42
+ // ============================================
43
+ // CREATE SEPARATOR
44
+ // ============================================
45
+
46
+ /**
47
+ * Provides the accessibility implementation for a separator.
48
+ * A separator is a visual divider between two groups of content,
49
+ * e.g. groups of menu items or sections of a page.
50
+ */
51
+ export function createSeparator(
52
+ props: MaybeAccessor<AriaSeparatorProps> = {}
53
+ ): SeparatorAria {
54
+ const getSeparatorProps = (): JSX.HTMLAttributes<HTMLElement> => {
55
+ const p = access(props);
56
+ const domProps = filterDOMProps(p as Record<string, unknown>, { labelable: true });
57
+
58
+ // if orientation is horizontal, aria-orientation default is horizontal, so we leave it undefined
59
+ // if it's vertical, we need to specify it
60
+ let ariaOrientation: 'vertical' | undefined;
61
+ if (p.orientation === 'vertical') {
62
+ ariaOrientation = 'vertical';
63
+ }
64
+
65
+ // hr elements implicitly have role = separator and a horizontal orientation
66
+ if (p.elementType !== 'hr') {
67
+ return {
68
+ ...domProps,
69
+ role: 'separator',
70
+ 'aria-orientation': ariaOrientation,
71
+ };
72
+ }
73
+
74
+ return domProps;
75
+ };
76
+
77
+ return {
78
+ get separatorProps() {
79
+ return getSeparatorProps();
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ createSeparator,
3
+ type AriaSeparatorProps,
4
+ type SeparatorAria,
5
+ type Orientation,
6
+ } from './createSeparator';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * SSR utilities for Solidaria
3
+ *
4
+ * SolidJS has built-in SSR support with `isServer` and `createUniqueId()`.
5
+ * These utilities provide a consistent API matching React-Aria's patterns.
6
+ */
7
+
8
+ import { createUniqueId } from 'solid-js';
9
+ import { isServer } from 'solid-js/web';
10
+
11
+ /**
12
+ * Returns whether the component is currently being server side rendered.
13
+ * Can be used to delay browser-specific rendering until after hydration.
14
+ */
15
+ export function createIsSSR(): boolean {
16
+ return isServer;
17
+ }
18
+
19
+ /**
20
+ * Generate a unique ID that is stable across server and client.
21
+ * Uses SolidJS's built-in createUniqueId which handles SSR correctly.
22
+ *
23
+ * @param defaultId - Optional default ID to use instead of generating one.
24
+ */
25
+ export function createId(defaultId?: string): string {
26
+ if (defaultId) {
27
+ return defaultId;
28
+ }
29
+ return `solidaria-${createUniqueId()}`;
30
+ }
31
+
32
+ /**
33
+ * Check if we can use DOM APIs.
34
+ * This is useful for code that needs to run only in the browser.
35
+ */
36
+ export const canUseDOM = !isServer;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Switch hook for Solidaria
3
+ *
4
+ * Provides the behavior and accessibility implementation for a switch component.
5
+ * A switch is similar to a checkbox, but represents on/off values as opposed to selection.
6
+ *
7
+ * This is a 1:1 port of @react-aria/switch's useSwitch hook.
8
+ */
9
+
10
+ import { JSX, Accessor } from 'solid-js';
11
+ import { createToggle, type AriaToggleProps } from '../toggle/createToggle';
12
+ import { type ToggleState } from '@proyecto-viviana/solid-stately';
13
+ import { type MaybeAccessor } from '../utils/reactivity';
14
+
15
+ // ============================================
16
+ // TYPES
17
+ // ============================================
18
+
19
+ export interface AriaSwitchProps extends AriaToggleProps {
20
+ // Switch uses the same props as toggle
21
+ }
22
+
23
+ export interface SwitchAria {
24
+ /** Props for the label wrapper element. */
25
+ labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
26
+ /** Props for the input element. */
27
+ inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
28
+ /** Whether the switch is selected. */
29
+ isSelected: Accessor<boolean>;
30
+ /** Whether the switch is in a pressed state. */
31
+ isPressed: Accessor<boolean>;
32
+ /** Whether the switch is disabled. */
33
+ isDisabled: boolean;
34
+ /** Whether the switch is read only. */
35
+ isReadOnly: boolean;
36
+ }
37
+
38
+ // ============================================
39
+ // IMPLEMENTATION
40
+ // ============================================
41
+
42
+ /**
43
+ * Provides the behavior and accessibility implementation for a switch component.
44
+ * A switch is similar to a checkbox, but represents on/off values as opposed to selection.
45
+ */
46
+ export function createSwitch(
47
+ props: MaybeAccessor<AriaSwitchProps>,
48
+ state: ToggleState,
49
+ ref: () => HTMLInputElement | null
50
+ ): SwitchAria {
51
+ // Don't destructure inputProps - it's a getter that needs to be evaluated each time
52
+ const toggle = createToggle(props, state, ref);
53
+
54
+ return {
55
+ labelProps: toggle.labelProps,
56
+ get inputProps() {
57
+ // Access toggle.inputProps (the getter) each time to get fresh values
58
+ const baseProps = toggle.inputProps;
59
+ return {
60
+ ...baseProps,
61
+ role: 'switch' as const,
62
+ checked: toggle.isSelected(),
63
+ };
64
+ },
65
+ isSelected: toggle.isSelected,
66
+ isPressed: toggle.isPressed,
67
+ isDisabled: toggle.isDisabled,
68
+ isReadOnly: toggle.isReadOnly,
69
+ };
70
+ }
@@ -0,0 +1 @@
1
+ export { createSwitch, type AriaSwitchProps, type SwitchAria } from './createSwitch';
@@ -0,0 +1,198 @@
1
+ /**
2
+ * TextField hook for Solidaria
3
+ *
4
+ * Provides the behavior and accessibility implementation for a text field.
5
+ *
6
+ * This is a 1:1 port of @react-aria/textfield's useTextField hook.
7
+ */
8
+
9
+ import { JSX } from 'solid-js';
10
+ import { createField, type AriaFieldProps, type FieldAria } from '../label';
11
+ import { createFocusable, type FocusableProps } from '../interactions';
12
+ import { mergeProps, filterDOMProps } from '../utils';
13
+ import { type MaybeAccessor, access } from '../utils/reactivity';
14
+
15
+ // ============================================
16
+ // TYPES
17
+ // ============================================
18
+
19
+ export interface AriaTextFieldProps extends AriaFieldProps, FocusableProps {
20
+ /** The current value (controlled). */
21
+ value?: string;
22
+ /** The default value (uncontrolled). */
23
+ defaultValue?: string;
24
+ /** Handler that is called when the value changes. */
25
+ onChange?: (value: string) => void;
26
+ /** Whether the input is disabled. */
27
+ isDisabled?: boolean;
28
+ /** Whether the input is read only. */
29
+ isReadOnly?: boolean;
30
+ /** Whether the input is required. */
31
+ isRequired?: boolean;
32
+ /** The type of input to render. */
33
+ type?: 'text' | 'search' | 'url' | 'tel' | 'email' | 'password' | string;
34
+ /** The input mode hint for virtual keyboards. */
35
+ inputMode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
36
+ /** The name of the input element, used when submitting an HTML form. */
37
+ name?: string;
38
+ /** Regex pattern to validate the input value. */
39
+ pattern?: string;
40
+ /** The maximum number of characters supported by the input. */
41
+ maxLength?: number;
42
+ /** The minimum number of characters required by the input. */
43
+ minLength?: number;
44
+ /** Placeholder text for the input. */
45
+ placeholder?: string;
46
+ /** Whether to enable auto complete. */
47
+ autoComplete?: string;
48
+ /** Whether to enable auto correct. */
49
+ autoCorrect?: string;
50
+ /** Whether to enable spell check. */
51
+ spellCheck?: 'true' | 'false';
52
+ /** Controls whether and how text input is automatically capitalized. */
53
+ autoCapitalize?: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters';
54
+ /** The element type to use for the input. Defaults to 'input'. */
55
+ inputElementType?: 'input' | 'textarea';
56
+
57
+ // Clipboard events
58
+ onCopy?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, ClipboardEvent>;
59
+ onCut?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, ClipboardEvent>;
60
+ onPaste?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, ClipboardEvent>;
61
+
62
+ // Composition events
63
+ onCompositionStart?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, CompositionEvent>;
64
+ onCompositionEnd?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, CompositionEvent>;
65
+ onCompositionUpdate?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, CompositionEvent>;
66
+
67
+ // Selection events
68
+ onSelect?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
69
+
70
+ // Input events
71
+ onBeforeInput?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
72
+ onInput?: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
73
+ }
74
+
75
+ export interface TextFieldAria<T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement> extends Omit<FieldAria, 'fieldProps'> {
76
+ /** Props for the input element. */
77
+ inputProps: JSX.InputHTMLAttributes<T>;
78
+ /** Whether the text field is invalid. */
79
+ isInvalid: boolean;
80
+ }
81
+
82
+ // ============================================
83
+ // IMPLEMENTATION
84
+ // ============================================
85
+
86
+ /**
87
+ * Provides the behavior and accessibility implementation for a text field.
88
+ * Text fields allow users to input text with a keyboard.
89
+ *
90
+ * @param props - Props for the text field.
91
+ * @param ref - Optional ref callback for the input element.
92
+ */
93
+ export function createTextField<T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(
94
+ props: MaybeAccessor<AriaTextFieldProps>,
95
+ ref?: (el: T) => void
96
+ ): TextFieldAria<T> {
97
+ const getProps = () => access(props);
98
+
99
+ // Get field accessibility props (label, description, error message)
100
+ const { labelProps, fieldProps, descriptionProps, errorMessageProps } = createField(props);
101
+
102
+ // Get focusable props
103
+ const { focusableProps } = createFocusable(
104
+ {
105
+ get isDisabled() {
106
+ return getProps().isDisabled;
107
+ },
108
+ get autoFocus() {
109
+ return getProps().autoFocus;
110
+ },
111
+ onFocus: getProps().onFocus,
112
+ onBlur: getProps().onBlur,
113
+ onFocusChange: getProps().onFocusChange,
114
+ onKeyDown: getProps().onKeyDown,
115
+ onKeyUp: getProps().onKeyUp,
116
+ },
117
+ ref as ((el: HTMLElement) => void) | undefined
118
+ );
119
+
120
+ // Filter DOM props
121
+ const getDomProps = () => filterDOMProps(getProps() as Record<string, unknown>, { labelable: true });
122
+
123
+ // Build input props
124
+ const getInputProps = (): JSX.InputHTMLAttributes<T> => {
125
+ const p = getProps();
126
+ const isInvalid = p.isInvalid ?? false;
127
+ const isTextarea = p.inputElementType === 'textarea';
128
+
129
+ return mergeProps(
130
+ getDomProps(),
131
+ {
132
+ disabled: p.isDisabled,
133
+ readOnly: p.isReadOnly,
134
+ required: p.isRequired,
135
+ 'aria-required': p.isRequired || undefined,
136
+ 'aria-invalid': isInvalid || undefined,
137
+ value: p.value ?? p.defaultValue ?? '',
138
+ onChange: (e: Event) => {
139
+ const target = e.target as HTMLInputElement | HTMLTextAreaElement;
140
+ p.onChange?.(target.value);
141
+ },
142
+ // Don't include type and pattern for textarea elements
143
+ type: isTextarea ? undefined : (p.type ?? 'text'),
144
+ inputMode: p.inputMode,
145
+ name: p.name,
146
+ pattern: isTextarea ? undefined : p.pattern,
147
+ maxLength: p.maxLength,
148
+ minLength: p.minLength,
149
+ placeholder: p.placeholder,
150
+ autoComplete: p.autoComplete,
151
+ autoCorrect: p.autoCorrect,
152
+ autoCapitalize: p.autoCapitalize,
153
+ spellCheck: p.spellCheck,
154
+
155
+ // Clipboard events
156
+ onCopy: p.onCopy,
157
+ onCut: p.onCut,
158
+ onPaste: p.onPaste,
159
+
160
+ // Composition events
161
+ onCompositionStart: p.onCompositionStart,
162
+ onCompositionEnd: p.onCompositionEnd,
163
+ onCompositionUpdate: p.onCompositionUpdate,
164
+
165
+ // Selection events
166
+ onSelect: p.onSelect,
167
+
168
+ // Input events
169
+ onBeforeInput: p.onBeforeInput,
170
+ onInput: p.onInput,
171
+ },
172
+ focusableProps as Record<string, unknown>,
173
+ fieldProps as Record<string, unknown>
174
+ ) as JSX.InputHTMLAttributes<T>;
175
+ };
176
+
177
+ const getIsInvalid = () => {
178
+ return getProps().isInvalid ?? false;
179
+ };
180
+
181
+ return {
182
+ get labelProps() {
183
+ return labelProps;
184
+ },
185
+ get inputProps() {
186
+ return getInputProps();
187
+ },
188
+ get descriptionProps() {
189
+ return descriptionProps;
190
+ },
191
+ get errorMessageProps() {
192
+ return errorMessageProps;
193
+ },
194
+ get isInvalid() {
195
+ return getIsInvalid();
196
+ },
197
+ };
198
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ createTextField,
3
+ type AriaTextFieldProps,
4
+ type TextFieldAria,
5
+ } from './createTextField';
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Toggle hook for Solidaria
3
+ *
4
+ * Handles interactions for toggle elements, e.g. Checkboxes and Switches.
5
+ *
6
+ * This is a 1:1 port of @react-aria/toggle's useToggle hook.
7
+ */
8
+
9
+ import { JSX, Accessor, createEffect } from 'solid-js';
10
+ import { createPress } from '../interactions/createPress';
11
+ import { createFocusable } from '../interactions/createFocusable';
12
+ import { mergeProps } from '../utils/mergeProps';
13
+ import { filterDOMProps } from '../utils/filterDOMProps';
14
+ import { type MaybeAccessor, access } from '../utils/reactivity';
15
+ import { type ToggleState } from '@proyecto-viviana/solid-stately';
16
+ import { type PressEvent } from '../interactions/PressEvent';
17
+
18
+ // ============================================
19
+ // TYPES
20
+ // ============================================
21
+
22
+ export interface AriaToggleProps {
23
+ /** Whether the element should be selected (controlled). */
24
+ isSelected?: boolean;
25
+ /** Whether the element should be selected by default (uncontrolled). */
26
+ defaultSelected?: boolean;
27
+ /** Handler that is called when the element's selection state changes. */
28
+ onChange?: (isSelected: boolean) => void;
29
+ /** The value of the input element, used when submitting an HTML form. */
30
+ value?: string;
31
+ /** The name of the input element, used when submitting an HTML form. */
32
+ name?: string;
33
+ /** The form to associate the input with. */
34
+ form?: string;
35
+ /** Whether the element is disabled. */
36
+ isDisabled?: boolean;
37
+ /** Whether the element is read only. */
38
+ isReadOnly?: boolean;
39
+ /** Whether the element is required. */
40
+ isRequired?: boolean;
41
+ /** Whether the element is invalid. */
42
+ isInvalid?: boolean;
43
+ /** The element's children. */
44
+ children?: JSX.Element;
45
+ /** Defines a string value that labels the current element. */
46
+ 'aria-label'?: string;
47
+ /** Identifies the element (or elements) that labels the current element. */
48
+ 'aria-labelledby'?: string;
49
+ /** Identifies the element (or elements) that describes the object. */
50
+ 'aria-describedby'?: string;
51
+ /** Identifies the element (or elements) that provide an error message for the object. */
52
+ 'aria-errormessage'?: string;
53
+ /** Identifies the element (or elements) whose contents or presence are controlled by the current element. */
54
+ 'aria-controls'?: string;
55
+ /** The element's unique identifier. */
56
+ id?: string;
57
+ /** Handler that is called when the press is released over the target. */
58
+ onPress?: (e: PressEvent) => void;
59
+ /** Handler that is called when a press interaction starts. */
60
+ onPressStart?: (e: PressEvent) => void;
61
+ /** Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. */
62
+ onPressEnd?: (e: PressEvent) => void;
63
+ /** Handler that is called when the press state changes. */
64
+ onPressChange?: (isPressed: boolean) => void;
65
+ /** Handler that is called when a press is released over the target, regardless of whether it started on the target or not. */
66
+ onPressUp?: (e: PressEvent) => void;
67
+ /** Handler that is called when the element is clicked. */
68
+ onClick?: (e: MouseEvent) => void;
69
+ /** Handler that is called when the element receives focus. */
70
+ onFocus?: (e: FocusEvent) => void;
71
+ /** Handler that is called when the element loses focus. */
72
+ onBlur?: (e: FocusEvent) => void;
73
+ /** Handler that is called when the element's focus status changes. */
74
+ onFocusChange?: (isFocused: boolean) => void;
75
+ /** Handler that is called when a key is pressed. */
76
+ onKeyDown?: (e: KeyboardEvent) => void;
77
+ /** Handler that is called when a key is released. */
78
+ onKeyUp?: (e: KeyboardEvent) => void;
79
+ /** Whether to exclude the element from the tab order. */
80
+ excludeFromTabOrder?: boolean;
81
+ /** Whether to autofocus the element. */
82
+ autoFocus?: boolean;
83
+ }
84
+
85
+ export interface ToggleAria {
86
+ /** Props to be spread on the label element. */
87
+ labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
88
+ /** Props to be spread on the input element. */
89
+ inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
90
+ /** Whether the toggle is selected. */
91
+ isSelected: Accessor<boolean>;
92
+ /** Whether the toggle is in a pressed state. */
93
+ isPressed: Accessor<boolean>;
94
+ /** Whether the toggle is disabled. */
95
+ isDisabled: boolean;
96
+ /** Whether the toggle is read only. */
97
+ isReadOnly: boolean;
98
+ /** Whether the toggle is invalid. */
99
+ isInvalid: boolean;
100
+ }
101
+
102
+ // ============================================
103
+ // IMPLEMENTATION
104
+ // ============================================
105
+
106
+ /**
107
+ * Handles interactions for toggle elements, e.g. Checkboxes and Switches.
108
+ */
109
+ export function createToggle(
110
+ props: MaybeAccessor<AriaToggleProps>,
111
+ state: ToggleState,
112
+ ref: () => HTMLInputElement | null
113
+ ): ToggleAria {
114
+ const getProps = () => access(props);
115
+
116
+ const isDisabled = () => getProps().isDisabled ?? false;
117
+ const isReadOnly = () => getProps().isReadOnly ?? false;
118
+ const isInvalid = () => {
119
+ return getProps().isInvalid ?? false;
120
+ };
121
+
122
+ // Handle press state for keyboard interactions and cases where labelProps is not used.
123
+ const { pressProps, isPressed } = createPress({
124
+ get onPressStart() { return getProps().onPressStart; },
125
+ get onPressEnd() { return getProps().onPressEnd; },
126
+ get onPressChange() { return getProps().onPressChange; },
127
+ get onPress() { return getProps().onPress; },
128
+ get onPressUp() { return getProps().onPressUp; },
129
+ get isDisabled() { return isDisabled(); },
130
+ });
131
+
132
+ // Handle press state on the label.
133
+ const { pressProps: labelPressProps, isPressed: isLabelPressed } = createPress({
134
+ get onPressStart() { return getProps().onPressStart; },
135
+ get onPressEnd() { return getProps().onPressEnd; },
136
+ get onPressChange() { return getProps().onPressChange; },
137
+ get onPressUp() { return getProps().onPressUp; },
138
+ onPress(e: PressEvent) {
139
+ getProps().onPress?.(e);
140
+ state.toggle();
141
+ ref()?.focus();
142
+ },
143
+ get isDisabled() { return isDisabled() || isReadOnly(); },
144
+ });
145
+
146
+ // Handle focusable - extract the relevant props for createFocusable
147
+ const { focusableProps } = createFocusable({
148
+ get isDisabled() { return isDisabled(); },
149
+ get autoFocus() { return getProps().autoFocus; },
150
+ get onFocus() { return getProps().onFocus; },
151
+ get onBlur() { return getProps().onBlur; },
152
+ get onFocusChange() { return getProps().onFocusChange; },
153
+ get onKeyDown() { return getProps().onKeyDown; },
154
+ get onKeyUp() { return getProps().onKeyUp; },
155
+ get excludeFromTabOrder() { return getProps().excludeFromTabOrder; },
156
+ }, ref as unknown as (el: HTMLElement) => void);
157
+
158
+ // Combine press and focusable props for input
159
+ const interactions = mergeProps(pressProps, focusableProps);
160
+
161
+ // Filter DOM props
162
+ const domProps = () => filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
163
+
164
+ // Handle input change
165
+ const onChange: JSX.EventHandler<HTMLInputElement, Event> = (e) => {
166
+ // Since we spread props on label, onChange will end up there as well as in here.
167
+ // So we have to stop propagation at the lowest level that we care about
168
+ e.stopPropagation();
169
+
170
+ // Don't update state if readonly
171
+ if (isReadOnly()) {
172
+ // Reset the checkbox to its previous state since the browser already toggled it
173
+ e.currentTarget.checked = state.isSelected();
174
+ return;
175
+ }
176
+
177
+ state.setSelected(e.currentTarget.checked);
178
+ };
179
+
180
+ // Warn if no accessible label
181
+ createEffect(() => {
182
+ const p = getProps();
183
+ const hasChildren = p.children != null;
184
+ const hasAriaLabel = p['aria-label'] != null || p['aria-labelledby'] != null;
185
+ if (!hasChildren && !hasAriaLabel && process.env.NODE_ENV !== 'production') {
186
+ console.warn('If you do not provide children, you must specify an aria-label for accessibility');
187
+ }
188
+ });
189
+
190
+ // Combined pressed state
191
+ const combinedIsPressed: Accessor<boolean> = () => isPressed() || isLabelPressed();
192
+
193
+ return {
194
+ labelProps: mergeProps(labelPressProps, {
195
+ onClick: (e: MouseEvent) => e.preventDefault(),
196
+ }),
197
+ get inputProps() {
198
+ const p = getProps();
199
+ return mergeProps(domProps(), {
200
+ 'aria-invalid': isInvalid() || undefined,
201
+ 'aria-errormessage': p['aria-errormessage'],
202
+ 'aria-controls': p['aria-controls'],
203
+ 'aria-readonly': isReadOnly() || undefined,
204
+ onChange,
205
+ disabled: isDisabled(),
206
+ ...(p.value == null ? {} : { value: p.value }),
207
+ name: p.name,
208
+ form: p.form,
209
+ type: 'checkbox' as const,
210
+ ...interactions,
211
+ // Stop click propagation to prevent labelProps.onClick from calling preventDefault
212
+ // which would prevent the checkbox from toggling in JSDOM/testing-library environments
213
+ onClick: (e: MouseEvent) => e.stopPropagation(),
214
+ }) as JSX.InputHTMLAttributes<HTMLInputElement>;
215
+ },
216
+ isSelected: state.isSelected,
217
+ isPressed: combinedIsPressed,
218
+ isDisabled: isDisabled(),
219
+ isReadOnly: isReadOnly(),
220
+ isInvalid: isInvalid(),
221
+ };
222
+ }