add a11y plugin example#2095
Conversation
size-limit report 📦
|
SlicedSilver
left a comment
There was a problem hiding this comment.
Thanks, this looks really good and extends beyond what was previously mentioned in the tutorial.
| :::info Ready-made plugin | ||
|
|
||
| If you would rather not wire these techniques up by hand, the | ||
| [Accessibility plugin](https://github.com/tradingview/lightweight-charts/tree/master/plugin-examples/src/plugins/accessibility) | ||
| in the plugin examples packages the keyboard navigation and ARIA layer described | ||
| here into a chart-level helper built on pane primitives: | ||
|
|
||
| ```js | ||
| addAccessibilityPlugin(chart, { chartTitle: 'My chart' }); | ||
| ``` | ||
|
|
||
| Keyboard navigation always covers the whole series (the arrow keys page the | ||
| chart as needed). The plugin defaults to `dataScope: 'visible'`, which scopes the | ||
| spoken *summaries* to the current viewport – the recommended mode for charts with | ||
| large data sets. Use `dataScope: 'all'` when you want those summaries to describe | ||
| the full history. | ||
|
|
||
| It is also fully translatable: numbers and dates follow the chart's | ||
| `localization.locale`, and every announced string can be overridden through a | ||
| `messages` bundle (with a `lang` attribute so screen readers use the right | ||
| voice). See the plugin's README and its Spanish example. | ||
|
|
||
| ::: | ||
|
|
There was a problem hiding this comment.
I would suggest that we also mention the plugin on the first page of the A11y tutorial. Most people would be happy to see that and skip the tutorial by just using a plugin instead.
There was a problem hiding this comment.
added a section at the first page
| import { generateLineData } from '../../../sample-data'; | ||
| import { addAccessibilityPlugin } from '../accessibility'; | ||
|
|
||
| const chart = ((window as unknown as any).chart = createChart('chart', { |
There was a problem hiding this comment.
I think we can remove = ((window as unknown as any).chart = from the examples
| } else { | ||
| // Business-day string, e.g. '2019-05-15'. | ||
| return time; | ||
| } |
There was a problem hiding this comment.
defaultTimeFormatter returns business-day string times verbatim (unlocalised) while timestamp/BusinessDay-object times get toLocaleDateString. Edge case, probably acceptable, but slightly inconsistent localisation.
| case 'PageUp': | ||
| event.preventDefault(); | ||
| this._movePoint(this._options.pageStep); | ||
| break; | ||
| case 'PageDown': | ||
| event.preventDefault(); | ||
| this._movePoint(-this._options.pageStep); | ||
| break; |
There was a problem hiding this comment.
PageUp moves +pageStep (forward/right in time), PageDown moves backward. This is internally consistent with ARIA slider semantics (PageUp increases the value), so I wouldn't call it a bug — but on a left-to-right time axis some users expect PageDown to advance. The README just says "jump ten points" without direction. A one-line note on direction in the keyboard table would remove the ambiguity.
| case 'ArrowUp': | ||
| event.preventDefault(); | ||
| this._moveSeries(-1); | ||
| break; | ||
| case 'ArrowDown': | ||
| event.preventDefault(); | ||
| this._moveSeries(1); | ||
| break; |
There was a problem hiding this comment.
If you have multiple series (and they don't have all the same timestamps, such as a moving average line not having the first 10 points) then the up / down keys won't select the same points (time) on the different series.
This also means that we don't always scroll the selected point into view when switching series because we assumed that they would have the same index on the time scale but this is incorrect.
There was a problem hiding this comment.
nice catch, fixed that one and changed moving average to be more realistic in the example
| help: ({ multiSeries }): string => | ||
| `Keyboard controls. Left and right arrows move between data points. ${multiSeries ? 'Up and down arrows switch between series. ' : ''}Page Up and Page Down jump ten points. Home and End jump to the first and last points. Enter or Space reads a summary of the series.`, | ||
| point: ({ position, total, time, label, values }): string => | ||
| `Point ${position} of ${total}. ${time}, ${label} ${values}.`, |
There was a problem hiding this comment.
When moving to different points (up and down arrows), I think we should mention the price first, then the date, and then the 'point 447 of 500' at the end. Mentioning the point 447... at the start doesn't make sense to me because it is the least important piece of information.
- announce dates in UTC to match the time axis - use pageStep in the keyboard help text - treat a viewport scrolled past the data as an empty scope - announce the newest bar as Latest, not the last visible one - hide the focus ring when all series are removed - re-announce identical messages from the standalone polite region - re-neutralise focusables and canvases the library re-creates - resolve the host pane dynamically when pane indices shift - drive init retries via requestUpdate - read series data lazily instead of caching snapshots
Summary
Adds a drop-in Accessibility plugin under
plugin-examples/It attaches one labelled, focusable primitive per pane, so multi-pane / multi-series charts work out of the box. detach() restores the DOM to exactly how it was found.
What it does
Notes & tradeoffs
role="application"is used so arrow keys drive data navigation under screen readers; this intentionally suspends the SR virtual cursor while focused on the chart (press Tab to move on — it is not a focus trap). Documented in the README.