@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,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
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ createProgressBar,
3
+ type AriaProgressBarProps,
4
+ type ProgressBarAria,
5
+ } from './createProgressBar';