@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proyecto-viviana/solidaria",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A 1-1 SolidJS port of React Aria - accessible UI primitives",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,13 +8,14 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "solid": "./dist/index.js",
11
+ "solid": "./src/index.ts",
12
12
  "import": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts"
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "src"
18
19
  ],
19
20
  "sideEffects": false,
20
21
  "scripts": {
@@ -0,0 +1,135 @@
1
+ import { Accessor } from 'solid-js';
2
+ import { createPress } from '../interactions';
3
+ import { createFocusable } from '../interactions';
4
+ import { mergeProps, filterDOMProps } from '../utils';
5
+ import type { AriaButtonProps, ButtonAria } from './types';
6
+
7
+ function isDisabledValue(isDisabled: Accessor<boolean> | boolean | undefined): boolean {
8
+ if (typeof isDisabled === 'function') {
9
+ return isDisabled();
10
+ }
11
+ return isDisabled ?? false;
12
+ }
13
+
14
+ /**
15
+ * Provides the behavior and accessibility implementation for a button component.
16
+ * Handles press interactions across mouse, touch, keyboard and screen readers.
17
+ *
18
+ * Based on react-aria's useButton but adapted for SolidJS.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * import { createButton } from 'solidaria';
23
+ *
24
+ * function Button(props) {
25
+ * let ref;
26
+ * const { buttonProps, isPressed } = createButton(props);
27
+ *
28
+ * return (
29
+ * <button
30
+ * {...buttonProps}
31
+ * ref={ref}
32
+ * class={isPressed() ? 'pressed' : ''}
33
+ * >
34
+ * {props.children}
35
+ * </button>
36
+ * );
37
+ * }
38
+ * ```
39
+ */
40
+ export function createButton(props: AriaButtonProps = {}): ButtonAria {
41
+ const elementType = props.elementType ?? 'button';
42
+
43
+ const { pressProps, isPressed } = createPress({
44
+ isDisabled: props.isDisabled,
45
+ onPress: props.onPress,
46
+ onPressStart: props.onPressStart,
47
+ onPressEnd: props.onPressEnd,
48
+ onPressUp: props.onPressUp,
49
+ onPressChange: props.onPressChange,
50
+ preventFocusOnPress: props.preventFocusOnPress,
51
+ });
52
+
53
+ const { focusableProps } = createFocusable({
54
+ isDisabled: props.isDisabled,
55
+ autoFocus: props.autoFocus,
56
+ excludeFromTabOrder: props.excludeFromTabOrder,
57
+ });
58
+
59
+ const isNativeButton = elementType === 'button' || elementType === 'input';
60
+ const isLink = elementType === 'a';
61
+ const disabled = isDisabledValue(props.isDisabled);
62
+
63
+ // Build base props based on element type
64
+ let additionalProps: Record<string, unknown> = {};
65
+
66
+ if (isNativeButton) {
67
+ additionalProps = {
68
+ type: props.type ?? 'button',
69
+ disabled: disabled,
70
+ // Form-related attributes
71
+ ...(props.form && { form: props.form }),
72
+ ...(props.formAction && { formAction: props.formAction }),
73
+ ...(props.formEncType && { formEncType: props.formEncType }),
74
+ ...(props.formMethod && { formMethod: props.formMethod }),
75
+ ...(props.formNoValidate && { formNoValidate: props.formNoValidate }),
76
+ ...(props.formTarget && { formTarget: props.formTarget }),
77
+ ...(props.name && { name: props.name }),
78
+ ...(props.value && { value: props.value }),
79
+ };
80
+ } else {
81
+ // Non-native buttons need role and tabIndex
82
+ additionalProps = {
83
+ role: 'button',
84
+ tabIndex: disabled ? undefined : 0,
85
+ 'aria-disabled': disabled ? true : undefined,
86
+ };
87
+
88
+ if (isLink) {
89
+ additionalProps.href = props.href;
90
+ additionalProps.target = props.target;
91
+ additionalProps.rel = props.rel;
92
+ }
93
+ }
94
+
95
+ // ARIA attributes
96
+ const ariaProps: Record<string, unknown> = {};
97
+
98
+ if (props['aria-pressed'] !== undefined) {
99
+ ariaProps['aria-pressed'] = props['aria-pressed'];
100
+ }
101
+ if (props['aria-haspopup'] !== undefined) {
102
+ ariaProps['aria-haspopup'] = props['aria-haspopup'];
103
+ }
104
+ if (props['aria-expanded'] !== undefined) {
105
+ ariaProps['aria-expanded'] = props['aria-expanded'];
106
+ }
107
+ if (props['aria-label']) {
108
+ ariaProps['aria-label'] = props['aria-label'];
109
+ }
110
+ if (props['aria-labelledby']) {
111
+ ariaProps['aria-labelledby'] = props['aria-labelledby'];
112
+ }
113
+ if (props['aria-describedby']) {
114
+ ariaProps['aria-describedby'] = props['aria-describedby'];
115
+ }
116
+ if (props['aria-controls']) {
117
+ ariaProps['aria-controls'] = props['aria-controls'];
118
+ }
119
+ if (props['aria-current'] !== undefined) {
120
+ ariaProps['aria-current'] = props['aria-current'];
121
+ }
122
+
123
+ const buttonProps = mergeProps(
124
+ filterDOMProps(props as Record<string, unknown>, { labelable: true }),
125
+ additionalProps,
126
+ ariaProps,
127
+ focusableProps as Record<string, unknown>,
128
+ pressProps as Record<string, unknown>
129
+ );
130
+
131
+ return {
132
+ buttonProps,
133
+ isPressed,
134
+ };
135
+ }
@@ -0,0 +1,101 @@
1
+ import { Accessor, createSignal } from 'solid-js';
2
+ import { createButton } from './createButton';
3
+ import { mergeProps } from '../utils';
4
+ import type { AriaButtonProps, ButtonAria } from './types';
5
+ import type { PressEvent } from '../interactions';
6
+
7
+ export interface AriaToggleButtonProps extends Omit<AriaButtonProps, 'aria-pressed'> {
8
+ /** Whether the button is selected (controlled). */
9
+ isSelected?: Accessor<boolean> | boolean;
10
+ /** Handler called when the button's selection state changes. */
11
+ onChange?: (isSelected: boolean) => void;
12
+ /** The default selected state (uncontrolled). */
13
+ defaultSelected?: boolean;
14
+ }
15
+
16
+ export interface ToggleButtonAria extends ButtonAria {
17
+ /** Whether the button is currently selected. */
18
+ isSelected: Accessor<boolean>;
19
+ }
20
+
21
+ function getSelectedValue(isSelected: Accessor<boolean> | boolean | undefined): boolean {
22
+ if (typeof isSelected === 'function') {
23
+ return isSelected();
24
+ }
25
+ return isSelected ?? false;
26
+ }
27
+
28
+ /**
29
+ * Provides the behavior and accessibility implementation for a toggle button component.
30
+ * Toggle buttons allow users to toggle a selection on or off.
31
+ *
32
+ * Based on react-aria's useToggleButton but adapted for SolidJS.
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * import { createToggleButton } from 'solidaria';
37
+ *
38
+ * function ToggleButton(props) {
39
+ * const { buttonProps, isPressed, isSelected } = createToggleButton(props);
40
+ *
41
+ * return (
42
+ * <button
43
+ * {...buttonProps}
44
+ * class={isSelected() ? 'selected' : ''}
45
+ * style={{ opacity: isPressed() ? 0.8 : 1 }}
46
+ * >
47
+ * {props.children}
48
+ * </button>
49
+ * );
50
+ * }
51
+ * ```
52
+ */
53
+ export function createToggleButton(props: AriaToggleButtonProps = {}): ToggleButtonAria {
54
+ // Handle controlled vs uncontrolled state
55
+ const isControlled = props.isSelected !== undefined;
56
+ const [uncontrolledSelected, setUncontrolledSelected] = createSignal(
57
+ props.defaultSelected ?? false
58
+ );
59
+
60
+ const isSelected = (): boolean => {
61
+ if (isControlled) {
62
+ return getSelectedValue(props.isSelected);
63
+ }
64
+ return uncontrolledSelected();
65
+ };
66
+
67
+ const toggleSelection = () => {
68
+ const newValue = !isSelected();
69
+ if (!isControlled) {
70
+ setUncontrolledSelected(newValue);
71
+ }
72
+ props.onChange?.(newValue);
73
+ };
74
+
75
+ // Create the press handler that toggles selection
76
+ const onPress = (e: PressEvent) => {
77
+ toggleSelection();
78
+ props.onPress?.(e);
79
+ };
80
+
81
+ // Get button props with our custom press handler
82
+ const { buttonProps: baseButtonProps, isPressed } = createButton(
83
+ mergeProps(props, {
84
+ onPress,
85
+ }) as AriaButtonProps
86
+ );
87
+
88
+ // Create buttonProps with a getter for aria-pressed so it stays reactive
89
+ const buttonProps = {
90
+ ...baseButtonProps,
91
+ get 'aria-pressed'() {
92
+ return isSelected();
93
+ },
94
+ };
95
+
96
+ return {
97
+ buttonProps,
98
+ isPressed,
99
+ isSelected,
100
+ };
101
+ }
@@ -0,0 +1,4 @@
1
+ export { createButton } from './createButton';
2
+ export { createToggleButton } from './createToggleButton';
3
+ export type { AriaButtonProps, ButtonAria } from './types';
4
+ export type { AriaToggleButtonProps, ToggleButtonAria } from './createToggleButton';
@@ -0,0 +1,67 @@
1
+ import { Accessor } from 'solid-js';
2
+ import { PressEvent } from '../interactions';
3
+
4
+ export interface AriaButtonProps {
5
+ /** Whether the button is disabled. */
6
+ isDisabled?: Accessor<boolean> | boolean;
7
+ /** Handler called when the press is released over the target. */
8
+ onPress?: (e: PressEvent) => void;
9
+ /** Handler called when a press interaction starts. */
10
+ onPressStart?: (e: PressEvent) => void;
11
+ /** Handler called when a press interaction ends. */
12
+ onPressEnd?: (e: PressEvent) => void;
13
+ /** Handler called when a press is released over the target. */
14
+ onPressUp?: (e: PressEvent) => void;
15
+ /** Handler called when the press state changes. */
16
+ onPressChange?: (isPressed: boolean) => void;
17
+ /** Whether the button should not receive focus on press. */
18
+ preventFocusOnPress?: boolean;
19
+ /** Whether the element should receive focus on render. */
20
+ autoFocus?: boolean;
21
+ /** The HTML element type to use for the button. */
22
+ elementType?: 'button' | 'a' | 'div' | 'input' | 'span';
23
+ /** The URL to link to (for anchor elements). */
24
+ href?: string;
25
+ /** The target for the link (for anchor elements). */
26
+ target?: string;
27
+ /** The rel attribute for the link (for anchor elements). */
28
+ rel?: string;
29
+ /** The type attribute for button elements. */
30
+ type?: 'button' | 'submit' | 'reset';
31
+ /** Whether the button is in a pressed state (controlled). */
32
+ 'aria-pressed'?: boolean | 'true' | 'false' | 'mixed';
33
+ /** Whether the button has a popup. */
34
+ 'aria-haspopup'?: boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false';
35
+ /** Whether the popup is expanded. */
36
+ 'aria-expanded'?: boolean | 'true' | 'false';
37
+ /** The accessible label for the button. */
38
+ 'aria-label'?: string;
39
+ /** The id of the element that labels the button. */
40
+ 'aria-labelledby'?: string;
41
+ /** The id of the element that describes the button. */
42
+ 'aria-describedby'?: string;
43
+ /** Identifies the element (or elements) whose contents or presence are controlled by the button. */
44
+ 'aria-controls'?: string;
45
+ /** Indicates the current "pressed" state of toggle buttons. */
46
+ 'aria-current'?: boolean | 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false';
47
+ /** Additional attributes for form buttons. */
48
+ form?: string;
49
+ formAction?: string;
50
+ formEncType?: string;
51
+ formMethod?: string;
52
+ formNoValidate?: boolean;
53
+ formTarget?: string;
54
+ /** The name attribute for form buttons. */
55
+ name?: string;
56
+ /** The value attribute for form buttons. */
57
+ value?: string;
58
+ /** Whether to exclude the button from the tab order. */
59
+ excludeFromTabOrder?: boolean;
60
+ }
61
+
62
+ export interface ButtonAria {
63
+ /** Props to spread on the button element. */
64
+ buttonProps: Record<string, unknown>;
65
+ /** Whether the button is currently pressed. */
66
+ isPressed: Accessor<boolean>;
67
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Checkbox hook for Solidaria
3
+ *
4
+ * Provides the behavior and accessibility implementation for a checkbox component.
5
+ * Checkboxes allow users to select multiple items from a list of individual items,
6
+ * or to mark one individual item as selected.
7
+ *
8
+ * This is a 1:1 port of @react-aria/checkbox's useCheckbox hook.
9
+ */
10
+
11
+ import { JSX, Accessor, createEffect } from 'solid-js';
12
+ import { createToggle, type AriaToggleProps } from '../toggle';
13
+ import { type ToggleState } from '@proyecto-viviana/solid-stately';
14
+ import { createPress } from '../interactions/createPress';
15
+ import { mergeProps } from '../utils/mergeProps';
16
+ import { type MaybeAccessor, access } from '../utils/reactivity';
17
+
18
+ // ============================================
19
+ // TYPES
20
+ // ============================================
21
+
22
+ export interface AriaCheckboxProps extends AriaToggleProps {
23
+ /**
24
+ * Indeterminism is presentational only.
25
+ * The indeterminate visual representation remains regardless of user interaction.
26
+ */
27
+ isIndeterminate?: boolean;
28
+ /**
29
+ * Whether the checkbox is required.
30
+ */
31
+ isRequired?: boolean;
32
+ /**
33
+ * The validation behavior for the checkbox.
34
+ * @default 'aria'
35
+ */
36
+ validationBehavior?: 'aria' | 'native';
37
+ }
38
+
39
+ export interface CheckboxAria {
40
+ /** Props for the label wrapper element. */
41
+ labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
42
+ /** Props for the input element. */
43
+ inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
44
+ /** Whether the checkbox is selected. */
45
+ isSelected: Accessor<boolean>;
46
+ /** Whether the checkbox is in a pressed state. */
47
+ isPressed: Accessor<boolean>;
48
+ /** Whether the checkbox is disabled. */
49
+ isDisabled: boolean;
50
+ /** Whether the checkbox is read only. */
51
+ isReadOnly: boolean;
52
+ /** Whether the checkbox is invalid. */
53
+ isInvalid: boolean;
54
+ }
55
+
56
+ // ============================================
57
+ // IMPLEMENTATION
58
+ // ============================================
59
+
60
+ /**
61
+ * Provides the behavior and accessibility implementation for a checkbox component.
62
+ * Checkboxes allow users to select multiple items from a list of individual items,
63
+ * or to mark one individual item as selected.
64
+ *
65
+ * @param props - Props for the checkbox.
66
+ * @param state - State for the checkbox, as returned by `createToggleState`.
67
+ * @param inputRef - A ref accessor for the HTML input element.
68
+ */
69
+ export function createCheckbox(
70
+ props: MaybeAccessor<AriaCheckboxProps>,
71
+ state: ToggleState,
72
+ inputRef: () => HTMLInputElement | null
73
+ ): CheckboxAria {
74
+ const getProps = () => access(props);
75
+
76
+ // Get toggle aria props
77
+ const toggleResult = createToggle(props, state, inputRef);
78
+ const {
79
+ labelProps: baseLabelProps,
80
+ isSelected,
81
+ isPressed,
82
+ isDisabled,
83
+ isReadOnly,
84
+ isInvalid,
85
+ } = toggleResult;
86
+
87
+ // Handle indeterminate state
88
+ createEffect(() => {
89
+ const input = inputRef();
90
+ const isIndeterminate = getProps().isIndeterminate;
91
+ if (input) {
92
+ // indeterminate is a property, but it can only be set via javascript
93
+ // https://css-tricks.com/indeterminate-checkboxes/
94
+ input.indeterminate = !!isIndeterminate;
95
+ }
96
+ });
97
+
98
+ // Reset validation state on label press for checkbox with a hidden input.
99
+ const { pressProps } = createPress({
100
+ get isDisabled() {
101
+ return isDisabled || isReadOnly;
102
+ },
103
+ onPress() {
104
+ // Validation state reset would be handled here if we had form validation
105
+ // For now, this is a no-op placeholder matching React-Aria's pattern
106
+ },
107
+ });
108
+
109
+ return {
110
+ labelProps: mergeProps(
111
+ baseLabelProps as unknown as Record<string, unknown>,
112
+ pressProps as unknown as Record<string, unknown>,
113
+ {
114
+ // Prevent label from being focused when mouse down on it.
115
+ // Note, this does not prevent the input from being focused in the `click` event.
116
+ onMouseDown: (e: MouseEvent) => e.preventDefault(),
117
+ } as Record<string, unknown>
118
+ ) as JSX.LabelHTMLAttributes<HTMLLabelElement>,
119
+ get inputProps() {
120
+ const p = getProps();
121
+ const { isRequired, validationBehavior = 'aria' } = p;
122
+
123
+ return mergeProps(toggleResult.inputProps, {
124
+ checked: isSelected(),
125
+ 'aria-required': (isRequired && validationBehavior === 'aria') || undefined,
126
+ required: isRequired && validationBehavior === 'native',
127
+ }) as JSX.InputHTMLAttributes<HTMLInputElement>;
128
+ },
129
+ isSelected,
130
+ isPressed,
131
+ isDisabled,
132
+ isReadOnly,
133
+ isInvalid,
134
+ };
135
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Checkbox group hook for Solidaria
3
+ *
4
+ * Provides the behavior and accessibility implementation for a checkbox group component.
5
+ * Checkbox groups allow users to select multiple items from a list of options.
6
+ *
7
+ * This is a 1:1 port of @react-aria/checkbox's useCheckboxGroup hook.
8
+ */
9
+
10
+ import { JSX } from 'solid-js';
11
+ import { createField } from '../label';
12
+ import { createFocusWithin } from '../interactions/createFocusWithin';
13
+ import { filterDOMProps } from '../utils/filterDOMProps';
14
+ import { mergeProps } from '../utils/mergeProps';
15
+ import { type MaybeAccessor, access } from '../utils/reactivity';
16
+ import { type CheckboxGroupState, type CheckboxGroupProps } from '@proyecto-viviana/solid-stately';
17
+
18
+ // ============================================
19
+ // TYPES
20
+ // ============================================
21
+
22
+ export interface AriaCheckboxGroupProps extends CheckboxGroupProps {
23
+ /** Defines a string value that labels the current element. */
24
+ 'aria-label'?: string;
25
+ /** Identifies the element (or elements) that labels the current element. */
26
+ 'aria-labelledby'?: string;
27
+ /** Identifies the element (or elements) that describes the object. */
28
+ 'aria-describedby'?: string;
29
+ /** Identifies the element (or elements) that provide a detailed, extended description for the object. */
30
+ 'aria-details'?: string;
31
+ /** A description for the field. Provides a hint such as specific requirements for what to choose. */
32
+ description?: JSX.Element;
33
+ /** An error message for the field. */
34
+ errorMessage?: JSX.Element;
35
+ }
36
+
37
+ export interface CheckboxGroupAria {
38
+ /** Props for the checkbox group wrapper element. */
39
+ groupProps: JSX.HTMLAttributes<HTMLElement>;
40
+ /** Props for the checkbox group's visible label (if any). */
41
+ labelProps: JSX.HTMLAttributes<HTMLElement>;
42
+ /** Props for the checkbox group description element, if any. */
43
+ descriptionProps: JSX.HTMLAttributes<HTMLElement>;
44
+ /** Props for the checkbox group error message element, if any. */
45
+ errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
46
+ /** Whether the checkbox group is invalid. */
47
+ isInvalid: boolean;
48
+ }
49
+
50
+ // WeakMap to share data between checkbox group and checkbox group items
51
+ export const checkboxGroupData = new WeakMap<
52
+ CheckboxGroupState,
53
+ {
54
+ name?: string;
55
+ form?: string;
56
+ descriptionId?: string;
57
+ errorMessageId?: string;
58
+ validationBehavior: 'aria' | 'native';
59
+ }
60
+ >();
61
+
62
+ // ============================================
63
+ // IMPLEMENTATION
64
+ // ============================================
65
+
66
+ /**
67
+ * Provides the behavior and accessibility implementation for a checkbox group component.
68
+ * Checkbox groups allow users to select multiple items from a list of options.
69
+ *
70
+ * @param props - Props for the checkbox group.
71
+ * @param state - State for the checkbox group, as returned by `createCheckboxGroupState`.
72
+ */
73
+ export function createCheckboxGroup(
74
+ props: MaybeAccessor<AriaCheckboxGroupProps>,
75
+ state: CheckboxGroupState
76
+ ): CheckboxGroupAria {
77
+ const getProps = () => access(props);
78
+
79
+ const isInvalid = () => state.isInvalid;
80
+
81
+ // Use field for label association
82
+ const { labelProps, fieldProps, descriptionProps, errorMessageProps } = createField({
83
+ get label() { return getProps().label; },
84
+ get 'aria-label'() { return getProps()['aria-label']; },
85
+ get 'aria-labelledby'() { return getProps()['aria-labelledby']; },
86
+ get 'aria-describedby'() { return getProps()['aria-describedby']; },
87
+ get 'aria-details'() { return getProps()['aria-details']; },
88
+ get description() { return getProps().description; },
89
+ get errorMessage() { return getProps().errorMessage ?? (isInvalid() ? 'Invalid selection' : undefined); },
90
+ get isInvalid() { return isInvalid(); },
91
+ // Checkbox group is not an HTML input element so it
92
+ // shouldn't be labeled by a <label> element.
93
+ labelElementType: 'span',
94
+ });
95
+
96
+ // Store data for checkbox group items
97
+ checkboxGroupData.set(state, {
98
+ name: getProps().name,
99
+ form: getProps().form,
100
+ descriptionId: descriptionProps.id,
101
+ errorMessageId: errorMessageProps.id,
102
+ validationBehavior: 'aria',
103
+ });
104
+
105
+ // Filter DOM props
106
+ const domProps = () => filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
107
+
108
+ // Handle focus within
109
+ const { focusWithinProps } = createFocusWithin({
110
+ get onBlurWithin() { return getProps().onBlur; },
111
+ get onFocusWithin() { return getProps().onFocus; },
112
+ get onFocusWithinChange() { return getProps().onFocusChange; },
113
+ });
114
+
115
+ return {
116
+ get groupProps() {
117
+ return mergeProps(domProps(), {
118
+ role: 'group',
119
+ 'aria-disabled': state.isDisabled || undefined,
120
+ ...fieldProps,
121
+ ...focusWithinProps,
122
+ }) as JSX.HTMLAttributes<HTMLElement>;
123
+ },
124
+ get labelProps() {
125
+ return labelProps as JSX.HTMLAttributes<HTMLElement>;
126
+ },
127
+ get descriptionProps() {
128
+ return descriptionProps;
129
+ },
130
+ get errorMessageProps() {
131
+ return errorMessageProps;
132
+ },
133
+ get isInvalid() {
134
+ return isInvalid();
135
+ },
136
+ };
137
+ }