Skip to content

Commit 84a8bba

Browse files
authored
Merge branch 'main' into test-enable-chromatic
2 parents 5850eec + 2a6efbf commit 84a8bba

File tree

12 files changed

+178
-68
lines changed

12 files changed

+178
-68
lines changed

.changeset/cuddly-experts-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Makes SegmentedControl uncontrolled by default.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": patch
3+
---
4+
5+
Fix slots infinite rendering when no `context` prop is provided

docs/content/Checkbox.mdx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ The `Checkbox` component can be used in controlled and uncontrolled modes.
4545
technologies.
4646
</Note>
4747

48-
## Indeterminate
48+
### Indeterminate
4949

5050
An `indeterminate` checkbox state should be used if the input value is neither true nor false. This can be useful in situations where you are required to display an incomplete state, or one that is dependent on other input selections to determine a value.
5151

@@ -74,6 +74,34 @@ An `indeterminate` checkbox state should be used if the input value is neither t
7474
</form>
7575
```
7676

77+
### Grouping Checkbox components
78+
79+
If you're not building something custom, you should use the [CheckboxGroup](/CheckboxGroup) component to render a group of checkbox inputs.
80+
81+
```jsx live
82+
<form>
83+
<CheckboxGroup>
84+
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
85+
<FormControl>
86+
<Checkbox value="1" />
87+
<FormControl.Label>Checkbox 1</FormControl.Label>
88+
</FormControl>
89+
<FormControl>
90+
<Checkbox value="2" />
91+
<FormControl.Label>Checkbox 2</FormControl.Label>
92+
</FormControl>
93+
<FormControl>
94+
<Checkbox value="3" />
95+
<FormControl.Label>Checkbox 3</FormControl.Label>
96+
</FormControl>
97+
<FormControl>
98+
<Checkbox value="4" />
99+
<FormControl.Label>Checkbox 4</FormControl.Label>
100+
</FormControl>
101+
</CheckboxGroup>
102+
</form>
103+
```
104+
77105
## Props
78106

79107
<PropsTable>

docs/content/Radio.mdx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,39 @@ Please use a [Checkbox](/Checkbox) if the user needs to select more than one opt
3131

3232
</Note>
3333

34-
## Grouping Radio components
34+
### Grouping Radio components
3535

3636
Use the `name` prop to group together related `Radio` components in a list.
3737

38-
If you're not building something custom, you should use the [ChoiceFieldset](/ChoiceFieldset) component to render a group of radio inputs.
38+
If you're not building something custom, you should use the [RadioGroup](/RadioGroup) component to render a group of radio inputs.
39+
40+
#### Using `RadioGroup`
41+
42+
```jsx live
43+
<form>
44+
<RadioGroup name="radioGroup-example">
45+
<RadioGroup.Label>Choices</RadioGroup.Label>
46+
<FormControl>
47+
<Radio value="1" />
48+
<FormControl.Label>Radio 1</FormControl.Label>
49+
</FormControl>
50+
<FormControl>
51+
<Radio value="2" />
52+
<FormControl.Label>Radio 2</FormControl.Label>
53+
</FormControl>
54+
<FormControl>
55+
<Radio value="3" />
56+
<FormControl.Label>Radio 3</FormControl.Label>
57+
</FormControl>
58+
<FormControl>
59+
<Radio value="4" />
60+
<FormControl.Label>Radio 4</FormControl.Label>
61+
</FormControl>
62+
</RadioGroup>
63+
</form>
64+
```
65+
66+
#### Using `name` to group Radio components
3967

4068
```jsx live
4169
<form>

docs/content/SegmentedControl.mdx renamed to docs/content/drafts/SegmentedControl.mdx

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,45 @@ status: Draft
44
description: Use a segmented control to let users select an option from a short list and immediately apply the selection
55
---
66

7-
<Note variant="warning">Not implemented yet</Note>
8-
97
## Examples
108

11-
### Simple
9+
### Uncontrolled (default)
1210

1311
```jsx live drafts
1412
<SegmentedControl aria-label="File view">
15-
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
13+
<SegmentedControl.Button defaultSelected>Preview</SegmentedControl.Button>
1614
<SegmentedControl.Button>Raw</SegmentedControl.Button>
1715
<SegmentedControl.Button>Blame</SegmentedControl.Button>
1816
</SegmentedControl>
1917
```
2018

