├── .assets
└── demo.gif
├── .eslintrc
├── .github
└── workflows
│ ├── main.yml
│ └── size.yml
├── .gitignore
├── LICENSE
├── README.md
├── example
├── .npmignore
├── index.css
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── package.json
├── src
├── forwardRefWithAs.tsx
└── index.tsx
├── styles.css
├── test
└── RadioGroup.test.tsx
├── tsconfig.json
└── yarn.lock
/.assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palmerhq/radio-group/fe79ea64a5cf8e213f0e48f8baa6b356cc5ca17d/.assets/demo.gif
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
6 |
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | node: ['10.x', '12.x', '14.x']
11 | os: [ubuntu-latest, windows-latest, macOS-latest]
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v2
16 |
17 | - name: Use Node ${{ matrix.node }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node }}
21 |
22 | - name: Install deps and build (with cache)
23 | uses: bahmutov/npm-install@v1
24 |
25 | - name: Lint
26 | run: yarn lint
27 |
28 | - name: Test
29 | run: yarn test --ci --coverage --maxWorkers=2
30 |
31 | - name: Build
32 | run: yarn build
33 |
--------------------------------------------------------------------------------
/.github/workflows/size.yml:
--------------------------------------------------------------------------------
1 | name: size
2 | on: [pull_request]
3 | jobs:
4 | size:
5 | runs-on: ubuntu-latest
6 | env:
7 | CI_JOB_NUMBER: 1
8 | steps:
9 | - uses: actions/checkout@v1
10 | - uses: andresz1/size-limit-action@v1
11 | with:
12 | github_token: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 The Palmer Group
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `@palmerhq/radio-group`
2 |
3 | An accessible [WAI-ARIA 1.1-compliant Radio Group](https://www.w3.org/TR/wai-aria-practices-1.1/#radiobutton) React component.
4 |
5 |
6 |
7 |
8 |
9 |
10 | - [`@palmerhq/radio-group`](#palmerhqradio-group)
11 | - [Installation](#installation)
12 | - [Usage](#usage)
13 | - [Usage with Formik v2](#usage-with-formik-v2)
14 | - [API Reference](#api-reference)
15 | - [``](#radiogroup-)
16 | - [`labelledBy?: string`](#labelledby-string)
17 | - [`onChange: (value: any) => void`](#onchange-value-any--void)
18 | - [`children: React.ComponentType[]`](#children-reactcomponenttyperadioprops)
19 | - [`value: any`](#value-any)
20 | - [`as?: React.ComponentType`](#as-reactcomponenttype)
21 | - [`autoFocus?: boolean`](#autofocus-boolean)
22 | - [``](#radio)
23 | - [`value: any`](#value-any-1)
24 | - [`onFocus?: () => void`](#onfocus---void)
25 | - [`onBlur?: () => void`](#onblur---void)
26 | - [`as?: React.ComponentType`](#as-reactcomponenttype-1)
27 | - [Underlying DOM Structure](#underlying-dom-structure)
28 | - [Overriding Styles](#overriding-styles)
29 | - [Accessibility Features](#accessibility-features)
30 | - [Authors](#authors)
31 |
32 |
33 |
34 | ## Installation
35 |
36 | ```
37 | yarn add @palmerhq/radio-group
38 | ```
39 |
40 | Or try it out in your browser on [CodeSandbox](https://codesandbox.io/embed/qxxnwzvy0w)
41 |
42 | > Note: This package uses `Array.prototype.findIndex`, so be sure that you have properly polyfilled.
43 |
44 | ## Usage
45 |
46 | ```tsx
47 | import * as React from 'react';
48 | import { RadioGroup, Radio } from '@palmerhq/radio-group';
49 | import '@palmerhq/radio-group/styles.css'; // use the default styles
50 |
51 | function App() {
52 | const [value, setValue] = React.useState();
53 |
54 | return (
55 | <>
56 |
Color
57 | setValue(value)}
61 | >
62 | Blue
63 | Red
64 | Green
65 |
66 | >
67 | );
68 | }
69 | ```
70 |
71 | ### Usage with Formik v2
72 |
73 | ```tsx
74 | import * as React from 'react';
75 | import { Formik, Form, useField } from 'formik';
76 | import { RadioGroup, Radio } from '@palmerhq/radio-group';
77 | import '@palmerhq/radio-group/styles.css'; // use the default styles
78 |
79 | function FRadioGroup(props) {
80 | const [{ onChange, onBlur, ...field }] = useField(props.name);
81 | return (
82 |
89 | );
90 | }
91 |
92 | function App() {
93 | return (
94 | {
100 | setTimeout(() => {
101 | alert(JSON.stringify(values, null, 2));
102 | setSubmitting(false);
103 | }, 500);
104 | }}
105 | >
106 |
114 |
115 | );
116 | }
117 | ```
118 |
119 | ## API Reference
120 |
121 | ### ``
122 |
123 | This renders a `div` and will pass through all props to the DOM element. It's children must be `` components.
124 |
125 | #### `labelledBy?: string`
126 |
127 | This should match the `id` you used to label the radio group.
128 |
129 | ```tsx
130 |
Color
131 |
132 | {/* ... */}
133 |
134 | ```
135 |
136 | #### `onChange: (value: any) => void`
137 |
138 | A callback function that will be fired with the `value` of the newly selected item.
139 |
140 | ```tsx
141 | import * as React from 'react';
142 | import { RadioGroup, Radio } from '@palmerhq/radio-group';
143 | import '@palmerhq/radio-group/styles.css'; // use the default styles
144 |
145 | function App() {
146 | const [value, setValue] = React.useState();
147 |
148 | return (
149 | <>
150 |
Color
151 | setValue(value)}
155 | >
156 | Blue
157 | Red
158 | Green
159 |
160 | >
161 | );
162 | }
163 | ```
164 |
165 | #### `children: React.ComponentType[]`
166 |
167 | **Required**
168 |
169 | The children of a `` can **ONLY** be `` components. In order to support compliant keyboard behavior, each sibling must know the value of the whole group and so `React.Children.map` is used internally.
170 |
171 | ```tsx
172 |
Color
173 |
174 | {/* ... */}
175 |
176 | ```
177 |
178 | #### `value: any`
179 |
180 | **Required**
181 |
182 | The current value of the radio group. This is shallowly compared to each `value` prop of the child `` components to determine which item is active.
183 |
184 | #### `as?: React.ComponentType`
185 |
186 | Component to use a the wrapper. Default is `
`.
187 |
188 | #### `autoFocus?: boolean`
189 |
190 | Whether to autoFocus the selected radio option.
191 |
192 | ### ``
193 |
194 | This renders a `div` with a data attribute `data-palmerhq-radio` and all the relevant perfect aria attributes. The React component will pass through all props to the DOM element.
195 |
196 | #### `value: any`
197 |
198 | **Required**
199 |
200 | The value of the radio button. This will be set / passed back to the `` when the item is selected.
201 |
202 | #### `onFocus?: () => void`
203 |
204 | Callback function for when the item is focused. When focused, a data attribute `data-palmerhq-radio-focus` is set to `"true"`. You can thus apply the selector to manage focus style like so:
205 |
206 | ```css
207 | [data-palmerhq-radio][data-palmerhq-radio-focus='true'] {
208 | background: blue;
209 | }
210 | ```
211 |
212 | #### `onBlur?: () => void`
213 |
214 | Callback function for when the item is blurred
215 |
216 | #### `as?: React.ComponentType`
217 |
218 | Component to use as radio. Default is `
`.
219 |
220 | ### Underlying DOM Structure
221 |
222 | For reference, the underlying HTML DOM structure are all `div`s and looks as follows.
223 |
224 | ```html
225 |
Moves focus to the checked radio button in the radiogroup.
374 |
If a radio button is not checked, focus moves to the first radio button in the group.
375 |
376 |
377 |
378 |
379 |
Space
380 |
381 |
382 |
If the radio button with focus is not checked, changes the state to checked.
383 |
Otherwise, does nothing.
384 |
Note: The state where a radio is not checked only occurs on page load.
385 |
386 |
387 |
388 |
389 |
Right arrow
390 |
391 |
392 |
Moves focus to and checks the next radio button in the group.
393 |
If focus is on the last radio button, moves focus to the first radio button.
394 |
The state of the previously checked radio button is changed to unchecked.
395 |
396 |
397 |
398 |
399 |
Down arrow
400 |
401 |
402 |
Moves focus to and checks the next radio button in the group.
403 |
If focus is on the last radio button, moves focus to the first radio button.
404 |
The state of the previously checked radio button is changed to unchecked.
405 |
406 |
407 |
408 |
409 |
Left arrow
410 |
411 |
412 |
Moves focus to and checks the previous radio button in the group.
413 |
If focus is on the first radio button, moves focus to and checks the last radio button.
414 |
The state of the previously checked radio button is changed to unchecked.
415 |
416 |
417 |
418 |
419 |
Up arrow
420 |
421 |
422 |
Moves focus to and checks the previous radio button in the group.
423 |
If focus is on the first radio button, moves focus to and checks the last radio button.
424 |
The state of the previously checked radio button is changed to unchecked.
425 |
426 |
427 |
428 |
429 |
430 |
431 |
Role, Property, State, and Tabindex Attributes
432 |
433 |
434 |
435 |
Role
436 |
Attributes
437 |
Element
438 |
Usage
439 |
440 |
441 |
442 |
443 |
radiogroup
444 |
445 |
div
446 |
447 |
448 |
Identifies the div element as a container for a group of radio buttons.
449 |
Is not focusable because focus is managed using a roving tabindex strategy as described below.
450 |
451 |
452 |
453 |
454 |
455 |
aria-labelledby="[IDREF]"
456 |
div
457 |
Refers to the element that contains the label of the radio group.
458 |
459 |
460 |
radio
461 |
462 |
div
463 |
464 |
465 |
Identifies the div element as an ARIA radio button.
466 |
The accessible name is computed from the child text content of the div element.
467 |
468 |
469 |
470 |
471 |
472 |
tabindex="-1"
473 |
div
474 |
475 |
476 |
Makes the element focusable but not part of the page Tab sequence.
477 |
Applied to all radio buttons contained in the radio group except for one that is included in the page Tab sequence.
478 |
This approach to managing focus is described in the section on roving tabindex.
479 |
480 |
481 |
482 |
483 |
484 |
tabindex="0"
485 |
div
486 |
487 |
488 |
Makes the radio button focusable and includes it in the page Tab sequence.
489 |
Set on only one radio in the radio group.
490 |
On page load, is set on the first radio button in the radio group.
491 |
Moves with focus inside the radio group so the most recently focused radio button is included in the page Tab sequence.
492 |
This approach to managing focus is described in the section on roving tabindex.
493 |
494 |
495 |
496 |
497 |
498 |
aria-checked="false"
499 |
div
500 |
501 |
502 |
Identifies radio buttons which are not checked.
503 |
CSS attribute selectors (e.g. [aria-checked="false"]) are used to synchronize the visual states with the value of the aria-checked attribute.
504 |
The CSS ::before pseudo-class is used to indicate visual state of unchecked radio buttons to support high contrast settings in operating systems and browsers.
505 |
506 |
507 |
508 |
509 |
510 |
aria-checked="true"
511 |
div
512 |
513 |
514 |
Identifies the radio button which is checked.
515 |
CSS attribute selectors (e.g. [aria-checked="true"]) are used to synchronize the visual states with the value of the aria-checked attribute.
516 |
The CSS ::before pseudo-class is used to indicate visual state of checked radio buttons to support high contrast settings in operating systems and browsers.