@proyecto-viviana/solidaria 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/button/createButton.ts +135 -0
- package/src/button/createToggleButton.ts +101 -0
- package/src/button/index.ts +4 -0
- package/src/button/types.ts +67 -0
- package/src/checkbox/createCheckbox.ts +135 -0
- package/src/checkbox/createCheckboxGroup.ts +137 -0
- package/src/checkbox/createCheckboxGroupItem.ts +117 -0
- package/src/checkbox/createCheckboxGroupState.ts +193 -0
- package/src/checkbox/index.ts +13 -0
- package/src/index.ts +128 -0
- package/src/interactions/FocusableProvider.tsx +44 -0
- package/src/interactions/PressEvent.ts +112 -0
- package/src/interactions/createFocus.ts +157 -0
- package/src/interactions/createFocusRing.ts +142 -0
- package/src/interactions/createFocusWithin.ts +141 -0
- package/src/interactions/createFocusable.ts +168 -0
- package/src/interactions/createHover.ts +214 -0
- package/src/interactions/createKeyboard.ts +82 -0
- package/src/interactions/createPress.ts +758 -0
- package/src/interactions/index.ts +45 -0
- package/src/label/createField.ts +145 -0
- package/src/label/createLabel.ts +116 -0
- package/src/label/createLabels.ts +50 -0
- package/src/label/index.ts +19 -0
- package/src/link/createLink.ts +176 -0
- package/src/link/index.ts +1 -0
- package/src/progress/createProgressBar.ts +128 -0
- package/src/progress/index.ts +5 -0
- package/src/radio/createRadio.ts +286 -0
- package/src/radio/createRadioGroup.ts +189 -0
- package/src/radio/createRadioGroupState.ts +201 -0
- package/src/radio/index.ts +23 -0
- package/src/separator/createSeparator.ts +82 -0
- package/src/separator/index.ts +6 -0
- package/src/ssr/index.ts +36 -0
- package/src/switch/createSwitch.ts +70 -0
- package/src/switch/index.ts +1 -0
- package/src/textfield/createTextField.ts +198 -0
- package/src/textfield/index.ts +5 -0
- package/src/toggle/createToggle.ts +222 -0
- package/src/toggle/createToggleState.ts +94 -0
- package/src/toggle/index.ts +7 -0
- package/src/utils/dom.ts +244 -0
- package/src/utils/events.ts +119 -0
- package/src/utils/filterDOMProps.ts +116 -0
- package/src/utils/focus.ts +151 -0
- package/src/utils/geometry.ts +115 -0
- package/src/utils/globalListeners.ts +142 -0
- package/src/utils/index.ts +66 -0
- package/src/utils/mergeProps.ts +49 -0
- package/src/utils/platform.ts +52 -0
- package/src/utils/reactivity.ts +36 -0
- package/src/utils/textSelection.ts +114 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proyecto-viviana/solidaria",
|
|
3
|
-
"version": "0.0.
|
|
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": "./
|
|
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,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
|
+
}
|