19+
### Controlled
20+
21+
```javascript noinline live drafts
22+
const Controlled = () => {
23+
const [selectedIndex, setSelectedIndex] = React.useState(0)
24+
25+
const handleSegmentChange = selectedIndex => {
26+
setSelectedIndex(selectedIndex)
27+
}
28+
29+
return (
30+
<SegmentedControl aria-label="File view" onChange={handleSegmentChange}>
31+
<SegmentedControl.Button selected={selectedIndex === 0}>Preview</SegmentedControl.Button>
32+
<SegmentedControl.Button selected={selectedIndex === 1}>Raw</SegmentedControl.Button>
33+
<SegmentedControl.Button selected={selectedIndex === 2}>Blame</SegmentedControl.Button>
34+
</SegmentedControl>
35+
)
36+
}
37+
38+
render(Controlled)
39+
```
40+
2141
### With icons and labels
2242

2343
```jsx live drafts
2444
<SegmentedControl aria-label="File view">
25-
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
45+
<SegmentedControl.Button defaultSelected leadingIcon={EyeIcon}>
2646
Preview
2747
</SegmentedControl.Button>
2848
<SegmentedControl.Button leadingIcon={FileCodeIcon}>Raw</SegmentedControl.Button>
@@ -34,7 +54,7 @@ description: Use a segmented control to let users select an option from a short
3454

3555
```jsx live drafts
3656
<SegmentedControl aria-label="File view">
37-
<SegmentedControl.IconButton selected icon={EyeIcon} aria-label="Preview" />
57+
<SegmentedControl.IconButton defaultSelected icon={EyeIcon} aria-label="Preview" />
3858
<SegmentedControl.IconButton icon={FileCodeIcon} aria-label="Raw" />
3959
<SegmentedControl.IconButton icon={PeopleIcon} aria-label="Blame" />
4060
</SegmentedControl>
@@ -44,7 +64,7 @@ description: Use a segmented control to let users select an option from a short
4464

4565
```jsx live drafts
4666
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'default'}}>
47-
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
67+
<SegmentedControl.Button defaultSelected leadingIcon={EyeIcon}>
4868
Preview
4969
</SegmentedControl.Button>
5070
<SegmentedControl.Button leadingIcon={FileCodeIcon}>Raw</SegmentedControl.Button>
@@ -56,7 +76,7 @@ description: Use a segmented control to let users select an option from a short
5676

5777
```jsx live drafts
5878
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'default'}}>
59-
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
79+
<SegmentedControl.Button defaultSelected leadingIcon={EyeIcon}>
6080
Preview
6181
</SegmentedControl.Button>
6282
<SegmentedControl.Button leadingIcon={FileCodeIcon}>Raw</SegmentedControl.Button>
@@ -68,17 +88,7 @@ description: Use a segmented control to let users select an option from a short
6888

