Skip to content

Commit c111ba8

Browse files
committed
Support uncontrolled open
1 parent 0065779 commit c111ba8

File tree

2 files changed

+46
-45
lines changed

2 files changed

+46
-45
lines changed

packages/react/src/SelectPanel/SelectPanel.stories.tsx

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,51 +15,35 @@ const meta: Meta<typeof SelectPanel> = {
1515

1616
export default meta
1717

18-
function getColorCircle(color: string) {
19-
return function () {
20-
return (
21-
<Box
22-
sx={{
23-
backgroundColor: color,
24-
borderColor: color,
25-
width: 14,
26-
height: 14,
27-
borderRadius: 10,
28-
margin: 'auto',
29-
borderWidth: '1px',
30-
borderStyle: 'solid',
31-
}}
32-
/>
33-
)
34-
}
35-
}
36-
3718
const items: ItemInput[] = [
3819
{
39-
leadingVisual: getColorCircle('#a2eeef'),
4020
text: 'enhancement',
41-
description: 'New feature or request',
42-
descriptionVariant: 'block',
4321
id: 1,
4422
},
4523
{
46-
leadingVisual: getColorCircle('#d73a4a'),
4724
text: 'bug',
48-
description: "Something isn't working",
49-
descriptionVariant: 'block',
5025
id: 2,
5126
},
5227
{
53-
leadingVisual: getColorCircle('#0cf478'),
5428
text: 'good first issue',
55-
description: 'Good for newcomers',
56-
descriptionVariant: 'block',
5729
id: 3,
5830
},
59-
{leadingVisual: getColorCircle('#ffd78e'), text: 'design', id: 4},
60-
{leadingVisual: getColorCircle('#ff0000'), text: 'blocker', id: 5},
61-
{leadingVisual: getColorCircle('#a4f287'), text: 'backend', id: 6},
62-
{leadingVisual: getColorCircle('#8dc6fc'), text: 'frontend', id: 7},
31+
{
32+
text: 'design',
33+
id: 4,
34+
},
35+
{
36+
text: 'blocker',
37+
id: 5,
38+
},
39+
{
40+
text: 'backend',
41+
id: 6,
42+
},
43+
{
44+
text: 'frontend',
45+
id: 7,
46+
},
6347
]
6448

6549
export const Default = () => {
@@ -80,7 +64,6 @@ export const Default = () => {
8064
if (!aIsSelected && bIsSelected) return 1
8165
return 0
8266
})
83-
const [open, setOpen] = useState(false)
8467

8568
return (
8669
<FormControl>
@@ -94,8 +77,6 @@ export const Default = () => {
9477
{children}
9578
</Button>
9679
)}
97-
open={open}
98-
onOpenChange={setOpen}
9980
items={selectedItemsSortedFirst}
10081
selected={selected}
10182
onSelectedChange={setSelected}

packages/react/src/SelectPanel/SelectPanel.tsx

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {clsx} from 'clsx'
2727
import {heightMap} from '../Overlay/Overlay'
2828
import {debounce} from '@github/mini-throttle'
2929

30+
type Gesture = 'anchor-click' | 'anchor-key-press' | 'click-outside' | 'escape' | 'selection'
31+
3032
// we add a delay so that it does not interrupt default screen reader announcement and queues after it
3133
const SHORT_DELAY_MS = 500
3234
const LONG_DELAY_MS = 1000
@@ -72,10 +74,8 @@ interface SelectPanelBaseProps {
7274
// TODO: Make `title` required in the next major version
7375
title?: string | React.ReactElement
7476
subtitle?: string | React.ReactElement
75-
onOpenChange: (
76-
open: boolean,
77-
gesture: 'anchor-click' | 'anchor-key-press' | 'click-outside' | 'escape' | 'selection',
78-
) => void
77+
open?: boolean
78+
onOpenChange?: (open: boolean, gesture: Gesture) => void
7979
placeholder?: string
8080
// TODO: Make `inputLabel` required in next major version
8181
inputLabel?: string
@@ -97,7 +97,7 @@ interface SelectPanelBaseProps {
9797

9898
export type SelectPanelProps = SelectPanelBaseProps &
9999
Omit<FilteredActionListProps, 'selectionVariant'> &
100-
Pick<AnchoredOverlayProps, 'open' | 'height' | 'width'> &
100+
Pick<AnchoredOverlayProps, 'height' | 'width'> &
101101
AnchoredOverlayWrapperAnchorProps &
102102
(SelectPanelSingleSelection | SelectPanelMultiSelection)
103103

@@ -122,9 +122,28 @@ const doesItemsIncludeItem = (items: ItemInput[], item: ItemInput) => {
122122
return items.some(i => areItemsEqual(i, item))
123123
}
124124

125+
// We can't use the `useProvidedStateOrCreate` hook here because we need to handle the gesture
126+
// separately from the open state. So, we have to use a separate state and a callback to update
127+
// the open state.
128+
function useProvidedOnOpenOrCreate(
129+
externalOpen: boolean | undefined,
130+
externalOnOpenChange: ((open: boolean, gesture: Gesture) => void) | undefined,
131+
) {
132+
const [internalOpen, setInternalOpen] = useState<boolean>(false)
133+
const open = externalOpen ?? internalOpen
134+
const onOpenChange = useCallback(
135+
(newOpen: boolean, gesture: Gesture) => {
136+
setInternalOpen(newOpen)
137+
if (externalOnOpenChange) externalOnOpenChange(open, gesture)
138+
},
139+
[open, externalOnOpenChange],
140+
)
141+
return [open, onOpenChange] as const
142+
}
143+
125144
export function SelectPanel({
126-
open,
127-
onOpenChange,
145+
open: externalOpen,
146+
onOpenChange: externalOnOpenChange,
128147
renderAnchor = props => {
129148
const {children, ...rest} = props
130149
return (
@@ -183,6 +202,8 @@ export function SelectPanel({
183202
[needsNoItemsAnnouncement],
184203
)
185204

205+
const [open, onOpenChange] = useProvidedOnOpenOrCreate(externalOpen, externalOnOpenChange)
206+
186207
const onInputRefChanged = useCallback(
187208
(ref: React.RefObject<HTMLInputElement>) => {
188209
setInputRef(ref)
@@ -307,9 +328,8 @@ export function SelectPanel({
307328
[onOpenChange],
308329
)
309330
const onClose = useCallback(
310-
(gesture: Parameters<Exclude<AnchoredOverlayProps['onClose'], undefined>>[0] | 'selection' | 'escape') => {
311-
onOpenChange(false, gesture)
312-
},
331+
(gesture: Parameters<Exclude<AnchoredOverlayProps['onClose'], undefined>>[0] | 'selection' | 'escape') =>
332+
onOpenChange(false, gesture),
313333
[onOpenChange],
314334
)
315335

0 commit comments

Comments
 (0)