Skip to content

Commit f6239e3

Browse files
authored
Add docs for the Group component (#5060)
1 parent 6f24731 commit f6239e3

File tree

6 files changed

+301
-12
lines changed

6 files changed

+301
-12
lines changed

packages/@react-aria/datepicker/src/useDatePicker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {AriaDialogProps} from '@react-types/dialog';
1616
import {CalendarProps} from '@react-types/calendar';
1717
import {createFocusManager} from '@react-aria/focus';
1818
import {DatePickerState} from '@react-stately/datepicker';
19-
import {DOMAttributes, KeyboardEvent} from '@react-types/shared';
19+
import {DOMAttributes, GroupDOMAttributes, KeyboardEvent} from '@react-types/shared';
2020
import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/utils';
2121
// @ts-ignore
2222
import intlMessages from '../intl/*.json';
@@ -31,7 +31,7 @@ export interface DatePickerAria {
3131
/** Props for the date picker's visible label element, if any. */
3232
labelProps: DOMAttributes,
3333
/** Props for the grouping element containing the date field and button. */
34-
groupProps: DOMAttributes,
34+
groupProps: GroupDOMAttributes,
3535
/** Props for the date field. */
3636
fieldProps: AriaDatePickerProps<DateValue>,
3737
/** Props for the popover trigger button. */
@@ -83,7 +83,7 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
8383

8484
return {
8585
groupProps: mergeProps(domProps, groupProps, fieldProps, descProps, focusWithinProps, {
86-
role: 'group',
86+
role: 'group' as const,
8787
'aria-disabled': props.isDisabled || null,
8888
'aria-labelledby': labelledBy,
8989
'aria-describedby': ariaDescribedBy,

packages/@react-aria/datepicker/src/useDateRangePicker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {AriaDatePickerProps, AriaDateRangePickerProps, DateValue} from '@react-t
1515
import {AriaDialogProps} from '@react-types/dialog';
1616
import {createFocusManager} from '@react-aria/focus';
1717
import {DateRangePickerState} from '@react-stately/datepicker';
18-
import {DOMAttributes, KeyboardEvent} from '@react-types/shared';
18+
import {DOMAttributes, GroupDOMAttributes, KeyboardEvent} from '@react-types/shared';
1919
import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/utils';
2020
import {focusManagerSymbol, roleSymbol} from './useDateField';
2121
// @ts-ignore
@@ -31,7 +31,7 @@ export interface DateRangePickerAria {
3131
/** Props for the date range picker's visible label element, if any. */
3232
labelProps: DOMAttributes,
3333
/** Props for the grouping element containing the date fields and button. */
34-
groupProps: DOMAttributes,
34+
groupProps: GroupDOMAttributes,
3535
/** Props for the start date field. */
3636
startFieldProps: AriaDatePickerProps<DateValue>,
3737
/** Props for the end date field. */
@@ -117,7 +117,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
117117

118118
return {
119119
groupProps: mergeProps(domProps, groupProps, fieldProps, descProps, focusWithinProps, {
120-
role: 'group',
120+
role: 'group' as const,
121121
'aria-disabled': props.isDisabled || null,
122122
'aria-describedby': ariaDescribedBy,
123123
onKeyDown(e: KeyboardEvent) {

packages/@react-aria/numberfield/src/useNumberField.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {AriaButtonProps} from '@react-types/button';
1414
import {AriaNumberFieldProps} from '@react-types/numberfield';
15-
import {DOMAttributes, TextInputDOMProps} from '@react-types/shared';
15+
import {DOMAttributes, GroupDOMAttributes, TextInputDOMProps} from '@react-types/shared';
1616
import {filterDOMProps, isAndroid, isIOS, isIPhone, mergeProps, useFormReset, useId} from '@react-aria/utils';
1717
import {
1818
InputHTMLAttributes,
@@ -37,7 +37,7 @@ export interface NumberFieldAria {
3737
/** Props for the label element. */
3838
labelProps: LabelHTMLAttributes<HTMLLabelElement>,
3939
/** Props for the group wrapper around the input and stepper buttons. */
40-
groupProps: DOMAttributes,
40+
groupProps: GroupDOMAttributes,
4141
/** Props for the input element. */
4242
inputProps: InputHTMLAttributes<HTMLInputElement>,
4343
/** Props for the increment button, to be passed to [useButton](useButton.html). */
@@ -294,10 +294,10 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
294294

295295
return {
296296
groupProps: {
297+
...focusWithinProps,
297298
role: 'group',
298299
'aria-disabled': isDisabled,
299-
'aria-invalid': validationState === 'invalid' ? 'true' : undefined,
300-
...focusWithinProps
300+
'aria-invalid': validationState === 'invalid' ? 'true' : undefined
301301
},
302302
labelProps,
303303
inputProps,

packages/@react-types/shared/src/dom.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,7 @@ export interface DOMAttributes<T = FocusableElement> extends AriaAttributes, Rea
178178
style?: CSSProperties | undefined,
179179
className?: string | undefined
180180
}
181+
182+
export interface GroupDOMAttributes extends Omit<DOMAttributes<HTMLElement>, 'role'> {
183+
role?: 'group' | 'region' | 'presentation'
184+
}
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
{/* Copyright 2020 Adobe. All rights reserved.
2+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License. You may obtain a copy
4+
of the License at http://www.apache.org/licenses/LICENSE-2.0
5+
Unless required by applicable law or agreed to in writing, software distributed under
6+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
7+
OF ANY KIND, either express or implied. See the License for the specific language
8+
governing permissions and limitations under the License. */}
9+
10+
import {Layout} from '@react-spectrum/docs';
11+
export default Layout;
12+
13+
import docs from 'docs:react-aria-components';
14+
import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs';
15+
import styles from '@react-spectrum/docs/src/docs.css';
16+
import packageData from 'react-aria-components/package.json';
17+
import ChevronRight from '@spectrum-icons/workflow/ChevronRight';
18+
import {Divider} from '@react-spectrum/divider';
19+
import {Keyboard} from '@react-spectrum/text';
20+
21+
---
22+
category: Content
23+
keywords: [group, aria]
24+
type: component
25+
---
26+
27+
# Group
28+
29+
<PageDescription>{docs.exports.Group.description}</PageDescription>
30+
31+
<HeaderInfo
32+
packageData={packageData}
33+
componentNames={['Group']}
34+
sourceData={[
35+
{type: 'W3C', url: 'https://w3c.github.io/aria/#group'}
36+
]} />
37+
38+
## Example
39+
40+
```tsx example
41+
import {TextField, Label, Group, Input, Button} from 'react-aria-components';
42+
43+
<TextField>
44+
<Label>Email</Label>
45+
<Group>
46+
<Input />
47+
<Button aria-label="Add email">➕</Button>
48+
</Group>
49+
</TextField>
50+
```
51+
52+
<details>
53+
<summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>
54+
55+
```css
56+
.react-aria-Group {
57+
--field-border: var(--spectrum-alias-border-color);
58+
--field-border-hovered: var(--spectrum-alias-border-color-hover);
59+
--field-background: var(--spectrum-global-color-gray-50);
60+
--text-color: var(--spectrum-alias-text-color);
61+
--text-color-disabled: var(--spectrum-alias-text-color-disabled);
62+
--focus-ring-color: slateblue;
63+
--invalid-color: var(--spectrum-global-color-red-600);
64+
65+
display: flex;
66+
align-items: center;
67+
width: fit-content;
68+
border-radius: 6px;
69+
border: 1px solid var(--field-border);
70+
background: var(--field-background);
71+
overflow: hidden;
72+
73+
&[data-hovered] {
74+
border-color: var(--field-border-hovered);
75+
}
76+
77+
&[data-focus-within] {
78+
border-color: var(--focus-ring-color);
79+
box-shadow: 0 0 0 1px var(--focus-ring-color);
80+
}
81+
82+
.react-aria-Input {
83+
padding: 0.286rem;
84+
margin: 0;
85+
font-size: 1rem;
86+
color: var(--text-color);
87+
outline: none;
88+
border: none;
89+
background: transparent;
90+
91+
&::placeholder {
92+
color: var(--spectrum-gray-600);
93+
}
94+
}
95+
96+
.react-aria-Button {
97+
padding: 0 6px;
98+
border-width: 0 0 0 1px;
99+
border-radius: 0 6px 6px 0;
100+
align-self: stretch;
101+
}
102+
}
103+
104+
.react-aria-Button {
105+
--border-color: var(--spectrum-alias-border-color);
106+
--border-color-pressed: var(--spectrum-alias-border-color-down);
107+
--border-color-disabled: var(--spectrum-alias-border-color-disabled);
108+
--background-color: var(--spectrum-global-color-gray-50);
109+
--background-color-pressed: var(--spectrum-global-color-gray-100);
110+
--text-color: var(--spectrum-alias-text-color);
111+
--text-color-disabled: var(--spectrum-alias-text-color-disabled);
112+
--focus-ring-color: slateblue;
113+
114+
color: var(--text-color);
115+
background: var(--background-color);
116+
border: 1px solid var(--border-color);
117+
border-radius: 4px;
118+
appearance: none;
119+
vertical-align: middle;
120+
font-size: 1rem;
121+
text-align: center;
122+
margin: 0;
123+
outline: none;
124+
padding: 6px 10px;
125+
text-decoration: none;
126+
127+
&[data-pressed] {
128+
box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
129+
background: var(--background-color-pressed);
130+
border-color: var(--border-color-pressed);
131+
}
132+
133+
&[data-focus-visible] {
134+
border-color: var(--focus-ring-color);
135+
box-shadow: 0 0 0 1px var(--focus-ring-color);
136+
}
137+
}
138+
139+
@media (forced-colors: active) {
140+
.react-aria-TextField {
141+
--field-border: ButtonBorder;
142+
--field-border-disabled: GrayText;
143+
--field-background: Field;
144+
--text-color: FieldText;
145+
--text-color-disabled: GrayText;
146+
--focus-ring-color: Highlight;
147+
--invalid-color: LinkText;
148+
}
149+
}
150+
```
151+
152+
</details>
153+
154+
## Features
155+
156+
A group can be created with a `<div role="group">` or via the HTML [&lt;fieldset&gt;](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset) element. The `Group` component supports additional UI states, and can be used standalone or as part of a larger pattern such as [NumberField](NumberField.html) or [DatePicker](Datepicker.html).
157+
158+
* **Styleable** – Hover, keyboard focus, disabled, and invalid states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes.
159+
* **Accessible** – Implemented using the ARIA "group" role by default, with optional support for the "region" landmark role.
160+
161+
## Anatomy
162+
163+
A group consists of a container element for a set of semantically related UI controls. It supports states such as hover, focus within, and disabled, which are useful to style visually adjoined children.
164+
165+
```tsx
166+
import {Group} from 'react-aria-components';
167+
168+
<Group>
169+
{/* ... */}
170+
</Group>
171+
```
172+
173+
## Accessibility
174+
175+
### Labeling
176+
177+
Group accepts the `aria-label` and `aria-labelledby` attributes to provide an accessible label to the group as a whole. This is read by assistive technology when navigating into the group from outside. When the labels of each child element of the group do not provide sufficient context on their own, the group should receive an additional label.
178+
179+
```tsx example
180+
<span id="label-id">Serial number</span>
181+
<Group aria-labelledby="label-id">
182+
<Input size={3} aria-label="First 3 digits" placeholder="000" />
183+
184+
<Input size={2} aria-label="Middle 2 digits" placeholder="00" />
185+
186+
<Input size={4} aria-label="Last 4 digits" placeholder="0000" />
187+
</Group>
188+
```
189+
190+
### Role
191+
192+
By default, `Group` uses the [group](https://w3c.github.io/aria/#group) ARIA role. If the contents of the group is important enough to be included in the page table of contents, use `role="region"` instead, and ensure that an `aria-label` or `aria-labelledby` prop is assigned.
193+
194+
```tsx
195+
<Group role="region" aria-label="Object details">
196+
{/* ... */}
197+
</Group>
198+
```
199+
200+
If the `Group` component is used for styling purposes only, and does not include a set of related UI controls, then use `role="presentation"` instead.
201+
202+
## Props
203+
204+
<PropTable component={docs.exports.Group} links={docs.links} />
205+
206+
## Styling
207+
208+
React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention.
209+
210+
```css
211+
.react-aria-Group {
212+
/* ... */
213+
}
214+
```
215+
216+
A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own.
217+
218+
```jsx
219+
<Group className="my-group">
220+
{/* ... */}
221+
</Group>
222+
```
223+
224+
In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example:
225+
226+
```css
227+
.react-aria-Group[data-hovered] {
228+
/* ... */
229+
}
230+
231+
.react-aria-Group[data-focus-visible] {
232+
/* ... */
233+
}
234+
```
235+
236+
The states, selectors, and render props for `Group` are documented below.
237+
238+
<StateTable properties={docs.exports.GroupRenderProps.properties} />
239+
240+
## Advanced customization
241+
242+
### Contexts
243+
244+
All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in [mergeProps](mergeProps.html)).
245+
246+
<ContextTable components={['Group']} docs={docs} />
247+
248+
This example shows a `LabeledGroup` component that accepts a label and a group as children. It uses the [useId](useId.html) hook to generate a unique id for the label, and provides this to the group via the `aria-labelledby` prop.
249+
250+
```tsx example
251+
import {LabelContext, GroupContext} from 'react-aria-components';
252+
import {useId} from 'react-aria';
253+
254+
function LabeledGroup({children}) {
255+
let labelId = useId();
256+
257+
return (
258+
<LabelContext.Provider value={{id: labelId, elementType: 'span'}}>
259+
<GroupContext.Provider value={{'aria-labelledby': labelId}}>
260+
{children}
261+
</GroupContext.Provider>
262+
</LabelContext.Provider>
263+
);
264+
}
265+
266+
<LabeledGroup>
267+
<Label>Expiration date</Label>
268+
<Group>
269+
<Input size={3} aria-label="Month" placeholder="mm" />
270+
/
271+
<Input size={4} aria-label="Year" placeholder="yyyy" />
272+
</Group>
273+
</LabeledGroup>
274+
```

0 commit comments

Comments
 (0)