6989
```jsx live drafts
7090
<SegmentedControl fullWidth aria-label="File view">
71-
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
72-
<SegmentedControl.Button>Raw</SegmentedControl.Button>
73-
<SegmentedControl.Button>Blame</SegmentedControl.Button>
74-
</SegmentedControl>
75-
```
76-
77-
### In a loading state
78-
79-
```jsx live drafts
80-
<SegmentedControl loading aria-label="File view">
81-
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
91+
<SegmentedControl.Button defaultSelected>Preview</SegmentedControl.Button>
8292
<SegmentedControl.Button>Raw</SegmentedControl.Button>
8393
<SegmentedControl.Button>Blame</SegmentedControl.Button>
8494
</SegmentedControl>
@@ -97,7 +107,7 @@ description: Use a segmented control to let users select an option from a short
97107
</Text>
98108
</Box>
99109
<SegmentedControl aria-labelledby="scLabel-vert" aria-describedby="scCaption-vert">
100-
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
110+
<SegmentedControl.Button defaultSelected>Preview</SegmentedControl.Button>
101111
<SegmentedControl.Button>Raw</SegmentedControl.Button>
102112
<SegmentedControl.Button>Blame</SegmentedControl.Button>
103113
</SegmentedControl>
@@ -110,7 +120,7 @@ description: Use a segmented control to let users select an option from a short
110120
<FormControl>
111121
<FormControl.Label id="scLabel-horiz">File view</FormControl.Label>
112122
<SegmentedControl aria-labelledby="scLabel-horiz" aria-describedby="scCaption-horiz">
113-
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
123+
<SegmentedControl.Button defaultSelected>Preview</SegmentedControl.Button>
114124
<SegmentedControl.Button>Raw</SegmentedControl.Button>
115125
<SegmentedControl.Button>Blame</SegmentedControl.Button>
116126
</SegmentedControl>
@@ -122,23 +132,8 @@ description: Use a segmented control to let users select an option from a short
122132

123133
```jsx live drafts
124134
<SegmentedControl aria-label="File view">
125-
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
126-
<SegmentedControl.Button selected>Raw</SegmentedControl.Button>
127-
<SegmentedControl.Button>Blame</SegmentedControl.Button>
128-
</SegmentedControl>
129-
```
130-
131-
### With a selection change handler
132-
133-
```jsx live drafts
134-
<SegmentedControl
135-
aria-label="File view"
136-
onChange={selectedIndex => {
137-
alert(`Segment ${selectedIndex}`)
138-
}}
139-
>
140135
<SegmentedControl.Button>Preview</SegmentedControl.Button>
141-
<SegmentedControl.Button>Raw</SegmentedControl.Button>
136+
<SegmentedControl.Button defaultSelected>Raw</SegmentedControl.Button>
142137
<SegmentedControl.Button>Blame</SegmentedControl.Button>
143138
</SegmentedControl>
144139
```
@@ -161,12 +156,10 @@ description: Use a segmented control to let users select an option from a short
161156
}`}
162157
description="Whether the control fills the width of its parent"
163158
/>
164-
<PropsTableRow name="loading" type="boolean" description="Whether the selected segment is being calculated" />
165159
<PropsTableRow
166160
name="onChange"
167161
type="(selectedIndex?: number) => void"
168162
description="The handler that gets called when a segment is selected"
169-
required
170163
/>
171164
<PropsTableRow
172165
name="variant"
@@ -187,7 +180,16 @@ description: Use a segmented control to let users select an option from a short
187180

188181
<PropsTable>
189182
<PropsTableRow name="leadingIcon" type="Component" description="The leading icon comes before item label" />
190-
<PropsTableRow name="selected" type="boolean" description="Whether the segment is selected" />
183+
<PropsTableRow
184+
name="selected"
185+
type="boolean"
186+
description="Whether the segment is selected. This is used for controlled SegmentedControls, and needs to be updated using the onChange handler on SegmentedControl."
187+
/>
188+
<PropsTableRow
189+
name="defaultSelected"
190+
type="boolean"
191+
description="Whether the segment is selected. This is used for uncontrolled SegmentedControls to pick one SegmentedControlButton that is selected on the initial render."
192+
/>
191193
<PropsTableSxRow />
192194
<PropsTableRefRow refType="HTMLButtonElement" />
193195
</PropsTable>
@@ -202,7 +204,16 @@ description: Use a segmented control to let users select an option from a short
202204
description="The icon that represents the segmented control item"
203205
required
204206
/>
205-
<PropsTableRow name="selected" type="boolean" description="Whether the segment is selected" />
207+
<PropsTableRow
208+
name="selected"
209+
type="boolean"
210+
description="Whether the segment is selected. This is used for controlled SegmentedControls, and needs to be updated using the onChange handler on SegmentedControl."
211+
/>
212+
<PropsTableRow
213+
name="defaultSelected"
214+
type="boolean"
215+
description="Whether the segment is selected. This is used for uncontrolled SegmentedControls to pick one SegmentedControlButton that is selected on the initial render."
216+
/>
206217
<PropsTableSxRow />
207218
<PropsTableRefRow refType="HTMLButtonElement" />
208219
</PropsTable>

src/SegmentedControl/SegmentedControl.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,25 @@ describe('SegmentedControl', () => {
175175
expect(handleChange).toHaveBeenCalledWith(1)
176176
})
177177

