@primer/components 30.3.0-rc.2010c7d4 → 31.0.0-rc.15aa0a10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +2 -1
- package/.storybook/preview.js +4 -4
- package/CHANGELOG.md +12 -2
- package/codemods/deprecateUtilityComponents.js +1 -1
- package/contributor-docs/adrs/adr-003-prop-norms.md +72 -0
- package/dist/browser.esm.js +798 -794
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +801 -797
- package/dist/browser.umd.js.map +1 -1
- package/docs/content/Autocomplete.mdx +627 -0
- package/docs/content/TextInputTokens.mdx +89 -0
- package/docs/content/getting-started.md +1 -1
- package/docs/content/overriding-styles.mdx +7 -6
- package/docs/content/theming.md +5 -5
- package/docs/package-lock.json +288 -511
- package/docs/package.json +1 -1
- package/docs/src/@primer/gatsby-theme-doctocat/components/hero.js +14 -12
- package/docs/src/@primer/gatsby-theme-doctocat/nav.yml +2 -0
- package/docs/src/@primer/gatsby-theme-doctocat/primer-components-hero.svg +7 -7
- package/lib/ActionList/Item.js +1 -1
- package/lib/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
- package/lib/AnchoredOverlay/AnchoredOverlay.js +11 -3
- package/lib/Autocomplete/Autocomplete.d.ts +304 -0
- package/lib/Autocomplete/Autocomplete.js +145 -0
- package/lib/Autocomplete/AutocompleteContext.d.ts +17 -0
- package/lib/Autocomplete/AutocompleteContext.js +11 -0
- package/lib/Autocomplete/AutocompleteInput.d.ts +292 -0
- package/lib/Autocomplete/AutocompleteInput.js +157 -0
- package/lib/Autocomplete/AutocompleteMenu.d.ts +72 -0
- package/lib/Autocomplete/AutocompleteMenu.js +224 -0
- package/lib/Autocomplete/AutocompleteOverlay.d.ts +20 -0
- package/lib/Autocomplete/AutocompleteOverlay.js +80 -0
- package/lib/Autocomplete/index.d.ts +2 -0
- package/lib/Autocomplete/index.js +15 -0
- package/lib/BaseStyles.js +1 -1
- package/lib/BorderBox.js +1 -1
- package/lib/Button/ButtonInvisible.js +1 -1
- package/lib/Caret.js +2 -2
- package/lib/Dialog.js +1 -1
- package/lib/FilteredActionList/FilteredActionList.js +5 -31
- package/lib/Flash.js +16 -16
- package/lib/Label.js +1 -1
- package/lib/Overlay.d.ts +1 -0
- package/lib/Overlay.js +3 -1
- package/lib/ProgressBar.js +1 -1
- package/lib/StateLabel.js +13 -19
- package/lib/Token/_RemoveTokenButton.js +1 -1
- package/lib/__tests__/Autocomplete.test.d.ts +1 -0
- package/lib/__tests__/Autocomplete.test.js +528 -0
- package/lib/__tests__/BorderBox.test.js +1 -1
- package/lib/__tests__/CircleOcticon.test.js +1 -1
- package/lib/__tests__/CounterLabel.test.js +4 -4
- package/lib/__tests__/Flash.test.js +4 -4
- package/lib/__tests__/Link.test.js +1 -1
- package/lib/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
- package/lib/__tests__/behaviors/scrollIntoViewingArea.test.js +226 -0
- package/lib/behaviors/scrollIntoViewingArea.d.ts +1 -0
- package/lib/behaviors/scrollIntoViewingArea.js +39 -0
- package/lib/hooks/useOpenAndCloseFocus.d.ts +2 -1
- package/lib/hooks/useOpenAndCloseFocus.js +7 -2
- package/lib/hooks/useOverlay.d.ts +2 -1
- package/lib/hooks/useOverlay.js +4 -2
- package/lib/index.d.ts +2 -0
- package/lib/index.js +8 -0
- package/lib/stories/Autocomplete.stories.js +608 -0
- package/lib/stories/Dialog.stories.js +3 -3
- package/lib/stories/IssueLabelToken.stories.js +1 -1
- package/lib/stories/ProfileToken.stories.js +1 -1
- package/lib/theme-preval.js +370 -3100
- package/lib/utils/testing.d.ts +50 -493
- package/lib/utils/types/MandateProps.d.ts +3 -0
- package/lib/utils/types/MandateProps.js +1 -0
- package/lib/utils/types/index.d.ts +1 -0
- package/lib/utils/types/index.js +13 -0
- package/lib-esm/ActionList/Item.js +1 -1
- package/lib-esm/AnchoredOverlay/AnchoredOverlay.d.ts +2 -1
- package/lib-esm/AnchoredOverlay/AnchoredOverlay.js +11 -3
- package/lib-esm/Autocomplete/Autocomplete.d.ts +304 -0
- package/lib-esm/Autocomplete/Autocomplete.js +123 -0
- package/lib-esm/Autocomplete/AutocompleteContext.d.ts +17 -0
- package/lib-esm/Autocomplete/AutocompleteContext.js +2 -0
- package/lib-esm/Autocomplete/AutocompleteInput.d.ts +292 -0
- package/lib-esm/Autocomplete/AutocompleteInput.js +138 -0
- package/lib-esm/Autocomplete/AutocompleteMenu.d.ts +72 -0
- package/lib-esm/Autocomplete/AutocompleteMenu.js +205 -0
- package/lib-esm/Autocomplete/AutocompleteOverlay.d.ts +20 -0
- package/lib-esm/Autocomplete/AutocompleteOverlay.js +62 -0
- package/lib-esm/Autocomplete/index.d.ts +2 -0
- package/lib-esm/Autocomplete/index.js +1 -0
- package/lib-esm/BaseStyles.js +1 -1
- package/lib-esm/BorderBox.js +1 -1
- package/lib-esm/Button/ButtonInvisible.js +1 -1
- package/lib-esm/Caret.js +2 -2
- package/lib-esm/Dialog.js +1 -1
- package/lib-esm/FilteredActionList/FilteredActionList.js +3 -31
- package/lib-esm/Flash.js +16 -16
- package/lib-esm/Label.js +1 -1
- package/lib-esm/Overlay.d.ts +1 -0
- package/lib-esm/Overlay.js +3 -1
- package/lib-esm/ProgressBar.js +1 -1
- package/lib-esm/StateLabel.js +13 -19
- package/lib-esm/Token/_RemoveTokenButton.js +1 -1
- package/lib-esm/__tests__/Autocomplete.test.d.ts +1 -0
- package/lib-esm/__tests__/Autocomplete.test.js +494 -0
- package/lib-esm/__tests__/BorderBox.test.js +1 -1
- package/lib-esm/__tests__/CircleOcticon.test.js +1 -1
- package/lib-esm/__tests__/CounterLabel.test.js +4 -4
- package/lib-esm/__tests__/Flash.test.js +4 -4
- package/lib-esm/__tests__/Link.test.js +1 -1
- package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.d.ts +1 -0
- package/lib-esm/__tests__/behaviors/scrollIntoViewingArea.test.js +224 -0
- package/lib-esm/behaviors/scrollIntoViewingArea.d.ts +1 -0
- package/lib-esm/behaviors/scrollIntoViewingArea.js +30 -0
- package/lib-esm/hooks/useOpenAndCloseFocus.d.ts +2 -1
- package/lib-esm/hooks/useOpenAndCloseFocus.js +7 -2
- package/lib-esm/hooks/useOverlay.d.ts +2 -1
- package/lib-esm/hooks/useOverlay.js +4 -2
- package/lib-esm/index.d.ts +2 -0
- package/lib-esm/index.js +1 -0
- package/lib-esm/stories/Autocomplete.stories.js +549 -0
- package/lib-esm/stories/Dialog.stories.js +3 -3
- package/lib-esm/stories/IssueLabelToken.stories.js +1 -1
- package/lib-esm/stories/ProfileToken.stories.js +1 -1
- package/lib-esm/theme-preval.js +370 -3100
- package/lib-esm/utils/testing.d.ts +50 -493
- package/lib-esm/utils/types/MandateProps.d.ts +3 -0
- package/lib-esm/utils/types/MandateProps.js +1 -0
- package/lib-esm/utils/types/index.d.ts +1 -0
- package/lib-esm/utils/types/index.js +2 -1
- package/package-lock.json +11 -8
- package/package.json +3 -3
- package/src/ActionList/Item.tsx +1 -1
- package/src/AnchoredOverlay/AnchoredOverlay.tsx +14 -3
- package/src/Autocomplete/Autocomplete.tsx +103 -0
- package/src/Autocomplete/AutocompleteContext.tsx +19 -0
- package/src/Autocomplete/AutocompleteInput.tsx +179 -0
- package/src/Autocomplete/AutocompleteMenu.tsx +341 -0
- package/src/Autocomplete/AutocompleteOverlay.tsx +68 -0
- package/src/Autocomplete/index.ts +2 -0
- package/src/BaseStyles.tsx +1 -1
- package/src/BorderBox.tsx +1 -1
- package/src/Button/ButtonInvisible.tsx +7 -2
- package/src/Caret.tsx +2 -2
- package/src/Dialog.tsx +1 -1
- package/src/FilteredActionList/FilteredActionList.tsx +10 -25
- package/src/Flash.tsx +16 -16
- package/src/Label.tsx +1 -1
- package/src/Overlay.tsx +4 -1
- package/src/ProgressBar.tsx +1 -1
- package/src/StateLabel.tsx +12 -20
- package/src/Token/_RemoveTokenButton.tsx +4 -2
- package/src/__tests__/Autocomplete.test.tsx +444 -0
- package/src/__tests__/BorderBox.test.tsx +1 -1
- package/src/__tests__/CircleOcticon.test.tsx +1 -1
- package/src/__tests__/CounterLabel.test.tsx +4 -4
- package/src/__tests__/Flash.test.tsx +4 -4
- package/src/__tests__/Link.test.tsx +1 -1
- package/src/__tests__/__snapshots__/AnchoredOverlay.test.tsx.snap +3 -3
- package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +3414 -0
- package/src/__tests__/__snapshots__/Button.test.tsx.snap +9 -1
- package/src/__tests__/__snapshots__/ConfirmationDialog.test.tsx.snap +1 -1
- package/src/__tests__/__snapshots__/SelectPanel.test.tsx.snap +1 -1
- package/src/__tests__/__snapshots__/StateLabel.test.tsx.snap +0 -21
- package/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap +16 -16
- package/src/__tests__/__snapshots__/Token.test.tsx.snap +34 -34
- package/src/__tests__/behaviors/scrollIntoViewingArea.test.ts +195 -0
- package/src/behaviors/scrollIntoViewingArea.ts +27 -0
- package/src/hooks/useOpenAndCloseFocus.ts +7 -2
- package/src/hooks/useOverlay.tsx +4 -2
- package/src/index.ts +2 -0
- package/src/stories/Autocomplete.stories.tsx +572 -0
- package/src/stories/Dialog.stories.tsx +3 -3
- package/src/stories/IssueLabelToken.stories.tsx +1 -1
- package/src/stories/ProfileToken.stories.tsx +1 -1
- package/src/utils/types/MandateProps.ts +19 -0
- package/src/utils/types/index.ts +1 -0
- package/stats.html +1 -1
- package/docs/src/@primer/gatsby-theme-doctocat/components/live-code.js +0 -84
- package/docs/src/@primer/gatsby-theme-doctocat/components/nav-dropdown.js +0 -48
- package/docs/src/@primer/gatsby-theme-doctocat/components/wrap-page-element.js +0 -25
@@ -5,14 +5,19 @@ export type UseOpenAndCloseFocusSettings = {
|
|
5
5
|
initialFocusRef?: React.RefObject<HTMLElement>
|
6
6
|
containerRef: React.RefObject<HTMLElement>
|
7
7
|
returnFocusRef: React.RefObject<HTMLElement>
|
8
|
+
preventFocusOnOpen?: boolean
|
8
9
|
}
|
9
10
|
|
10
11
|
export function useOpenAndCloseFocus({
|
11
12
|
initialFocusRef,
|
12
13
|
returnFocusRef,
|
13
|
-
containerRef
|
14
|
+
containerRef,
|
15
|
+
preventFocusOnOpen
|
14
16
|
}: UseOpenAndCloseFocusSettings): void {
|
15
17
|
useEffect(() => {
|
18
|
+
if (preventFocusOnOpen) {
|
19
|
+
return
|
20
|
+
}
|
16
21
|
const returnRef = returnFocusRef.current
|
17
22
|
if (initialFocusRef && initialFocusRef.current) {
|
18
23
|
initialFocusRef.current.focus()
|
@@ -23,5 +28,5 @@ export function useOpenAndCloseFocus({
|
|
23
28
|
return function () {
|
24
29
|
returnRef?.focus()
|
25
30
|
}
|
26
|
-
}, [initialFocusRef, returnFocusRef, containerRef])
|
31
|
+
}, [initialFocusRef, returnFocusRef, containerRef, preventFocusOnOpen])
|
27
32
|
}
|
package/src/hooks/useOverlay.tsx
CHANGED
@@ -10,6 +10,7 @@ export type UseOverlaySettings = {
|
|
10
10
|
onEscape: (e: KeyboardEvent) => void
|
11
11
|
onClickOutside: (e: TouchOrMouseEvent) => void
|
12
12
|
overlayRef?: React.RefObject<HTMLDivElement>
|
13
|
+
preventFocusOnOpen?: boolean
|
13
14
|
}
|
14
15
|
|
15
16
|
export type OverlayReturnProps = {
|
@@ -22,10 +23,11 @@ export const useOverlay = ({
|
|
22
23
|
initialFocusRef,
|
23
24
|
onEscape,
|
24
25
|
ignoreClickRefs,
|
25
|
-
onClickOutside
|
26
|
+
onClickOutside,
|
27
|
+
preventFocusOnOpen
|
26
28
|
}: UseOverlaySettings): OverlayReturnProps => {
|
27
29
|
const overlayRef = useProvidedRefOrCreate<HTMLDivElement>(_overlayRef)
|
28
|
-
useOpenAndCloseFocus({containerRef: overlayRef, returnFocusRef, initialFocusRef})
|
30
|
+
useOpenAndCloseFocus({containerRef: overlayRef, returnFocusRef, initialFocusRef, preventFocusOnOpen})
|
29
31
|
useOnOutsideClick({containerRef: overlayRef, ignoreClickRefs, onClickOutside})
|
30
32
|
useOnEscapePress(onEscape)
|
31
33
|
return {ref: overlayRef}
|
package/src/index.ts
CHANGED
@@ -30,6 +30,8 @@ export {useConfirm} from './Dialog/ConfirmationDialog'
|
|
30
30
|
export {ActionList} from './ActionList'
|
31
31
|
export {ActionMenu} from './ActionMenu'
|
32
32
|
export type {ActionMenuProps} from './ActionMenu'
|
33
|
+
export {default as Autocomplete} from './Autocomplete'
|
34
|
+
export type {AutocompleteMenuProps, AutocompleteInputProps, AutocompleteOverlayProps} from './Autocomplete'
|
33
35
|
export {default as Avatar} from './Avatar'
|
34
36
|
export type {AvatarProps} from './Avatar'
|
35
37
|
export {default as AvatarPair} from './AvatarPair'
|
@@ -0,0 +1,572 @@
|
|
1
|
+
import React, {ChangeEventHandler, RefObject, useCallback, useRef, useState} from 'react'
|
2
|
+
import {Meta} from '@storybook/react'
|
3
|
+
|
4
|
+
import {BaseStyles, Box, Text, TextInput, ThemeProvider} from '..'
|
5
|
+
import TextInputTokens from '../TextInputWithTokens'
|
6
|
+
import Autocomplete from '../Autocomplete/Autocomplete'
|
7
|
+
import {AnchoredOverlay} from '../AnchoredOverlay'
|
8
|
+
import {ButtonInvisible} from '../Button'
|
9
|
+
|
10
|
+
type ItemMetadata = {
|
11
|
+
fillColor: React.CSSProperties['backgroundColor']
|
12
|
+
}
|
13
|
+
|
14
|
+
type Datum = {
|
15
|
+
id: string | number
|
16
|
+
text: string
|
17
|
+
selected?: boolean
|
18
|
+
metadata?: ItemMetadata
|
19
|
+
}
|
20
|
+
|
21
|
+
const items: Datum[] = [
|
22
|
+
{text: 'zero', id: 0},
|
23
|
+
{text: 'one', id: 1},
|
24
|
+
{text: 'two', id: 2},
|
25
|
+
{text: 'three', id: 3},
|
26
|
+
{text: 'four', id: 4},
|
27
|
+
{text: 'five', id: 5},
|
28
|
+
{text: 'six', id: 6},
|
29
|
+
{text: 'seven', id: 7},
|
30
|
+
{text: 'twenty', id: 20},
|
31
|
+
{text: 'twentyone', id: 21}
|
32
|
+
]
|
33
|
+
|
34
|
+
const mockTokens: Datum[] = [
|
35
|
+
{text: 'zero', id: 0},
|
36
|
+
{text: 'one', id: 1},
|
37
|
+
{text: 'three', id: 3},
|
38
|
+
{text: 'four', id: 4}
|
39
|
+
]
|
40
|
+
|
41
|
+
export default {
|
42
|
+
title: 'Forms/Autocomplete',
|
43
|
+
|
44
|
+
decorators: [
|
45
|
+
Story => {
|
46
|
+
const [lastKey, setLastKey] = useState('none')
|
47
|
+
const reportKey = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
48
|
+
setLastKey(event.key)
|
49
|
+
}, [])
|
50
|
+
|
51
|
+
return (
|
52
|
+
<ThemeProvider>
|
53
|
+
<BaseStyles>
|
54
|
+
<Box onKeyDownCapture={reportKey}>
|
55
|
+
<Box position="absolute" right={5} top={2}>
|
56
|
+
Last key pressed: {lastKey}
|
57
|
+
</Box>
|
58
|
+
<Box paddingTop={5}>
|
59
|
+
<Story />
|
60
|
+
</Box>
|
61
|
+
</Box>
|
62
|
+
</BaseStyles>
|
63
|
+
</ThemeProvider>
|
64
|
+
)
|
65
|
+
}
|
66
|
+
]
|
67
|
+
} as Meta
|
68
|
+
|
69
|
+
export const SingleSelect = () => {
|
70
|
+
return (
|
71
|
+
<>
|
72
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
73
|
+
Pick an option
|
74
|
+
</Box>
|
75
|
+
<Autocomplete>
|
76
|
+
<Autocomplete.Input id="autocompleteInput" />
|
77
|
+
<Autocomplete.Overlay>
|
78
|
+
<Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
|
79
|
+
</Autocomplete.Overlay>
|
80
|
+
</Autocomplete>
|
81
|
+
</>
|
82
|
+
)
|
83
|
+
}
|
84
|
+
|
85
|
+
export const MultiSelect = () => {
|
86
|
+
const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>([])
|
87
|
+
const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
|
88
|
+
if (!Array.isArray(newlySelectedItems)) {
|
89
|
+
return
|
90
|
+
}
|
91
|
+
|
92
|
+
setSelectedItemIds(newlySelectedItems.map(item => item.id))
|
93
|
+
}
|
94
|
+
|
95
|
+
const getItemById = (id: string | number) => items.find(item => item.id === id)
|
96
|
+
|
97
|
+
return (
|
98
|
+
<Box display="flex" sx={{gap: '1em'}}>
|
99
|
+
<div>
|
100
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
101
|
+
Pick an option
|
102
|
+
</Box>
|
103
|
+
<Autocomplete>
|
104
|
+
<Autocomplete.Input id="autocompleteInput" />
|
105
|
+
<Autocomplete.Overlay>
|
106
|
+
<Autocomplete.Menu
|
107
|
+
items={items}
|
108
|
+
selectedItemIds={selectedItemIds}
|
109
|
+
aria-labelledby="autocompleteLabel"
|
110
|
+
onSelectedChange={onSelectedChange}
|
111
|
+
selectionVariant="multiple"
|
112
|
+
/>
|
113
|
+
</Autocomplete.Overlay>
|
114
|
+
</Autocomplete>
|
115
|
+
</div>
|
116
|
+
<div>
|
117
|
+
<div>Selected items:</div>
|
118
|
+
<Box as="ul" my={0}>
|
119
|
+
{selectedItemIds.map(selectedItemId => (
|
120
|
+
<li key={selectedItemId}>{getItemById(selectedItemId)?.text}</li>
|
121
|
+
))}
|
122
|
+
</Box>
|
123
|
+
</div>
|
124
|
+
</Box>
|
125
|
+
)
|
126
|
+
}
|
127
|
+
|
128
|
+
export const MultiSelectWithTokenInput = () => {
|
129
|
+
const [tokens, setTokens] = useState<Datum[]>(mockTokens)
|
130
|
+
const selectedTokenIds = tokens.map(token => token.id)
|
131
|
+
const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>(selectedTokenIds)
|
132
|
+
const onTokenRemove: (tokenId: string | number) => void = tokenId => {
|
133
|
+
setTokens(tokens.filter(token => token.id !== tokenId))
|
134
|
+
setSelectedItemIds(selectedItemIds.filter(id => id !== tokenId))
|
135
|
+
}
|
136
|
+
const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
|
137
|
+
if (!Array.isArray(newlySelectedItems)) {
|
138
|
+
return
|
139
|
+
}
|
140
|
+
|
141
|
+
setSelectedItemIds(newlySelectedItems.map(item => item.id))
|
142
|
+
|
143
|
+
if (newlySelectedItems.length < selectedItemIds.length) {
|
144
|
+
const newlySelectedItemIds = newlySelectedItems.map(({id}) => id)
|
145
|
+
const removedItemIds = selectedTokenIds.filter(id => !newlySelectedItemIds.includes(id))
|
146
|
+
|
147
|
+
for (const removedItemId of removedItemIds) {
|
148
|
+
onTokenRemove(removedItemId)
|
149
|
+
}
|
150
|
+
|
151
|
+
return
|
152
|
+
}
|
153
|
+
|
154
|
+
setTokens(newlySelectedItems.map(({id, text}) => ({id, text})))
|
155
|
+
}
|
156
|
+
|
157
|
+
return (
|
158
|
+
<>
|
159
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
160
|
+
Pick options
|
161
|
+
</Box>
|
162
|
+
<Autocomplete>
|
163
|
+
<Autocomplete.Input as={TextInputTokens} tokens={tokens} onTokenRemove={onTokenRemove} id="autocompleteInput" />
|
164
|
+
<Autocomplete.Overlay>
|
165
|
+
<Autocomplete.Menu
|
166
|
+
items={items}
|
167
|
+
selectedItemIds={selectedItemIds}
|
168
|
+
onSelectedChange={onSelectedChange}
|
169
|
+
selectionVariant="multiple"
|
170
|
+
aria-labelledby="autocompleteLabel"
|
171
|
+
/>
|
172
|
+
</Autocomplete.Overlay>
|
173
|
+
</Autocomplete>
|
174
|
+
</>
|
175
|
+
)
|
176
|
+
}
|
177
|
+
|
178
|
+
export const MultiSelectAddNewItem = () => {
|
179
|
+
const [localItemsState, setLocalItemsState] = useState<Datum[]>(items)
|
180
|
+
const [filterVal, setFilterVal] = useState<string>('')
|
181
|
+
const [tokens, setTokens] = useState<Datum[]>(mockTokens)
|
182
|
+
const selectedTokenIds = tokens.map(token => token.id)
|
183
|
+
const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>(selectedTokenIds)
|
184
|
+
const onTokenRemove: (tokenId: string | number) => void = tokenId => {
|
185
|
+
setTokens(tokens.filter(token => token.id !== tokenId))
|
186
|
+
setSelectedItemIds(selectedItemIds.filter(id => id !== tokenId))
|
187
|
+
}
|
188
|
+
const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
|
189
|
+
if (!Array.isArray(newlySelectedItems)) {
|
190
|
+
return
|
191
|
+
}
|
192
|
+
|
193
|
+
setSelectedItemIds(newlySelectedItems.map(item => item.id))
|
194
|
+
|
195
|
+
if (newlySelectedItems.length < selectedItemIds.length) {
|
196
|
+
const newlySelectedItemIds = newlySelectedItems.map(({id}) => id)
|
197
|
+
const removedItemIds = selectedTokenIds.filter(id => !newlySelectedItemIds.includes(id))
|
198
|
+
|
199
|
+
for (const removedItemId of removedItemIds) {
|
200
|
+
onTokenRemove(removedItemId)
|
201
|
+
}
|
202
|
+
|
203
|
+
return
|
204
|
+
}
|
205
|
+
|
206
|
+
setTokens(newlySelectedItems.map(({id, text}) => ({id, text})))
|
207
|
+
}
|
208
|
+
|
209
|
+
const onItemSelect: (item: Datum) => void = item => {
|
210
|
+
onSelectedChange([...selectedItemIds.map(id => items.find(selectedItem => selectedItem.id === id) as Datum), item])
|
211
|
+
|
212
|
+
if (!localItemsState.some(localItem => localItem.id === item.id)) {
|
213
|
+
setLocalItemsState([...localItemsState, item])
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
const handleChange: ChangeEventHandler<HTMLInputElement> = e => {
|
218
|
+
setFilterVal(e.currentTarget.value)
|
219
|
+
}
|
220
|
+
|
221
|
+
return (
|
222
|
+
<>
|
223
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
224
|
+
Pick options
|
225
|
+
</Box>
|
226
|
+
<Autocomplete>
|
227
|
+
<Autocomplete.Input
|
228
|
+
as={TextInputTokens}
|
229
|
+
tokens={tokens}
|
230
|
+
onTokenRemove={onTokenRemove}
|
231
|
+
onChange={handleChange}
|
232
|
+
id="autocompleteInput"
|
233
|
+
/>
|
234
|
+
<Autocomplete.Overlay>
|
235
|
+
<Autocomplete.Menu
|
236
|
+
addNewItem={
|
237
|
+
filterVal && !localItemsState.map(localItem => localItem.text).includes(filterVal)
|
238
|
+
? {
|
239
|
+
text: `Add '${filterVal}'`,
|
240
|
+
handleAddItem: item => {
|
241
|
+
onItemSelect({
|
242
|
+
...item,
|
243
|
+
text: filterVal,
|
244
|
+
selected: true
|
245
|
+
})
|
246
|
+
setFilterVal('')
|
247
|
+
}
|
248
|
+
}
|
249
|
+
: undefined
|
250
|
+
}
|
251
|
+
items={localItemsState}
|
252
|
+
selectedItemIds={selectedItemIds}
|
253
|
+
onSelectedChange={onSelectedChange}
|
254
|
+
selectionVariant="multiple"
|
255
|
+
aria-labelledby="autocompleteLabel"
|
256
|
+
/>
|
257
|
+
</Autocomplete.Overlay>
|
258
|
+
</Autocomplete>
|
259
|
+
</>
|
260
|
+
)
|
261
|
+
}
|
262
|
+
|
263
|
+
export const CustomEmptyStateMessage = () => {
|
264
|
+
return (
|
265
|
+
<>
|
266
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
267
|
+
Pick an option
|
268
|
+
</Box>
|
269
|
+
<Autocomplete>
|
270
|
+
<Autocomplete.Input id="autocompleteInput" />
|
271
|
+
<Autocomplete.Overlay>
|
272
|
+
<Autocomplete.Menu
|
273
|
+
items={items}
|
274
|
+
selectedItemIds={[]}
|
275
|
+
aria-labelledby="autocompleteLabel"
|
276
|
+
emptyStateText="Sorry, no matches"
|
277
|
+
/>
|
278
|
+
</Autocomplete.Overlay>
|
279
|
+
</Autocomplete>
|
280
|
+
</>
|
281
|
+
)
|
282
|
+
}
|
283
|
+
|
284
|
+
export const CustomSearchFilter = () => {
|
285
|
+
const [filterVal, setFilterVal] = useState<string>('')
|
286
|
+
const handleChange: ChangeEventHandler<HTMLInputElement> = e => {
|
287
|
+
setFilterVal(e.currentTarget.value)
|
288
|
+
}
|
289
|
+
const customFilterFn = (item: Datum) => item.text.includes(filterVal)
|
290
|
+
|
291
|
+
return (
|
292
|
+
<>
|
293
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
294
|
+
Pick an option
|
295
|
+
</Box>
|
296
|
+
<Autocomplete>
|
297
|
+
<Autocomplete.Input id="autocompleteInput" onChange={handleChange} />
|
298
|
+
<Autocomplete.Overlay>
|
299
|
+
<Autocomplete.Menu
|
300
|
+
items={items}
|
301
|
+
selectedItemIds={[]}
|
302
|
+
aria-labelledby="autocompleteLabel"
|
303
|
+
filterFn={customFilterFn}
|
304
|
+
/>
|
305
|
+
</Autocomplete.Overlay>
|
306
|
+
</Autocomplete>
|
307
|
+
<Text fontSize={0} display="block" color="fg.subtle" mt={2}>
|
308
|
+
Items in dropdown are filtered if their text has no part that matches the input value
|
309
|
+
</Text>
|
310
|
+
</>
|
311
|
+
)
|
312
|
+
}
|
313
|
+
|
314
|
+
export const CustomSortAfterMenuClose = () => {
|
315
|
+
const [selectedItemIds, setSelectedItemIds] = useState<Array<string | number>>([])
|
316
|
+
const isItemSelected = (itemId: string | number) => selectedItemIds.includes(itemId)
|
317
|
+
const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
|
318
|
+
if (!Array.isArray(newlySelectedItems)) {
|
319
|
+
return
|
320
|
+
}
|
321
|
+
|
322
|
+
setSelectedItemIds(newlySelectedItems.map(item => item.id))
|
323
|
+
}
|
324
|
+
const customSortFn = (itemIdA: string | number, itemIdB: string | number) =>
|
325
|
+
isItemSelected(itemIdA) === isItemSelected(itemIdB) ? 0 : isItemSelected(itemIdA) ? 1 : -1
|
326
|
+
|
327
|
+
return (
|
328
|
+
<>
|
329
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
330
|
+
Pick an option
|
331
|
+
</Box>
|
332
|
+
<Autocomplete>
|
333
|
+
<Autocomplete.Input id="autocompleteInput" />
|
334
|
+
<Autocomplete.Overlay>
|
335
|
+
<Autocomplete.Menu
|
336
|
+
items={items}
|
337
|
+
selectedItemIds={selectedItemIds}
|
338
|
+
aria-labelledby="autocompleteLabel"
|
339
|
+
onSelectedChange={onSelectedChange}
|
340
|
+
sortOnCloseFn={customSortFn}
|
341
|
+
selectionVariant="multiple"
|
342
|
+
/>
|
343
|
+
</Autocomplete.Overlay>
|
344
|
+
</Autocomplete>
|
345
|
+
<Text fontSize={0} display="block" color="fg.subtle" mt={2}>
|
346
|
+
When the dropdown closes, selected items are sorted to the end
|
347
|
+
</Text>
|
348
|
+
</>
|
349
|
+
)
|
350
|
+
}
|
351
|
+
|
352
|
+
export const WithCallbackWhenOverlayOpenStateChanges = () => {
|
353
|
+
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
|
354
|
+
const onOpenChange = (isOpen: boolean) => {
|
355
|
+
setIsMenuOpen(isOpen)
|
356
|
+
}
|
357
|
+
|
358
|
+
return (
|
359
|
+
<Box display="flex" sx={{gap: '1em'}}>
|
360
|
+
<div>
|
361
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
362
|
+
Pick an option
|
363
|
+
</Box>
|
364
|
+
<Autocomplete>
|
365
|
+
<Autocomplete.Input id="autocompleteInput" />
|
366
|
+
<Autocomplete.Overlay>
|
367
|
+
<Autocomplete.Menu
|
368
|
+
items={items}
|
369
|
+
selectedItemIds={[]}
|
370
|
+
aria-labelledby="autocompleteLabel"
|
371
|
+
onOpenChange={onOpenChange}
|
372
|
+
/>
|
373
|
+
</Autocomplete.Overlay>
|
374
|
+
</Autocomplete>
|
375
|
+
</div>
|
376
|
+
<div>
|
377
|
+
The menu is <strong>{isMenuOpen ? 'opened' : 'closed'}</strong>
|
378
|
+
</div>
|
379
|
+
</Box>
|
380
|
+
)
|
381
|
+
}
|
382
|
+
|
383
|
+
export const AsyncLoadingOfItems = () => {
|
384
|
+
const [loadedItems, setLoadedItems] = useState<Datum[]>([])
|
385
|
+
const onOpenChange = () => {
|
386
|
+
setTimeout(() => {
|
387
|
+
setLoadedItems(items)
|
388
|
+
}, 1500)
|
389
|
+
}
|
390
|
+
|
391
|
+
return (
|
392
|
+
<>
|
393
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
394
|
+
Pick an option
|
395
|
+
</Box>
|
396
|
+
<Autocomplete>
|
397
|
+
<Autocomplete.Input id="autocompleteInput" />
|
398
|
+
<Autocomplete.Overlay>
|
399
|
+
<Autocomplete.Menu
|
400
|
+
items={loadedItems}
|
401
|
+
selectedItemIds={[]}
|
402
|
+
aria-labelledby="autocompleteLabel"
|
403
|
+
onOpenChange={onOpenChange}
|
404
|
+
loading={loadedItems.length === 0}
|
405
|
+
/>
|
406
|
+
</Autocomplete.Overlay>
|
407
|
+
</Autocomplete>
|
408
|
+
</>
|
409
|
+
)
|
410
|
+
}
|
411
|
+
|
412
|
+
export const RenderingTheMenuOutsideAnOverlay = () => {
|
413
|
+
return (
|
414
|
+
<>
|
415
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
416
|
+
Pick an option
|
417
|
+
</Box>
|
418
|
+
<Autocomplete>
|
419
|
+
<Autocomplete.Input id="autocompleteInput" />
|
420
|
+
<Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
|
421
|
+
</Autocomplete>
|
422
|
+
</>
|
423
|
+
)
|
424
|
+
}
|
425
|
+
|
426
|
+
export const CustomOverlayMenuAnchor = () => {
|
427
|
+
const menuAnchorRef = useRef<HTMLElement>(null)
|
428
|
+
const anchorWrapperStyles = {
|
429
|
+
display: 'flex',
|
430
|
+
alignItems: 'center',
|
431
|
+
flexGrow: 1,
|
432
|
+
flexShrink: 0,
|
433
|
+
flexBasis: '25%',
|
434
|
+
border: '1px solid black',
|
435
|
+
padding: '1em'
|
436
|
+
}
|
437
|
+
|
438
|
+
return (
|
439
|
+
<>
|
440
|
+
<Box as="label" htmlFor="autocompleteInput" id="autocompleteLabel">
|
441
|
+
Pick labels
|
442
|
+
</Box>
|
443
|
+
<Box {...anchorWrapperStyles} ref={menuAnchorRef as React.RefObject<HTMLDivElement>}>
|
444
|
+
<Autocomplete>
|
445
|
+
<Autocomplete.Input
|
446
|
+
as={TextInput}
|
447
|
+
id="autocompleteInput"
|
448
|
+
sx={{
|
449
|
+
border: '0',
|
450
|
+
padding: '0',
|
451
|
+
boxShadow: 'none',
|
452
|
+
':focus-within': {
|
453
|
+
border: '0',
|
454
|
+
boxShadow: 'none'
|
455
|
+
}
|
456
|
+
}}
|
457
|
+
/>
|
458
|
+
<Autocomplete.Overlay menuAnchorRef={menuAnchorRef}>
|
459
|
+
<Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
|
460
|
+
</Autocomplete.Overlay>
|
461
|
+
</Autocomplete>
|
462
|
+
</Box>
|
463
|
+
<Text fontSize={0} display="block" color="fg.subtle" mt={2}>
|
464
|
+
The overlay menu's position is anchored to the div with the black border instead of to the text input
|
465
|
+
</Text>
|
466
|
+
</>
|
467
|
+
)
|
468
|
+
}
|
469
|
+
|
470
|
+
export const WithCustomOverlayProps = () => {
|
471
|
+
return (
|
472
|
+
<>
|
473
|
+
<Box as="label" display="block" htmlFor="autocompleteInput" id="autocompleteLabel">
|
474
|
+
Pick an option
|
475
|
+
</Box>
|
476
|
+
<Autocomplete>
|
477
|
+
<Autocomplete.Input id="autocompleteInput" />
|
478
|
+
<Autocomplete.Overlay
|
479
|
+
overlayProps={{
|
480
|
+
width: 'large',
|
481
|
+
height: 'xsmall'
|
482
|
+
}}
|
483
|
+
>
|
484
|
+
<Autocomplete.Menu items={items} selectedItemIds={[]} aria-labelledby="autocompleteLabel" />
|
485
|
+
</Autocomplete.Overlay>
|
486
|
+
</Autocomplete>
|
487
|
+
</>
|
488
|
+
)
|
489
|
+
}
|
490
|
+
|
491
|
+
export const InOverlayWithCustomScrollContainerRef = () => {
|
492
|
+
const scrollContainerRef = useRef<HTMLElement>(null)
|
493
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
494
|
+
|
495
|
+
const [isOpen, setIsOpen] = useState(false)
|
496
|
+
const handleOpen = () => {
|
497
|
+
setIsOpen(true)
|
498
|
+
inputRef.current && inputRef.current.focus()
|
499
|
+
}
|
500
|
+
|
501
|
+
return (
|
502
|
+
<AnchoredOverlay
|
503
|
+
open={isOpen}
|
504
|
+
onOpen={handleOpen}
|
505
|
+
onClose={() => setIsOpen(false)}
|
506
|
+
width="large"
|
507
|
+
height="xsmall"
|
508
|
+
focusTrapSettings={{initialFocusRef: inputRef}}
|
509
|
+
side="inside-top"
|
510
|
+
renderAnchor={props => <ButtonInvisible {...props}>open overlay</ButtonInvisible>}
|
511
|
+
>
|
512
|
+
<Box
|
513
|
+
as="label"
|
514
|
+
display="block"
|
515
|
+
htmlFor="autocompleteInput"
|
516
|
+
id="autocompleteLabel"
|
517
|
+
sx={{
|
518
|
+
// visually hides this label for sighted users
|
519
|
+
position: 'absolute',
|
520
|
+
width: '1px',
|
521
|
+
height: '1px',
|
522
|
+
padding: '0',
|
523
|
+
margin: '-1px',
|
524
|
+
overflow: 'hidden',
|
525
|
+
clip: 'rect(0, 0, 0, 0)',
|
526
|
+
whiteSpace: 'nowrap',
|
527
|
+
borderWidth: '0'
|
528
|
+
}}
|
529
|
+
>
|
530
|
+
Pick options
|
531
|
+
</Box>
|
532
|
+
<Autocomplete>
|
533
|
+
<Box display="flex" flexDirection="column" height="100%">
|
534
|
+
<Box
|
535
|
+
paddingX="3"
|
536
|
+
paddingY="1"
|
537
|
+
borderWidth={0}
|
538
|
+
borderBottomWidth={1}
|
539
|
+
borderColor="border.default"
|
540
|
+
borderStyle="solid"
|
541
|
+
>
|
542
|
+
<Autocomplete.Input
|
543
|
+
block
|
544
|
+
as={TextInput}
|
545
|
+
ref={inputRef}
|
546
|
+
id="autocompleteInput"
|
547
|
+
sx={{
|
548
|
+
display: 'flex',
|
549
|
+
border: '0',
|
550
|
+
padding: '0',
|
551
|
+
boxShadow: 'none',
|
552
|
+
':focus-within': {
|
553
|
+
border: '0',
|
554
|
+
boxShadow: 'none'
|
555
|
+
}
|
556
|
+
}}
|
557
|
+
/>
|
558
|
+
</Box>
|
559
|
+
<Box overflow="auto" flexGrow={1} ref={scrollContainerRef as RefObject<HTMLDivElement>}>
|
560
|
+
<Autocomplete.Menu
|
561
|
+
items={items}
|
562
|
+
selectedItemIds={[]}
|
563
|
+
// onSelectedChange={onSelectedChange}
|
564
|
+
customScrollContainerRef={scrollContainerRef}
|
565
|
+
aria-labelledby="autocompleteLabel"
|
566
|
+
/>
|
567
|
+
</Box>
|
568
|
+
</Box>
|
569
|
+
</Autocomplete>
|
570
|
+
</AnchoredOverlay>
|
571
|
+
)
|
572
|
+
}
|
@@ -150,7 +150,7 @@ function CustomHeader({
|
|
150
150
|
}, [onClose])
|
151
151
|
if (typeof title === 'string' && typeof subtitle === 'string') {
|
152
152
|
return (
|
153
|
-
<Box bg="
|
153
|
+
<Box bg="accent.subtle">
|
154
154
|
<h1 id={dialogLabelId}>{title.toUpperCase()}</h1>
|
155
155
|
<h2 id={dialogDescriptionId}>{subtitle.toLowerCase()}</h2>
|
156
156
|
<Dialog.CloseButton onClose={onCloseClick} />
|
@@ -160,11 +160,11 @@ function CustomHeader({
|
|
160
160
|
return null
|
161
161
|
}
|
162
162
|
function CustomBody({children}: React.PropsWithChildren<DialogProps>) {
|
163
|
-
return <Dialog.Body bg="
|
163
|
+
return <Dialog.Body bg="danger.subtle">{children}</Dialog.Body>
|
164
164
|
}
|
165
165
|
function CustomFooter({footerButtons}: React.PropsWithChildren<DialogProps>) {
|
166
166
|
return (
|
167
|
-
<Dialog.Footer bg="
|
167
|
+
<Dialog.Footer bg="attention.subtle">
|
168
168
|
{footerButtons ? <Dialog.Buttons buttons={footerButtons} /> : null}
|
169
169
|
</Dialog.Footer>
|
170
170
|
)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
/*
|
2
|
+
Used to convert a list of properties in a type from optional to required
|
3
|
+
|
4
|
+
For example, we could make a new type from `Datum`
|
5
|
+
where 'id' and 'label' required:
|
6
|
+
type Datum = {
|
7
|
+
description?: string
|
8
|
+
id?: string
|
9
|
+
label?: string
|
10
|
+
value: string
|
11
|
+
}
|
12
|
+
|
13
|
+
type DatumWithRequiredIdAndLabel = MandateProps<Datum, 'id' | 'label'>
|
14
|
+
*/
|
15
|
+
|
16
|
+
export type MandateProps<T extends unknown, K extends keyof T> = Omit<T, K> &
|
17
|
+
{
|
18
|
+
[MK in K]-?: NonNullable<T[MK]>
|
19
|
+
}
|
package/src/utils/types/index.ts
CHANGED