@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Press interactions
|
|
2
|
+
export { createPress, type CreatePressProps, type PressResult } from './createPress';
|
|
3
|
+
export { PressEvent, type IPressEvent, type PointerType, type PressEventType } from './PressEvent';
|
|
4
|
+
|
|
5
|
+
// Focus interactions
|
|
6
|
+
export { createFocus, type CreateFocusProps, type FocusResult, type FocusEvents } from './createFocus';
|
|
7
|
+
export {
|
|
8
|
+
createFocusWithin,
|
|
9
|
+
type FocusWithinProps,
|
|
10
|
+
type FocusWithinResult,
|
|
11
|
+
} from './createFocusWithin';
|
|
12
|
+
export {
|
|
13
|
+
createFocusable,
|
|
14
|
+
FocusableContext,
|
|
15
|
+
type CreateFocusableProps,
|
|
16
|
+
type FocusableResult,
|
|
17
|
+
type FocusableContextValue,
|
|
18
|
+
type FocusableProviderProps,
|
|
19
|
+
type FocusableProps,
|
|
20
|
+
type FocusableDOMProps,
|
|
21
|
+
} from './createFocusable';
|
|
22
|
+
export { FocusableProvider } from './FocusableProvider';
|
|
23
|
+
export {
|
|
24
|
+
createFocusRing,
|
|
25
|
+
type FocusRingProps,
|
|
26
|
+
type FocusRingResult,
|
|
27
|
+
} from './createFocusRing';
|
|
28
|
+
|
|
29
|
+
// Hover interactions
|
|
30
|
+
export {
|
|
31
|
+
createHover,
|
|
32
|
+
type CreateHoverProps,
|
|
33
|
+
type HoverResult,
|
|
34
|
+
type HoverEvent,
|
|
35
|
+
type HoverEvents,
|
|
36
|
+
} from './createHover';
|
|
37
|
+
|
|
38
|
+
// Keyboard interactions
|
|
39
|
+
export {
|
|
40
|
+
createKeyboard,
|
|
41
|
+
type CreateKeyboardProps,
|
|
42
|
+
type KeyboardResult,
|
|
43
|
+
type KeyboardEvents,
|
|
44
|
+
type KeyboardEvent,
|
|
45
|
+
} from './createKeyboard';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the accessibility implementation for input fields.
|
|
5
|
+
* Fields accept user input, gain context from their label, and may display
|
|
6
|
+
* a description or error message.
|
|
7
|
+
*
|
|
8
|
+
* This is a 1:1 port of @react-aria/label's useField hook.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { JSX } from 'solid-js';
|
|
12
|
+
import { createId } from '../ssr';
|
|
13
|
+
import { createLabel, type LabelAriaProps, type LabelAria, type AriaLabelingProps, type DOMProps } from './createLabel';
|
|
14
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
15
|
+
import { type MaybeAccessor, access } from '../utils/reactivity';
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// TYPES
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export interface HelpTextProps {
|
|
22
|
+
/** A description for the field. Provides a hint such as specific requirements for what to choose. */
|
|
23
|
+
description?: JSX.Element;
|
|
24
|
+
/** An error message for the field. */
|
|
25
|
+
errorMessage?: JSX.Element | ((validation: ValidationResult) => JSX.Element);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ValidationResult {
|
|
29
|
+
/** Whether the input value is invalid. */
|
|
30
|
+
isInvalid: boolean;
|
|
31
|
+
/** The current error messages for the input if it is invalid, otherwise an empty array. */
|
|
32
|
+
validationErrors: string[];
|
|
33
|
+
/** The native validity state for the input. */
|
|
34
|
+
validationDetails: ValidityState;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Validation<T> {
|
|
38
|
+
/** Whether the input value is invalid. */
|
|
39
|
+
isInvalid?: boolean;
|
|
40
|
+
/** Whether the input is required before form submission. */
|
|
41
|
+
isRequired?: boolean;
|
|
42
|
+
/** A function that returns an error message if a given value is invalid. */
|
|
43
|
+
validate?: (value: T) => string | string[] | true | null | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AriaFieldProps extends LabelAriaProps, HelpTextProps, Omit<Validation<any>, 'isRequired'> {}
|
|
47
|
+
|
|
48
|
+
export interface FieldAria extends LabelAria {
|
|
49
|
+
/** Props for the description element, if any. */
|
|
50
|
+
descriptionProps: JSX.HTMLAttributes<HTMLElement>;
|
|
51
|
+
/** Props for the error message element, if any. */
|
|
52
|
+
errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// IMPLEMENTATION
|
|
57
|
+
// ============================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Provides the accessibility implementation for input fields.
|
|
61
|
+
* Fields accept user input, gain context from their label, and may display
|
|
62
|
+
* a description or error message.
|
|
63
|
+
*
|
|
64
|
+
* @param props - Props for the Field.
|
|
65
|
+
*/
|
|
66
|
+
export function createField(props: MaybeAccessor<AriaFieldProps>): FieldAria {
|
|
67
|
+
const getProps = () => access(props);
|
|
68
|
+
|
|
69
|
+
const { labelProps, fieldProps: baseLabelFieldProps } = createLabel(props);
|
|
70
|
+
|
|
71
|
+
// Generate IDs for description and error message
|
|
72
|
+
const descriptionId = createId();
|
|
73
|
+
const errorMessageId = createId();
|
|
74
|
+
|
|
75
|
+
const getDescriptionProps = (): FieldAria['descriptionProps'] => {
|
|
76
|
+
const { description, errorMessage, isInvalid } = getProps();
|
|
77
|
+
|
|
78
|
+
// Only include ID if description exists or there's an error message that might be shown
|
|
79
|
+
if (!description && !errorMessage && !isInvalid) {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id: descriptionId,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getErrorMessageProps = (): FieldAria['errorMessageProps'] => {
|
|
89
|
+
const { errorMessage, isInvalid } = getProps();
|
|
90
|
+
|
|
91
|
+
// Only include ID if there's an error message and the field is invalid
|
|
92
|
+
if (!errorMessage && !isInvalid) {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: errorMessageId,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getFieldProps = (): AriaLabelingProps & DOMProps => {
|
|
102
|
+
const { description, errorMessage, isInvalid } = getProps();
|
|
103
|
+
|
|
104
|
+
const describedByIds: string[] = [];
|
|
105
|
+
|
|
106
|
+
// Add description ID if description exists
|
|
107
|
+
if (description) {
|
|
108
|
+
describedByIds.push(descriptionId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add error message ID if field is invalid and error message exists
|
|
112
|
+
// Use aria-describedby for error message because aria-errormessage is unsupported
|
|
113
|
+
// using VoiceOver or NVDA. See https://github.com/adobe/react-spectrum/issues/1346#issuecomment-740136268
|
|
114
|
+
if (isInvalid && errorMessage) {
|
|
115
|
+
describedByIds.push(errorMessageId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add any existing aria-describedby from props
|
|
119
|
+
const existingDescribedBy = getProps()['aria-describedby'];
|
|
120
|
+
if (existingDescribedBy) {
|
|
121
|
+
describedByIds.push(existingDescribedBy);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ariaDescribedBy = describedByIds.length > 0 ? describedByIds.join(' ') : undefined;
|
|
125
|
+
|
|
126
|
+
return mergeProps(baseLabelFieldProps, {
|
|
127
|
+
'aria-describedby': ariaDescribedBy,
|
|
128
|
+
}) as AriaLabelingProps & DOMProps;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
get labelProps() {
|
|
133
|
+
return labelProps;
|
|
134
|
+
},
|
|
135
|
+
get fieldProps() {
|
|
136
|
+
return getFieldProps();
|
|
137
|
+
},
|
|
138
|
+
get descriptionProps() {
|
|
139
|
+
return getDescriptionProps();
|
|
140
|
+
},
|
|
141
|
+
get errorMessageProps() {
|
|
142
|
+
return getErrorMessageProps();
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the accessibility implementation for labels and their associated elements.
|
|
5
|
+
* Labels provide context for user inputs.
|
|
6
|
+
*
|
|
7
|
+
* This is a 1:1 port of @react-aria/label's useLabel hook.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { JSX } from 'solid-js';
|
|
11
|
+
import { createId } from '../ssr';
|
|
12
|
+
import { createLabels } from './createLabels';
|
|
13
|
+
import { type MaybeAccessor, access } from '../utils/reactivity';
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
export interface AriaLabelingProps {
|
|
20
|
+
/** Defines a string value that labels the current element. */
|
|
21
|
+
'aria-label'?: string;
|
|
22
|
+
/** Identifies the element (or elements) that labels the current element. */
|
|
23
|
+
'aria-labelledby'?: string;
|
|
24
|
+
/** Identifies the element (or elements) that describes the object. */
|
|
25
|
+
'aria-describedby'?: string;
|
|
26
|
+
/** Identifies the element (or elements) that provide a detailed, extended description for the object. */
|
|
27
|
+
'aria-details'?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LabelableProps {
|
|
31
|
+
/** The content to display as the label. */
|
|
32
|
+
label?: JSX.Element;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DOMProps {
|
|
36
|
+
/** The element's unique identifier. */
|
|
37
|
+
id?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LabelAriaProps extends LabelableProps, DOMProps, AriaLabelingProps {
|
|
41
|
+
/**
|
|
42
|
+
* The HTML element used to render the label, e.g. 'label', or 'span'.
|
|
43
|
+
* @default 'label'
|
|
44
|
+
*/
|
|
45
|
+
labelElementType?: 'label' | 'span' | 'div';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface LabelAria {
|
|
49
|
+
/** Props to apply to the label container element. */
|
|
50
|
+
labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement> | JSX.HTMLAttributes<HTMLSpanElement>;
|
|
51
|
+
/** Props to apply to the field container element being labeled. */
|
|
52
|
+
fieldProps: AriaLabelingProps & DOMProps;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================
|
|
56
|
+
// IMPLEMENTATION
|
|
57
|
+
// ============================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Provides the accessibility implementation for labels and their associated elements.
|
|
61
|
+
* Labels provide context for user inputs.
|
|
62
|
+
*
|
|
63
|
+
* @param props - The props for labels and fields.
|
|
64
|
+
*/
|
|
65
|
+
export function createLabel(props: MaybeAccessor<LabelAriaProps>): LabelAria {
|
|
66
|
+
const getProps = () => access(props);
|
|
67
|
+
|
|
68
|
+
const id = createId(getProps().id);
|
|
69
|
+
const labelId = createId();
|
|
70
|
+
|
|
71
|
+
const getLabelProps = (): LabelAria['labelProps'] => {
|
|
72
|
+
const { label, labelElementType = 'label' } = getProps();
|
|
73
|
+
|
|
74
|
+
if (!label) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id: labelId,
|
|
80
|
+
...(labelElementType === 'label' ? { for: id } : {}),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const getFieldProps = (): LabelAria['fieldProps'] => {
|
|
85
|
+
const {
|
|
86
|
+
label,
|
|
87
|
+
'aria-labelledby': ariaLabelledby,
|
|
88
|
+
'aria-label': ariaLabel,
|
|
89
|
+
} = getProps();
|
|
90
|
+
|
|
91
|
+
let labelledBy = ariaLabelledby;
|
|
92
|
+
|
|
93
|
+
if (label) {
|
|
94
|
+
labelledBy = ariaLabelledby ? `${labelId} ${ariaLabelledby}` : labelId;
|
|
95
|
+
} else if (!ariaLabelledby && !ariaLabel && process.env.NODE_ENV !== 'production') {
|
|
96
|
+
console.warn(
|
|
97
|
+
'If you do not provide a visible label, you must specify an aria-label or aria-labelledby attribute for accessibility'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return createLabels({
|
|
102
|
+
id,
|
|
103
|
+
'aria-label': ariaLabel,
|
|
104
|
+
'aria-labelledby': labelledBy,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
get labelProps() {
|
|
110
|
+
return getLabelProps();
|
|
111
|
+
},
|
|
112
|
+
get fieldProps() {
|
|
113
|
+
return getFieldProps();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Labels utility for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Merges aria-label and aria-labelledby into aria-labelledby when both exist.
|
|
5
|
+
*
|
|
6
|
+
* This is a 1:1 port of @react-aria/utils's useLabels hook.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createId } from '../ssr';
|
|
10
|
+
import type { AriaLabelingProps, DOMProps } from './createLabel';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Merges aria-label and aria-labelledby into aria-labelledby when both exist.
|
|
14
|
+
*
|
|
15
|
+
* @param props - Aria label props.
|
|
16
|
+
* @param defaultLabel - Default value for aria-label when not present.
|
|
17
|
+
*/
|
|
18
|
+
export function createLabels(
|
|
19
|
+
props: DOMProps & AriaLabelingProps,
|
|
20
|
+
defaultLabel?: string
|
|
21
|
+
): DOMProps & AriaLabelingProps {
|
|
22
|
+
let {
|
|
23
|
+
id,
|
|
24
|
+
'aria-label': label,
|
|
25
|
+
'aria-labelledby': labelledBy,
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
// Generate an ID if not provided
|
|
29
|
+
id = createId(id);
|
|
30
|
+
|
|
31
|
+
// If there is both an aria-label and aria-labelledby,
|
|
32
|
+
// combine them by pointing to the element itself.
|
|
33
|
+
if (labelledBy && label) {
|
|
34
|
+
const ids = new Set([id, ...labelledBy.trim().split(/\s+/)]);
|
|
35
|
+
labelledBy = [...ids].join(' ');
|
|
36
|
+
} else if (labelledBy) {
|
|
37
|
+
labelledBy = labelledBy.trim().split(/\s+/).join(' ');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If no labels are provided, use the default
|
|
41
|
+
if (!label && !labelledBy && defaultLabel) {
|
|
42
|
+
label = defaultLabel;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
'aria-label': label,
|
|
48
|
+
'aria-labelledby': labelledBy,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { createLabel } from './createLabel';
|
|
2
|
+
export type {
|
|
3
|
+
LabelAriaProps,
|
|
4
|
+
LabelAria,
|
|
5
|
+
AriaLabelingProps,
|
|
6
|
+
LabelableProps,
|
|
7
|
+
DOMProps,
|
|
8
|
+
} from './createLabel';
|
|
9
|
+
|
|
10
|
+
export { createField } from './createField';
|
|
11
|
+
export type {
|
|
12
|
+
AriaFieldProps,
|
|
13
|
+
FieldAria,
|
|
14
|
+
HelpTextProps,
|
|
15
|
+
ValidationResult,
|
|
16
|
+
Validation,
|
|
17
|
+
} from './createField';
|
|
18
|
+
|
|
19
|
+
export { createLabels } from './createLabels';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the behavior and accessibility implementation for a link component.
|
|
5
|
+
* A link allows a user to navigate to another page or resource within a web page
|
|
6
|
+
* or application.
|
|
7
|
+
*
|
|
8
|
+
* This is a 1:1 port of @react-aria/link's useLink hook.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { type Accessor } from 'solid-js';
|
|
12
|
+
import { createPress } from '../interactions/createPress';
|
|
13
|
+
import { createFocusable } from '../interactions/createFocusable';
|
|
14
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
15
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
16
|
+
import { type MaybeAccessor, access } from '../utils/reactivity';
|
|
17
|
+
import { type PressEvent } from '../interactions/PressEvent';
|
|
18
|
+
|
|
19
|
+
// ============================================
|
|
20
|
+
// TYPES
|
|
21
|
+
// ============================================
|
|
22
|
+
|
|
23
|
+
export interface AriaLinkProps {
|
|
24
|
+
/** Whether the link is disabled. */
|
|
25
|
+
isDisabled?: boolean;
|
|
26
|
+
/** The HTML element used to render the link, e.g. 'a', or 'span'. @default 'a' */
|
|
27
|
+
elementType?: string;
|
|
28
|
+
/** The URL to link to. */
|
|
29
|
+
href?: string;
|
|
30
|
+
/** The target window for the link. */
|
|
31
|
+
target?: string;
|
|
32
|
+
/** The relationship between the linked resource and the current page. */
|
|
33
|
+
rel?: string;
|
|
34
|
+
/** Handler that is called when the press is released over the target. */
|
|
35
|
+
onPress?: (e: PressEvent) => void;
|
|
36
|
+
/** Handler that is called when a press interaction starts. */
|
|
37
|
+
onPressStart?: (e: PressEvent) => void;
|
|
38
|
+
/** Handler that is called when a press interaction ends. */
|
|
39
|
+
onPressEnd?: (e: PressEvent) => void;
|
|
40
|
+
/** Handler that is called when the element is clicked. */
|
|
41
|
+
onClick?: (e: MouseEvent) => void;
|
|
42
|
+
/** Handler that is called when the element receives focus. */
|
|
43
|
+
onFocus?: (e: FocusEvent) => void;
|
|
44
|
+
/** Handler that is called when the element loses focus. */
|
|
45
|
+
onBlur?: (e: FocusEvent) => void;
|
|
46
|
+
/** Handler that is called when the element's focus status changes. */
|
|
47
|
+
onFocusChange?: (isFocused: boolean) => void;
|
|
48
|
+
/** Handler that is called when a key is pressed. */
|
|
49
|
+
onKeyDown?: (e: KeyboardEvent) => void;
|
|
50
|
+
/** Handler that is called when a key is released. */
|
|
51
|
+
onKeyUp?: (e: KeyboardEvent) => void;
|
|
52
|
+
/** Whether to autofocus the element. */
|
|
53
|
+
autoFocus?: boolean;
|
|
54
|
+
/** Indicates the current "page" or state within a set of related elements. */
|
|
55
|
+
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean;
|
|
56
|
+
/** Defines a string value that labels the current element. */
|
|
57
|
+
'aria-label'?: string;
|
|
58
|
+
/** Identifies the element (or elements) that labels the current element. */
|
|
59
|
+
'aria-labelledby'?: string;
|
|
60
|
+
/** Identifies the element (or elements) that describes the object. */
|
|
61
|
+
'aria-describedby'?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LinkAria {
|
|
65
|
+
/** Props for the link element. */
|
|
66
|
+
linkProps: Record<string, unknown>;
|
|
67
|
+
/** Whether the link is currently pressed. */
|
|
68
|
+
isPressed: Accessor<boolean>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================
|
|
72
|
+
// IMPLEMENTATION
|
|
73
|
+
// ============================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Provides the behavior and accessibility implementation for a link component.
|
|
77
|
+
* A link allows a user to navigate to another page or resource within a web page
|
|
78
|
+
* or application.
|
|
79
|
+
*/
|
|
80
|
+
export function createLink(
|
|
81
|
+
props: MaybeAccessor<AriaLinkProps> = {}
|
|
82
|
+
): LinkAria {
|
|
83
|
+
const getProps = () => access(props);
|
|
84
|
+
|
|
85
|
+
const isDisabled = () => getProps().isDisabled ?? false;
|
|
86
|
+
const elementType = () => getProps().elementType ?? 'a';
|
|
87
|
+
|
|
88
|
+
// Create press handling
|
|
89
|
+
const { pressProps, isPressed } = createPress({
|
|
90
|
+
get isDisabled() { return isDisabled(); },
|
|
91
|
+
get onPress() { return getProps().onPress; },
|
|
92
|
+
get onPressStart() { return getProps().onPressStart; },
|
|
93
|
+
get onPressEnd() { return getProps().onPressEnd; },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Create focusable handling
|
|
97
|
+
const { focusableProps } = createFocusable({
|
|
98
|
+
get isDisabled() { return isDisabled(); },
|
|
99
|
+
get autoFocus() { return getProps().autoFocus; },
|
|
100
|
+
get onFocus() { return getProps().onFocus; },
|
|
101
|
+
get onBlur() { return getProps().onBlur; },
|
|
102
|
+
get onFocusChange() { return getProps().onFocusChange; },
|
|
103
|
+
get onKeyDown() { return getProps().onKeyDown; },
|
|
104
|
+
get onKeyUp() { return getProps().onKeyUp; },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Build link props
|
|
108
|
+
const getLinkProps = (): Record<string, unknown> => {
|
|
109
|
+
const p = getProps();
|
|
110
|
+
const elType = elementType();
|
|
111
|
+
const disabled = isDisabled();
|
|
112
|
+
|
|
113
|
+
let baseProps: Record<string, unknown> = {};
|
|
114
|
+
|
|
115
|
+
// If not an <a>, add role and tabIndex
|
|
116
|
+
if (elType !== 'a') {
|
|
117
|
+
baseProps = {
|
|
118
|
+
role: 'link',
|
|
119
|
+
tabIndex: disabled ? undefined : 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add link-specific props
|
|
124
|
+
if (elType === 'a') {
|
|
125
|
+
if (p.href) baseProps.href = p.href;
|
|
126
|
+
if (p.target) baseProps.target = p.target;
|
|
127
|
+
if (p.rel) baseProps.rel = p.rel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ARIA attributes
|
|
131
|
+
const ariaProps: Record<string, unknown> = {
|
|
132
|
+
'aria-disabled': disabled || undefined,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (p['aria-current'] !== undefined) {
|
|
136
|
+
ariaProps['aria-current'] = p['aria-current'];
|
|
137
|
+
}
|
|
138
|
+
if (p['aria-label']) {
|
|
139
|
+
ariaProps['aria-label'] = p['aria-label'];
|
|
140
|
+
}
|
|
141
|
+
if (p['aria-labelledby']) {
|
|
142
|
+
ariaProps['aria-labelledby'] = p['aria-labelledby'];
|
|
143
|
+
}
|
|
144
|
+
if (p['aria-describedby']) {
|
|
145
|
+
ariaProps['aria-describedby'] = p['aria-describedby'];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle onClick - only call user's onClick when not disabled
|
|
149
|
+
const onClick = (e: MouseEvent) => {
|
|
150
|
+
// If disabled, prevent navigation and don't call user's onClick
|
|
151
|
+
if (disabled) {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Call user's onClick if provided
|
|
157
|
+
p.onClick?.(e);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return mergeProps(
|
|
161
|
+
filterDOMProps(p as Record<string, unknown>, { labelable: true }),
|
|
162
|
+
baseProps,
|
|
163
|
+
ariaProps,
|
|
164
|
+
focusableProps as Record<string, unknown>,
|
|
165
|
+
pressProps as Record<string, unknown>,
|
|
166
|
+
{ onClick }
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
get linkProps() {
|
|
172
|
+
return getLinkProps();
|
|
173
|
+
},
|
|
174
|
+
isPressed,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createLink, type AriaLinkProps, type LinkAria } from './createLink';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar hook for Solidaria
|
|
3
|
+
*
|
|
4
|
+
* Provides the accessibility implementation for a progress bar component.
|
|
5
|
+
* Progress bars show either determinate or indeterminate progress of an operation
|
|
6
|
+
* over time.
|
|
7
|
+
*
|
|
8
|
+
* This is a 1:1 port of @react-aria/progress's useProgressBar hook.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createLabel } from '../label/createLabel';
|
|
12
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
13
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
14
|
+
import { type MaybeAccessor, access } from '../utils/reactivity';
|
|
15
|
+
|
|
16
|
+
// ============================================
|
|
17
|
+
// TYPES
|
|
18
|
+
// ============================================
|
|
19
|
+
|
|
20
|
+
export interface AriaProgressBarProps {
|
|
21
|
+
/** The current value (controlled). */
|
|
22
|
+
value?: number;
|
|
23
|
+
/** The smallest value allowed for the input. @default 0 */
|
|
24
|
+
minValue?: number;
|
|
25
|
+
/** The largest value allowed for the input. @default 100 */
|
|
26
|
+
maxValue?: number;
|
|
27
|
+
/** The content to display as the value's label (e.g. 1 of 4). */
|
|
28
|
+
valueLabel?: string;
|
|
29
|
+
/** Whether presentation is indeterminate when progress isn't known. */
|
|
30
|
+
isIndeterminate?: boolean;
|
|
31
|
+
/** The display format of the value label. */
|
|
32
|
+
formatOptions?: Intl.NumberFormatOptions;
|
|
33
|
+
/** The content to display as the label. */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** An accessibility label for this item. */
|
|
36
|
+
'aria-label'?: string;
|
|
37
|
+
/** Identifies the element (or elements) that labels the current element. */
|
|
38
|
+
'aria-labelledby'?: string;
|
|
39
|
+
/** Identifies the element (or elements) that describes the object. */
|
|
40
|
+
'aria-describedby'?: string;
|
|
41
|
+
/** Identifies the element (or elements) that provide a detailed, extended description for the object. */
|
|
42
|
+
'aria-details'?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProgressBarAria {
|
|
46
|
+
/** Props for the progress bar container element. */
|
|
47
|
+
progressBarProps: Record<string, unknown>;
|
|
48
|
+
/** Props for the progress bar's visual label element (if any). */
|
|
49
|
+
labelProps: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// UTILITIES
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
function clamp(value: number, min: number, max: number): number {
|
|
57
|
+
return Math.min(Math.max(value, min), max);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================
|
|
61
|
+
// IMPLEMENTATION
|
|
62
|
+
// ============================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Provides the accessibility implementation for a progress bar component.
|
|
66
|
+
* Progress bars show either determinate or indeterminate progress of an operation
|
|
67
|
+
* over time.
|
|
68
|
+
*/
|
|
69
|
+
export function createProgressBar(
|
|
70
|
+
props: MaybeAccessor<AriaProgressBarProps> = {}
|
|
71
|
+
): ProgressBarAria {
|
|
72
|
+
const getProps = () => access(props);
|
|
73
|
+
|
|
74
|
+
// Create label handling
|
|
75
|
+
const { labelProps, fieldProps } = createLabel({
|
|
76
|
+
get label() { return getProps().label; },
|
|
77
|
+
get 'aria-label'() { return getProps()['aria-label']; },
|
|
78
|
+
get 'aria-labelledby'() { return getProps()['aria-labelledby']; },
|
|
79
|
+
// Progress bar is not an HTML input element so it
|
|
80
|
+
// shouldn't be labeled by a <label> element.
|
|
81
|
+
labelElementType: 'span',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Build progress bar props
|
|
85
|
+
const getProgressBarProps = (): Record<string, unknown> => {
|
|
86
|
+
const p = getProps();
|
|
87
|
+
const value = p.value ?? 0;
|
|
88
|
+
const minValue = p.minValue ?? 0;
|
|
89
|
+
const maxValue = p.maxValue ?? 100;
|
|
90
|
+
const isIndeterminate = p.isIndeterminate ?? false;
|
|
91
|
+
const formatOptions = p.formatOptions ?? { style: 'percent' as const };
|
|
92
|
+
|
|
93
|
+
const clampedValue = clamp(value, minValue, maxValue);
|
|
94
|
+
const percentage = (clampedValue - minValue) / (maxValue - minValue);
|
|
95
|
+
|
|
96
|
+
// Format value label
|
|
97
|
+
let valueLabel = p.valueLabel;
|
|
98
|
+
if (!isIndeterminate && !valueLabel) {
|
|
99
|
+
const valueToFormat = formatOptions.style === 'percent' ? percentage : clampedValue;
|
|
100
|
+
try {
|
|
101
|
+
const formatter = new Intl.NumberFormat(undefined, formatOptions);
|
|
102
|
+
valueLabel = formatter.format(valueToFormat);
|
|
103
|
+
} catch {
|
|
104
|
+
// Fallback if formatting fails
|
|
105
|
+
valueLabel = `${Math.round(percentage * 100)}%`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const domProps = filterDOMProps(p as Record<string, unknown>, { labelable: true });
|
|
110
|
+
|
|
111
|
+
return mergeProps(domProps, fieldProps as Record<string, unknown>, {
|
|
112
|
+
'aria-valuenow': isIndeterminate ? undefined : clampedValue,
|
|
113
|
+
'aria-valuemin': minValue,
|
|
114
|
+
'aria-valuemax': maxValue,
|
|
115
|
+
'aria-valuetext': isIndeterminate ? undefined : valueLabel,
|
|
116
|
+
role: 'progressbar',
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
get progressBarProps() {
|
|
122
|
+
return getProgressBarProps();
|
|
123
|
+
},
|
|
124
|
+
get labelProps() {
|
|
125
|
+
return labelProps as Record<string, unknown>;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|