@praxiis/ui 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/package.json +11 -12
- package/src/README.md +688 -0
- package/src/components/CalendarStrip/CalendarStrip.a11y.ts +51 -0
- package/src/components/CalendarStrip/DayCard/DayCard.a11y.ts +52 -0
- package/src/components/EmptyState/EmptyState.a11y.ts +53 -0
- package/src/components/Header/Header.a11y.ts +82 -0
- package/src/components/ScheduleItem/ScheduleItem/ScheduleItem.a11y.ts +15 -0
- package/src/primitives/actions/Button/Button.a11y.ts +38 -0
- package/src/primitives/actions/IconButton/IconButton.a11y.ts +55 -0
- package/src/primitives/content/Avatar/Avatar.a11y.ts +50 -0
- package/src/primitives/content/Badge/Badge.a11y.ts +83 -0
- package/src/primitives/content/Card/Card.a11y.ts +60 -0
- package/src/primitives/content/Chip/Chip.a11y.ts +101 -0
- package/src/primitives/content/Icon/Icon.a11y.ts +43 -0
- package/src/primitives/feedback/ProgressBar/ProgressBar.a11y.ts +68 -0
- package/src/primitives/feedback/Skeleton/Skeleton.a11y.ts +46 -0
- package/src/primitives/feedback/Spinner/Spinner.a11y.ts +47 -0
- package/src/primitives/feedback/Toast/Toast.a11y.ts +75 -0
- package/src/primitives/inputs/Checkbox/Checkbox.a11y.ts +47 -0
- package/src/primitives/inputs/RadioButton/RadioButton.a11y.ts +48 -0
- package/src/primitives/inputs/SegmentedControl/SegmentedControl.a11y.ts +59 -0
- package/src/primitives/inputs/SelectSheet/SelectSheet.a11y.ts +117 -0
- package/src/primitives/inputs/Switch/Switch.a11y.ts +29 -0
- package/src/primitives/inputs/TextInput/TextInput.a11y.ts +77 -0
- package/src/primitives/layout/Divider/Divider.a11y.ts +55 -0
- package/src/primitives/overlays/Modal/Modal.a11y.ts +64 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the ProgressBar component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - 4.1.2 Name, Role, Value: Progress bars need proper role and value
|
|
8
|
+
* - The progressbar role requires min, max, and now values
|
|
9
|
+
*
|
|
10
|
+
* The progress bar uses "progressbar" role with explicit value range
|
|
11
|
+
* to provide determinate progress feedback to assistive technologies.
|
|
12
|
+
*
|
|
13
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/name-role-value
|
|
14
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { clampProgress, progressToPercent } from "./ProgressBar.helpers";
|
|
18
|
+
import {
|
|
19
|
+
ProgressBarA11yParams,
|
|
20
|
+
ProgressBarA11yProps,
|
|
21
|
+
} from "./ProgressBar.types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate accessibility props for ProgressBar.
|
|
25
|
+
*
|
|
26
|
+
* Creates a determinate progress indicator with:
|
|
27
|
+
* - accessibilityRole: "progressbar"
|
|
28
|
+
* - accessibilityValue: { min: 0, max: 100, now: current percentage }
|
|
29
|
+
* - accessibilityLabel: Description of what's in progress
|
|
30
|
+
*
|
|
31
|
+
* @param params - Accessibility parameters
|
|
32
|
+
* @returns Object to spread onto the ProgressBar component
|
|
33
|
+
*
|
|
34
|
+
* @warning Logs console warning in dev if accessibilityLabel missing
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const a11yProps = getProgressBarA11y({
|
|
38
|
+
* progress: 0.75,
|
|
39
|
+
* accessibilityLabel: "File upload"
|
|
40
|
+
* });
|
|
41
|
+
* // {
|
|
42
|
+
* // accessibilityRole: "progressbar",
|
|
43
|
+
* // accessibilityLabel: "File upload",
|
|
44
|
+
* // accessibilityValue: { min: 0, max: 100, now: 75 }
|
|
45
|
+
* // }
|
|
46
|
+
*/
|
|
47
|
+
export function getProgressBarA11y(
|
|
48
|
+
params: ProgressBarA11yParams
|
|
49
|
+
): ProgressBarA11yProps {
|
|
50
|
+
const { progress, accessibilityLabel, fallbackLabel } = params;
|
|
51
|
+
|
|
52
|
+
if (!accessibilityLabel && process.env.NODE_ENV !== "production") {
|
|
53
|
+
console.warn("[ProgressBar] Missing accessibilityLabel (WCAG 4.1.2)");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const clampedProgress = clampProgress(progress);
|
|
57
|
+
const progressPercent = progressToPercent(clampedProgress);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
accessibilityRole: "progressbar",
|
|
61
|
+
accessibilityLabel: accessibilityLabel || fallbackLabel,
|
|
62
|
+
accessibilityValue: {
|
|
63
|
+
min: 0,
|
|
64
|
+
max: 100,
|
|
65
|
+
now: progressPercent,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skeleton Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the Skeleton component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - 1.3.1 Info and Relationships: Decorative content should be hidden
|
|
8
|
+
* - 4.1.2 Name, Role, Value: Non-interactive elements need appropriate roles
|
|
9
|
+
*
|
|
10
|
+
* Skeletons are purely decorative loading placeholders and should be
|
|
11
|
+
* completely hidden from assistive technologies. The actual loading
|
|
12
|
+
* state should be communicated through other means (e.g., live regions,
|
|
13
|
+
* loading spinners with proper labels).
|
|
14
|
+
*
|
|
15
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { SkeletonA11yParams, SkeletonA11yProps } from "./Skeleton.types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate accessibility props for Skeleton.
|
|
22
|
+
*
|
|
23
|
+
* Skeletons are hidden from screen readers as they are decorative.
|
|
24
|
+
* Loading state should be communicated through semantic live regions
|
|
25
|
+
* or dedicated loading indicators with proper accessibility labels.
|
|
26
|
+
*
|
|
27
|
+
* @param _params - Accessibility parameters (currently unused, reserved for future)
|
|
28
|
+
* @returns Object to spread onto the Skeleton component
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const a11yProps = getSkeletonA11y({});
|
|
32
|
+
* // {
|
|
33
|
+
* // accessible: false,
|
|
34
|
+
* // accessibilityRole: "none",
|
|
35
|
+
* // accessibilityElementsHidden: true,
|
|
36
|
+
* // importantForAccessibility: "no-hide-descendants"
|
|
37
|
+
* // }
|
|
38
|
+
*/
|
|
39
|
+
export function getSkeletonA11y(_params: SkeletonA11yParams = {}): SkeletonA11yProps {
|
|
40
|
+
return {
|
|
41
|
+
accessible: false,
|
|
42
|
+
accessibilityRole: "none",
|
|
43
|
+
accessibilityElementsHidden: true,
|
|
44
|
+
importantForAccessibility: "no-hide-descendants",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spinner Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the Spinner component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - 4.1.2 Name, Role, Value: Spinners need proper role and label
|
|
8
|
+
* - 2.2.2 Pause, Stop, Hide: Users should be informed of loading state
|
|
9
|
+
*
|
|
10
|
+
* The spinner uses "progressbar" role with busy state to indicate
|
|
11
|
+
* that content is loading and the interface may change.
|
|
12
|
+
*
|
|
13
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/name-role-value
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { SpinnerA11yParams, SpinnerA11yProps } from "./Spinner.types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate accessibility props for Spinner.
|
|
20
|
+
*
|
|
21
|
+
* Spinners are announced as indeterminate progress indicators.
|
|
22
|
+
* The busy state informs assistive technology that the UI is updating.
|
|
23
|
+
*
|
|
24
|
+
* @param params - Accessibility parameters
|
|
25
|
+
* @returns Object to spread onto the Spinner component
|
|
26
|
+
*
|
|
27
|
+
* @warning Logs console warning in dev if accessibilityLabel missing
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const a11yProps = getSpinnerA11y({ accessibilityLabel: "Loading messages" });
|
|
31
|
+
* // { accessibilityRole: "progressbar", accessibilityLabel: "Loading messages", accessibilityState: { busy: true } }
|
|
32
|
+
*/
|
|
33
|
+
export function getSpinnerA11y(
|
|
34
|
+
params: SpinnerA11yParams
|
|
35
|
+
): SpinnerA11yProps {
|
|
36
|
+
const { accessibilityLabel, fallbackLabel } = params;
|
|
37
|
+
|
|
38
|
+
if (!accessibilityLabel && process.env.NODE_ENV !== "production") {
|
|
39
|
+
console.warn("[Spinner] Missing accessibilityLabel (WCAG 4.1.2)");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
accessibilityLabel: accessibilityLabel || fallbackLabel,
|
|
44
|
+
accessibilityRole: "progressbar",
|
|
45
|
+
accessibilityState: { busy: true },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toast Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the Toast component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - 4.1.2 Name, Role, Value: Toasts need proper role and label
|
|
8
|
+
* - 4.1.3 Status Messages: Use aria-live regions for toast announcements
|
|
9
|
+
*
|
|
10
|
+
* The toast uses "alert" role with appropriate live region urgency:
|
|
11
|
+
* - Error: assertive (immediate announcement)
|
|
12
|
+
* - Success/Warning/Info: polite (waits for current speech to complete)
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/status-messages
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { ToastA11yParams, ToastA11yProps, ToastVariant } from "./Toast.types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determines live region urgency based on variant.
|
|
21
|
+
*
|
|
22
|
+
* @param variant - Toast variant
|
|
23
|
+
* @returns "assertive" for errors, "polite" for others
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
function getLiveRegionUrgency(
|
|
27
|
+
variant: ToastVariant
|
|
28
|
+
): "polite" | "assertive" {
|
|
29
|
+
// Errors should interrupt immediately for critical feedback
|
|
30
|
+
return variant === "error" ? "assertive" : "polite";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate accessibility props for Toast.
|
|
35
|
+
*
|
|
36
|
+
* Toasts are announced as alerts with appropriate urgency.
|
|
37
|
+
* Error toasts interrupt immediately; others wait politely.
|
|
38
|
+
*
|
|
39
|
+
* @param params - Accessibility parameters
|
|
40
|
+
* @returns Object to spread onto the Toast component
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const a11yProps = getToastA11y({
|
|
44
|
+
* variant: "success",
|
|
45
|
+
* message: "Changes saved",
|
|
46
|
+
* });
|
|
47
|
+
* // {
|
|
48
|
+
* // accessibilityRole: "alert",
|
|
49
|
+
* // accessibilityLabel: "Success: Changes saved",
|
|
50
|
+
* // accessibilityLiveRegion: "polite"
|
|
51
|
+
* // }
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* const a11yProps = getToastA11y({
|
|
55
|
+
* variant: "error",
|
|
56
|
+
* message: "Failed to save",
|
|
57
|
+
* });
|
|
58
|
+
* // {
|
|
59
|
+
* // accessibilityRole: "alert",
|
|
60
|
+
* // accessibilityLabel: "Error: Failed to save",
|
|
61
|
+
* // accessibilityLiveRegion: "assertive"
|
|
62
|
+
* // }
|
|
63
|
+
*/
|
|
64
|
+
export function getToastA11y(params: ToastA11yParams): ToastA11yProps {
|
|
65
|
+
const { variant, accessibilityLabel, message, variantPrefixes } = params;
|
|
66
|
+
|
|
67
|
+
// Build default label with translated variant prefix for context
|
|
68
|
+
const defaultLabel = `${variantPrefixes[variant]}: ${message}`;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
accessibilityLabel: accessibilityLabel ?? defaultLabel,
|
|
72
|
+
accessibilityRole: "alert",
|
|
73
|
+
accessibilityLiveRegion: getLiveRegionUrgency(variant),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkbox Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the Checkbox component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - 4.1.2 Name, Role, Value: Checkboxes need proper role and label
|
|
8
|
+
* - 1.3.1 Info and Relationships: State must be programmatically determinable
|
|
9
|
+
*
|
|
10
|
+
* The checkbox uses "checkbox" role with checked state to indicate
|
|
11
|
+
* the current selection.
|
|
12
|
+
*
|
|
13
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/name-role-value
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { CheckboxA11yParams, CheckboxA11yProps } from "./Checkbox.types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate accessibility props for Checkbox.
|
|
20
|
+
*
|
|
21
|
+
* @param params - Accessibility parameters
|
|
22
|
+
* @returns Object to spread onto the Checkbox component
|
|
23
|
+
*
|
|
24
|
+
* @warning Logs console warning in dev if both label and accessibilityLabel missing
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const a11yProps = getCheckboxA11y({
|
|
28
|
+
* label: "Accept terms",
|
|
29
|
+
* checked: true,
|
|
30
|
+
* disabled: false,
|
|
31
|
+
* });
|
|
32
|
+
* // { accessibilityRole: "checkbox", accessibilityLabel: "Accept terms", accessibilityState: { checked: true, disabled: false } }
|
|
33
|
+
*/
|
|
34
|
+
export function getCheckboxA11y(params: CheckboxA11yParams): CheckboxA11yProps {
|
|
35
|
+
const { label, accessibilityLabel, accessibilityHint, checked, disabled } = params;
|
|
36
|
+
|
|
37
|
+
if (!accessibilityLabel && !label && process.env.NODE_ENV !== "production") {
|
|
38
|
+
console.warn("[Checkbox] Missing label or accessibilityLabel (WCAG 4.1.2)");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
accessibilityLabel: accessibilityLabel || label || "Checkbox",
|
|
43
|
+
accessibilityHint,
|
|
44
|
+
accessibilityRole: "checkbox",
|
|
45
|
+
accessibilityState: { checked, disabled },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RadioButton Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the RadioButton component.
|
|
5
|
+
* Follows WCAG 2.1 guidelines for radio button accessibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RadioButtonA11yParams, RadioButtonA11yProps } from "./RadioButton.types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate accessibility props for RadioButton.
|
|
12
|
+
*
|
|
13
|
+
* Provides proper semantic information for screen readers:
|
|
14
|
+
* - Role: "radio" for proper radio button semantics
|
|
15
|
+
* - State: checked and disabled states
|
|
16
|
+
* - Label: Uses provided label or accessibility label
|
|
17
|
+
*
|
|
18
|
+
* @param params - Accessibility configuration
|
|
19
|
+
* @returns Accessibility props object
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const a11yProps = getRadioButtonA11y({
|
|
23
|
+
* label: "Option A",
|
|
24
|
+
* checked: true,
|
|
25
|
+
* disabled: false,
|
|
26
|
+
* });
|
|
27
|
+
* // Returns: { accessibilityRole: "radio", accessibilityState: { checked: true, disabled: false }, ... }
|
|
28
|
+
*/
|
|
29
|
+
export function getRadioButtonA11y(params: RadioButtonA11yParams): RadioButtonA11yProps {
|
|
30
|
+
const {
|
|
31
|
+
label,
|
|
32
|
+
accessibilityLabel,
|
|
33
|
+
accessibilityHint,
|
|
34
|
+
checked,
|
|
35
|
+
disabled,
|
|
36
|
+
fallbackLabel,
|
|
37
|
+
} = params;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
accessibilityLabel: accessibilityLabel || label || fallbackLabel,
|
|
41
|
+
accessibilityHint,
|
|
42
|
+
accessibilityRole: "radio",
|
|
43
|
+
accessibilityState: {
|
|
44
|
+
checked,
|
|
45
|
+
disabled,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SegmentOption } from "./SegmentedControl.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Accessibility props for the SegmentedControl container
|
|
5
|
+
*/
|
|
6
|
+
export function getSegmentedControlContainerA11y(
|
|
7
|
+
optionsCount: number,
|
|
8
|
+
i18n: { containerLabel: string; optionsLabel: string },
|
|
9
|
+
): {
|
|
10
|
+
accessible: boolean;
|
|
11
|
+
accessibilityRole: "radiogroup";
|
|
12
|
+
accessibilityLabel: string;
|
|
13
|
+
} {
|
|
14
|
+
return {
|
|
15
|
+
accessible: true,
|
|
16
|
+
accessibilityRole: "radiogroup",
|
|
17
|
+
accessibilityLabel: `${i18n.containerLabel} ${optionsCount} ${i18n.optionsLabel}`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Accessibility props for individual segments
|
|
23
|
+
*/
|
|
24
|
+
export function getSegmentA11y(
|
|
25
|
+
option: SegmentOption,
|
|
26
|
+
index: number,
|
|
27
|
+
totalOptions: number,
|
|
28
|
+
isSelected: boolean,
|
|
29
|
+
i18n: {
|
|
30
|
+
selectedLabel: string;
|
|
31
|
+
currentlySelectedHint: string;
|
|
32
|
+
doubleTapToSelectHint: string;
|
|
33
|
+
ofLabel: string;
|
|
34
|
+
},
|
|
35
|
+
): {
|
|
36
|
+
accessible: boolean;
|
|
37
|
+
accessibilityRole: "radio";
|
|
38
|
+
accessibilityState: {
|
|
39
|
+
selected: boolean;
|
|
40
|
+
disabled: boolean;
|
|
41
|
+
};
|
|
42
|
+
accessibilityLabel: string;
|
|
43
|
+
accessibilityHint: string;
|
|
44
|
+
} {
|
|
45
|
+
const positionText = `${index + 1} ${i18n.ofLabel} ${totalOptions}`;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
accessible: true,
|
|
49
|
+
accessibilityRole: "radio",
|
|
50
|
+
accessibilityState: {
|
|
51
|
+
selected: isSelected,
|
|
52
|
+
disabled: option.disabled ?? false,
|
|
53
|
+
},
|
|
54
|
+
accessibilityLabel: `${option.label}, ${positionText}${isSelected ? `, ${i18n.selectedLabel}` : ""}`,
|
|
55
|
+
accessibilityHint: isSelected
|
|
56
|
+
? i18n.currentlySelectedHint
|
|
57
|
+
: `${i18n.doubleTapToSelectHint} ${option.label}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SelectSheet Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Accessibility utilities for the SelectSheet component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AccessibilityProps } from "react-native";
|
|
8
|
+
|
|
9
|
+
import { SelectSheetMode, SelectSheetPosition } from "./SelectSheet.types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get accessibility props for the sheet container
|
|
13
|
+
*/
|
|
14
|
+
export function getSheetA11yProps(params: {
|
|
15
|
+
title?: string;
|
|
16
|
+
position: SelectSheetPosition;
|
|
17
|
+
accessibilityLabel?: string;
|
|
18
|
+
positionLabels: { bottom: string; top: string; center: string };
|
|
19
|
+
fallbackTitle: string;
|
|
20
|
+
}): AccessibilityProps {
|
|
21
|
+
const { title, position, accessibilityLabel, positionLabels, fallbackTitle } = params;
|
|
22
|
+
|
|
23
|
+
const positionLabel = positionLabels[position];
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
accessible: true,
|
|
27
|
+
accessibilityRole: "none",
|
|
28
|
+
accessibilityLabel:
|
|
29
|
+
accessibilityLabel || `${title || fallbackTitle} ${positionLabel}`,
|
|
30
|
+
accessibilityViewIsModal: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get accessibility props for an option row
|
|
36
|
+
*/
|
|
37
|
+
export function getOptionA11yProps(params: {
|
|
38
|
+
label: string;
|
|
39
|
+
isSelected: boolean;
|
|
40
|
+
isDisabled: boolean;
|
|
41
|
+
isMultiple: boolean;
|
|
42
|
+
index: number;
|
|
43
|
+
total: number;
|
|
44
|
+
selectedLabel: string;
|
|
45
|
+
disabledLabel: string;
|
|
46
|
+
ofLabel: string;
|
|
47
|
+
}): AccessibilityProps {
|
|
48
|
+
const { label, isSelected, isDisabled, isMultiple, index, total, selectedLabel, disabledLabel, ofLabel } = params;
|
|
49
|
+
|
|
50
|
+
const role = isMultiple ? "checkbox" : "radio";
|
|
51
|
+
const selectedText = isSelected ? `, ${selectedLabel}` : "";
|
|
52
|
+
const disabledText = isDisabled ? `, ${disabledLabel}` : "";
|
|
53
|
+
const positionText = `${index + 1} ${ofLabel} ${total}`;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
accessible: true,
|
|
57
|
+
accessibilityRole: role,
|
|
58
|
+
accessibilityLabel: `${label}${selectedText}${disabledText}, ${positionText}`,
|
|
59
|
+
accessibilityState: {
|
|
60
|
+
checked: isSelected,
|
|
61
|
+
disabled: isDisabled,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get accessibility props for the done button
|
|
68
|
+
*/
|
|
69
|
+
export function getDoneButtonA11yProps(params: {
|
|
70
|
+
selectedCount: number;
|
|
71
|
+
mode: SelectSheetMode;
|
|
72
|
+
doneLabel: string;
|
|
73
|
+
itemsSelectedLabel: string;
|
|
74
|
+
doneHint: string;
|
|
75
|
+
}): AccessibilityProps {
|
|
76
|
+
const { selectedCount, mode, doneLabel, itemsSelectedLabel, doneHint } = params;
|
|
77
|
+
|
|
78
|
+
const countText =
|
|
79
|
+
mode === "multiple" ? ` with ${selectedCount} ${itemsSelectedLabel}` : "";
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
accessible: true,
|
|
83
|
+
accessibilityRole: "button",
|
|
84
|
+
accessibilityLabel: `${doneLabel}${countText}`,
|
|
85
|
+
accessibilityHint: doneHint,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get accessibility props for the close button
|
|
91
|
+
*/
|
|
92
|
+
export function getCloseButtonA11yProps(params: {
|
|
93
|
+
closeLabel: string;
|
|
94
|
+
closeHint: string;
|
|
95
|
+
}): AccessibilityProps {
|
|
96
|
+
return {
|
|
97
|
+
accessible: true,
|
|
98
|
+
accessibilityRole: "button",
|
|
99
|
+
accessibilityLabel: params.closeLabel,
|
|
100
|
+
accessibilityHint: params.closeHint,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get announcement for selection change
|
|
106
|
+
*/
|
|
107
|
+
export function getSelectionAnnouncement(
|
|
108
|
+
label: string,
|
|
109
|
+
isSelected: boolean,
|
|
110
|
+
isMultiple: boolean,
|
|
111
|
+
i18n: { selectedLabel: string; deselectedLabel: string },
|
|
112
|
+
): string {
|
|
113
|
+
if (isMultiple) {
|
|
114
|
+
return isSelected ? `${label} ${i18n.selectedLabel}` : `${label} ${i18n.deselectedLabel}`;
|
|
115
|
+
}
|
|
116
|
+
return `${label} ${i18n.selectedLabel}`;
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SwitchA11yParams, SwitchA11yProps } from "./Switch.types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get accessibility props for the Switch component.
|
|
5
|
+
*
|
|
6
|
+
* Implements WCAG 4.1.2 (Name, Role, Value) requirements:
|
|
7
|
+
* - accessibilityRole: "switch" for proper identification
|
|
8
|
+
* - accessibilityLabel: Required for screen readers (warns if missing)
|
|
9
|
+
* - accessibilityHint: Optional additional context
|
|
10
|
+
* - accessibilityState: Communicates checked and disabled states
|
|
11
|
+
*
|
|
12
|
+
* @param params - Accessibility parameters
|
|
13
|
+
* @returns Accessibility props for the switch
|
|
14
|
+
*/
|
|
15
|
+
export function getSwitchA11y(params: SwitchA11yParams): SwitchA11yProps {
|
|
16
|
+
const { label, accessibilityLabel, accessibilityHint, value, disabled } =
|
|
17
|
+
params;
|
|
18
|
+
|
|
19
|
+
if (!accessibilityLabel && !label && process.env.NODE_ENV !== "production") {
|
|
20
|
+
console.warn("[Switch] Missing label or accessibilityLabel (WCAG 4.1.2)");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
accessibilityLabel: accessibilityLabel || label || "Switch",
|
|
25
|
+
accessibilityHint,
|
|
26
|
+
accessibilityRole: "switch",
|
|
27
|
+
accessibilityState: { checked: value, disabled },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextInput Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the TextInput component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - 4.1.2 Name, Role, Value: Inputs need proper labels
|
|
8
|
+
* - 3.3.1 Error Identification: Errors announced via hint
|
|
9
|
+
* - 3.3.2 Labels or Instructions: Required fields indicated
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { TextInputA11yParams, TextInputA11yProps } from "./TextInput.types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate accessibility props for TextInput.
|
|
18
|
+
*
|
|
19
|
+
* Implements WCAG requirements:
|
|
20
|
+
* - accessibilityLabel: Uses label prop as fallback, adds "required" suffix
|
|
21
|
+
* - accessibilityHint: Error message takes precedence over custom hint
|
|
22
|
+
* - accessibilityState: Communicates disabled state
|
|
23
|
+
*
|
|
24
|
+
* @param params - Accessibility parameters
|
|
25
|
+
* @returns Accessibility props for the text input
|
|
26
|
+
*
|
|
27
|
+
* @warning Logs console warning in dev if both label and accessibilityLabel missing
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const a11yProps = getTextInputA11y({
|
|
31
|
+
* label: "Email",
|
|
32
|
+
* required: true,
|
|
33
|
+
* disabled: false,
|
|
34
|
+
* error: "Invalid email",
|
|
35
|
+
* });
|
|
36
|
+
* // { accessibilityLabel: "Email, required", accessibilityHint: "Error: Invalid email", accessibilityState: { disabled: false } }
|
|
37
|
+
*/
|
|
38
|
+
export function getTextInputA11y(
|
|
39
|
+
params: TextInputA11yParams
|
|
40
|
+
): TextInputA11yProps {
|
|
41
|
+
const {
|
|
42
|
+
label,
|
|
43
|
+
accessibilityLabel,
|
|
44
|
+
accessibilityHint,
|
|
45
|
+
disabled,
|
|
46
|
+
error,
|
|
47
|
+
required,
|
|
48
|
+
fallbackLabel,
|
|
49
|
+
requiredLabel,
|
|
50
|
+
errorPrefixLabel,
|
|
51
|
+
} = params;
|
|
52
|
+
|
|
53
|
+
if (!accessibilityLabel && !label && process.env.NODE_ENV !== "production") {
|
|
54
|
+
console.warn(
|
|
55
|
+
"[TextInput] Missing label or accessibilityLabel (WCAG 4.1.2)"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const baseLabel = accessibilityLabel || label || fallbackLabel;
|
|
60
|
+
const fullLabel = required ? `${baseLabel}, ${requiredLabel}` : baseLabel;
|
|
61
|
+
|
|
62
|
+
// Error takes precedence, then custom hint
|
|
63
|
+
let finalHint: string | undefined;
|
|
64
|
+
if (error) {
|
|
65
|
+
finalHint = `${errorPrefixLabel}: ${error}`;
|
|
66
|
+
} else if (accessibilityHint) {
|
|
67
|
+
finalHint = accessibilityHint;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
accessibilityLabel: fullLabel,
|
|
72
|
+
accessibilityHint: finalHint,
|
|
73
|
+
accessibilityState: {
|
|
74
|
+
disabled,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Divider Accessibility Helpers
|
|
3
|
+
*
|
|
4
|
+
* Generates accessibility props for the Divider component.
|
|
5
|
+
*
|
|
6
|
+
* WCAG References:
|
|
7
|
+
* - Decorative elements should be hidden from assistive technologies
|
|
8
|
+
* - When a divider has semantic meaning (with label), it should be announced
|
|
9
|
+
*
|
|
10
|
+
* The divider uses "none" role by default as it's purely decorative.
|
|
11
|
+
* When a label is present, it's announced via accessibilityLabel.
|
|
12
|
+
*
|
|
13
|
+
* @see https://www.w3.org/WAI/WCAG21/Understanding/name-role-value
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { DividerA11yParams, DividerA11yProps } from "./Divider.types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate accessibility props for Divider.
|
|
20
|
+
*
|
|
21
|
+
* Dividers are typically decorative, so they use role="none" and
|
|
22
|
+
* are not accessible by default. When a label is present, the
|
|
23
|
+
* divider becomes accessible with the label as its description.
|
|
24
|
+
*
|
|
25
|
+
* @param params - Accessibility parameters
|
|
26
|
+
* @returns Object to spread onto the Divider component
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Decorative divider (hidden from AT)
|
|
30
|
+
* const a11yProps = getDividerA11y({ orientation: "horizontal" });
|
|
31
|
+
* // { accessibilityRole: "none", accessible: false }
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Divider with label (announced by AT)
|
|
35
|
+
* const a11yProps = getDividerA11y({
|
|
36
|
+
* orientation: "horizontal",
|
|
37
|
+
* label: "OR",
|
|
38
|
+
* accessibilityLabel: "Or"
|
|
39
|
+
* });
|
|
40
|
+
* // { accessibilityRole: "none", accessible: true, accessibilityLabel: "Or" }
|
|
41
|
+
*/
|
|
42
|
+
export function getDividerA11y(params: DividerA11yParams): DividerA11yProps {
|
|
43
|
+
const { label, accessibilityLabel } = params;
|
|
44
|
+
|
|
45
|
+
// If there's a label or explicit a11y label, make it accessible
|
|
46
|
+
const hasSemanticMeaning = Boolean(label || accessibilityLabel);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
accessibilityRole: "none",
|
|
50
|
+
accessible: hasSemanticMeaning,
|
|
51
|
+
...(hasSemanticMeaning && {
|
|
52
|
+
accessibilityLabel: accessibilityLabel || label,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|