Skip to content

Commit 0a867bd

Browse files
authored
Accessibility Labels (#227)
* Accessibility labels * Removed AndroidTV example * Prettier changes * Changed onUtterText to undefined default, added type for init options
1 parent 6c42656 commit 0a867bd

7 files changed

Lines changed: 428 additions & 43 deletions

File tree

.changeset/accessibility-labels.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@noriginmedia/norigin-spatial-navigation-core': minor
3+
'@noriginmedia/norigin-spatial-navigation-react': minor
4+
'@noriginmedia/norigin-spatial-navigation': minor
5+
---
6+
7+
Add accessibility labels on focusable components.
8+
9+
Introduces a new optional `onUtterText` callback on `init()` and an `accessibilityLabel` prop on `useFocusable()` (and the underlying `addFocusable` / `updateFocusable` payloads). When focus lands on a focusable component, the library concatenates the labels of all newly-entered parent regions with the leaf node's own label and passes the resulting string to `onUtterText`. Parent region labels are only included when focus enters a region for the first time (similar to how `aria-label` on `role="region"` behaves), so subsequent focus moves within the same parent only utter the leaf label.
10+
11+
This library does not implement Text-To-Speech itself — it only provides a unified way to declare accessibility labels and wire the callback to the platform's TTS engine, which is particularly useful for cross-platform TV apps where native `aria-*` support is fragmented.

apps/react-demo/src/App.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import logo from './logo.png';
2424
init({
2525
debug: false,
2626
visualDebug: false,
27-
distanceCalculationMethod: 'center'
27+
distanceCalculationMethod: 'center',
28+
onUtterText: (text: string) => {
29+
// eslint-disable-next-line no-console
30+
console.log('onUtterText', text);
31+
}
2832
});
2933

3034
const rows = shuffle([
@@ -233,6 +237,7 @@ function Asset({
233237
index
234238
}: AssetProps) {
235239
const { ref, focused } = useFocusable<object, HTMLDivElement>({
240+
accessibilityLabel: title,
236241
onEnterPress,
237242
onFocus,
238243
extraProps: {
@@ -298,6 +303,7 @@ function ContentRow({
298303
isShuffleSize
299304
}: ContentRowProps) {
300305
const { ref, focusKey } = useFocusable<object, HTMLDivElement>({
306+
accessibilityLabel: rowTitle,
301307
onFocus
302308
});
303309

docs/api-reference/SpatialNavigation.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,27 @@ init(config?: {
5353
isVerticalDirection: boolean,
5454
distanceCalculationMethod: string
5555
) => number;
56+
onUtterText?: (text: string) => void;
5657
}): void
5758
```
5859

5960
### Config options
6061

61-
| Option | Type | Default | Description |
62-
| ----------------------------------- | ---------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
63-
| `debug` | `boolean` | `false` | Log navigation decisions to the browser console. |
64-
| `visualDebug` | `boolean` | `false` | Draw a canvas overlay showing component bounding boxes and navigation paths. |
65-
| `nativeMode` | `boolean` | `false` | **Deprecated.** Disable DOM key event listeners (for React Native). You must drive navigation manually. |
66-
| `throttle` | `number` | `0` | Milliseconds to wait between processing repeated key presses. `0` means no throttle. |
67-
| `throttleKeypresses` | `boolean` | `false` | When `true` and `throttle > 0`, throttle key repeat events while a key is held down. |
68-
| `useGetBoundingClientRect` | `boolean` | `false` | Use `getBoundingClientRect()` instead of `offsetLeft/Top` for layout measurement. Use this when elements are CSS-transformed or scaled. |
69-
| `shouldFocusDOMNode` | `boolean` | `false` | Call `HTMLElement.focus()` on the focused component's DOM node, enabling native browser focus behavior and accessibility. |
70-
| `domNodeFocusOptions` | `FocusOptions` | `undefined` | Options passed to `HTMLElement.focus()` when `shouldFocusDOMNode` is `true`. |
71-
| `shouldUseNativeEvents` | `boolean` | `false` | Do not call `preventDefault()` on key events, allowing the browser to handle them natively as well. |
72-
| `rtl` | `boolean` | `false` | Enable right-to-left layout mode. Left and right navigation directions are swapped. |
73-
| `distanceCalculationMethod` | `'center' \| 'edges' \| 'corners'` | `'corners'` | Algorithm used to calculate distance between components. See [Distance Calculation](../guides/distance-calculation.md). |
74-
| `customDistanceCalculationFunction` | `function` | `undefined` | Override the secondary-axis distance calculation. See [Distance Calculation](../guides/distance-calculation.md). |
62+
| Option | Type | Default | Description |
63+
| ----------------------------------- | ---------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
64+
| `debug` | `boolean` | `false` | Log navigation decisions to the browser console. |
65+
| `visualDebug` | `boolean` | `false` | Draw a canvas overlay showing component bounding boxes and navigation paths. |
66+
| `nativeMode` | `boolean` | `false` | **Deprecated.** Disable DOM key event listeners (for React Native). You must drive navigation manually. |
67+
| `throttle` | `number` | `0` | Milliseconds to wait between processing repeated key presses. `0` means no throttle. |
68+
| `throttleKeypresses` | `boolean` | `false` | When `true` and `throttle > 0`, throttle key repeat events while a key is held down. |
69+
| `useGetBoundingClientRect` | `boolean` | `false` | Use `getBoundingClientRect()` instead of `offsetLeft/Top` for layout measurement. Use this when elements are CSS-transformed or scaled. |
70+
| `shouldFocusDOMNode` | `boolean` | `false` | Call `HTMLElement.focus()` on the focused component's DOM node, enabling native browser focus behavior and accessibility. |
71+
| `domNodeFocusOptions` | `FocusOptions` | `undefined` | Options passed to `HTMLElement.focus()` when `shouldFocusDOMNode` is `true`. |
72+
| `shouldUseNativeEvents` | `boolean` | `false` | Do not call `preventDefault()` on key events, allowing the browser to handle them natively as well. |
73+
| `rtl` | `boolean` | `false` | Enable right-to-left layout mode. Left and right navigation directions are swapped. |
74+
| `distanceCalculationMethod` | `'center' \| 'edges' \| 'corners'` | `'corners'` | Algorithm used to calculate distance between components. See [Distance Calculation](../guides/distance-calculation.md). |
75+
| `customDistanceCalculationFunction` | `function` | `undefined` | Override the secondary-axis distance calculation. See [Distance Calculation](../guides/distance-calculation.md). |
76+
| `onUtterText` | `(text: string) => void` | `undefined` | Global callback invoked with a concatenated accessibility label string whenever focus changes. Wire this to your platform's Text-To-Speech engine. See the [Accessibility Labels](../guides/accessibility-labels.md) guide. |
7577

7678
### Example
7779

@@ -83,7 +85,11 @@ init({
8385
visualDebug: false,
8486
distanceCalculationMethod: 'center',
8587
throttle: 150,
86-
throttleKeypresses: true
88+
throttleKeypresses: true,
89+
onUtterText: (text) => {
90+
// Hand the string off to the platform's Text-To-Speech engine
91+
platformTTS.speak(text);
92+
}
8793
});
8894
```
8995

0 commit comments

Comments
 (0)