178+
it('changes selection to the clicked segment even without onChange being passed', async () => {
179+
const user = userEvent.setup()
180+
const {getByText} = render(
181+
<SegmentedControl aria-label="File view">
182+
{segmentData.map(({label}) => (
183+
<SegmentedControl.Button key={label}>{label}</SegmentedControl.Button>
184+
))}
185+
</SegmentedControl>
186+
)
187+
188+
const buttonToClick = getByText('Raw').closest('button')
189+
190+
expect(buttonToClick?.getAttribute('aria-current')).toBe('false')
191+
if (buttonToClick) {
192+
await user.click(buttonToClick)
193+
}
194+
expect(buttonToClick?.getAttribute('aria-current')).toBe('true')
195+
})
196+
178197
it('calls segment button onClick if it is passed', async () => {
179198
const user = userEvent.setup()
180199
const handleClick = jest.fn()

src/SegmentedControl/SegmentedControl.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useRef} from 'react'
1+
import React, {useRef, useState} from 'react'
22
import Button, {SegmentedControlButtonProps} from './SegmentedControlButton'
33
import SegmentedControlIconButton, {SegmentedControlIconButtonProps} from './SegmentedControlIconButton'
44
import {ActionList, ActionMenu, useTheme} from '..'
@@ -51,14 +51,21 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
5151
}) => {
5252
const segmentedControlContainerRef = useRef<HTMLUListElement>(null)
5353
const {theme} = useTheme()
54+
const isUncontrolled =
55+
onChange === undefined ||
56+
React.Children.toArray(children).some(
57+
child => React.isValidElement<SegmentedControlButtonProps>(child) && child.props.defaultSelected !== undefined
58+
)
5459
const responsiveVariant = useResponsiveValue(variant, 'default')
5560
const isFullWidth = useResponsiveValue(fullWidth, false)
5661
const selectedSegments = React.Children.toArray(children).map(
5762
child =>
5863
React.isValidElement<SegmentedControlButtonProps | SegmentedControlIconButtonProps>(child) && child.props.selected
5964
)
6065
const hasSelectedButton = selectedSegments.some(isSelected => isSelected)
61-
const selectedIndex = hasSelectedButton ? selectedSegments.indexOf(true) : 0
66+
const selectedIndexExternal = hasSelectedButton ? selectedSegments.indexOf(true) : 0
67+
const [selectedIndexInternalState, setSelectedIndexInternalState] = useState<number>(selectedIndexExternal)
68+
const selectedIndex = isUncontrolled ? selectedIndexInternalState : selectedIndexExternal
6269
const selectedChild = React.isValidElement<SegmentedControlButtonProps | SegmentedControlIconButtonProps>(
6370
React.Children.toArray(children)[selectedIndex]
6471
)
@@ -108,18 +115,11 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
108115
<ActionList.Item
109116
key={`segmented-control-action-btn-${index}`}
110117
selected={index === selectedIndex}
111-
onSelect={
112-
onChange
113-
? (event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>) => {
114-
onChange(index)
115-
// TODO: figure out a way around the typecasting
116-
child.props.onClick && child.props.onClick(event as React.MouseEvent<HTMLLIElement>)
117-
}
118-
: // TODO: figure out a way around the typecasting
119-
(child.props.onClick as (
120-
event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>
121-
) => void)
122-
}
118+
onSelect={(event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>) => {
119+
isUncontrolled && setSelectedIndexInternalState(index)
120+
onChange && onChange(index)
121+
child.props.onClick && child.props.onClick(event as React.MouseEvent<HTMLLIElement>)
122+
}}
123123
>
124124
{ChildIcon && <ChildIcon />} {getChildText(child)}
125125
</ActionList.Item>
@@ -146,9 +146,13 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
146146
onClick: onChange
147147
? (event: React.MouseEvent<HTMLButtonElement>) => {
148148
onChange(index)
149+
isUncontrolled && setSelectedIndexInternalState(index)
149150
child.props.onClick && child.props.onClick(event)
150151
}
151-
: child.props.onClick,
152+
: (event: React.MouseEvent<HTMLButtonElement>) => {
153+
child.props.onClick && child.props.onClick(event)
154+
isUncontrolled && setSelectedIndexInternalState(index)
155+
},
152156
selected: index === selectedIndex,
153157
sx: {
154158
'--separator-color':

src/SegmentedControl/SegmentedControlButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {getSegmentedControlButtonStyles, getSegmentedControlListItemStyles} from
88
export type SegmentedControlButtonProps = {
99
/** The visible label rendered in the button */
1010
children: string
11-
/** Whether the segment is selected */
11+
/** Whether the segment is selected. This is used for controlled `SegmentedControls`, and needs to be updated using the `onChange` handler on `SegmentedControl`. */
1212
selected?: boolean
13+
/** Whether the segment is selected. This is used for uncontrolled `SegmentedControls` to pick one `SegmentedControlButton` that is selected on the initial render. */
14+
defaultSelected?: boolean
1315
/** The leading icon comes before item label */
1416
leadingIcon?: React.FunctionComponent<React.PropsWithChildren<IconProps>>
1517
} & SxProp &

0 commit comments

Comments
 (0)