Skip to content

Commit acc5d27

Browse files
authored
Add Notice to SelectPanel (#5790)
1 parent fbc6f97 commit acc5d27

File tree

4 files changed

+152
-1
lines changed

4 files changed

+152
-1
lines changed

.changeset/slick-teams-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Add Notice to SelectPanel

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ import {Button} from '../Button'
55
import type {ItemInput, GroupedListProps} from '../deprecated/ActionList/List'
66
import {SelectPanel, type SelectPanelProps} from './SelectPanel'
77
import {
8+
AlertIcon,
89
FilterIcon,
910
GearIcon,
11+
InfoIcon,
1012
NoteIcon,
1113
ProjectIcon,
1214
SearchIcon,
15+
StopIcon,
1316
TriangleDownIcon,
1417
TypographyIcon,
1518
VersionsIcon,
1619
} from '@primer/octicons-react'
1720
import useSafeTimeout from '../hooks/useSafeTimeout'
1821
import FormControl from '../FormControl'
22+
import Link from '../Link'
23+
import {SegmentedControl} from '../SegmentedControl'
24+
import {Stack} from '../Stack'
1925

2026
const meta = {
2127
title: 'Components/SelectPanel/Features',
@@ -312,6 +318,92 @@ export const WithFooter = () => {
312318
)
313319
}
314320

321+
export const WithNotice = () => {
322+
const [selected, setSelected] = useState<ItemInput[]>(items.slice(1, 3))
323+
const [filter, setFilter] = useState('')
324+
const filteredItems = items.filter(
325+
item =>
326+
// design guidelines say to always show selected items in the list
327+
selected.some(selectedItem => selectedItem.text === item.text) ||
328+
// then filter the rest
329+
item.text.toLowerCase().startsWith(filter.toLowerCase()),
330+
)
331+
// design guidelines say to sort selected items first
332+
const selectedItemsSortedFirst = filteredItems.sort((a, b) => {
333+
const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text)
334+
const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text)
335+
if (aIsSelected && !bIsSelected) return -1
336+
if (!aIsSelected && bIsSelected) return 1
337+
return 0
338+
})
339+
const [open, setOpen] = useState(false)
340+
const [noticeVariant, setNoticeVariant] = useState(0)
341+
342+
const noticeVariants: Array<{text: string | React.ReactElement; variant: 'info' | 'warning' | 'error'}> = [
343+
{
344+
variant: 'info',
345+
text: 'Try a different search term.',
346+
},
347+
{
348+
variant: 'warning',
349+
text: (
350+
<>
351+
You have reached the limit of assignees on your free account.{' '}
352+
<Link href="/upgrade">Upgrade your account.</Link>
353+
</>
354+
),
355+
},
356+
{
357+
variant: 'error',
358+
text: (
359+
<>
360+
We couldn&apos;t load all collaborators. Try again or if the problem persists,{' '}
361+
<Link href="/support">contact support</Link>
362+
</>
363+
),
364+
},
365+
]
366+
367+
return (
368+
<Stack align="start">
369+
<FormControl>
370+
<FormControl.Label>Notice variant</FormControl.Label>
371+
<SegmentedControl aria-label="Notice variant" onChange={setNoticeVariant}>
372+
<SegmentedControl.Button defaultSelected aria-label={'Info'} leadingIcon={InfoIcon}>
373+
Info notice
374+
</SegmentedControl.Button>
375+
<SegmentedControl.Button aria-label={'Warning'} leadingIcon={AlertIcon}>
376+
Warning notice
377+
</SegmentedControl.Button>
378+
<SegmentedControl.Button aria-label={'Error'} leadingIcon={StopIcon}>
379+
Error notice
380+
</SegmentedControl.Button>
381+
</SegmentedControl>
382+
</FormControl>
383+
<FormControl>
384+
<FormControl.Label>SelectPanel with notice</FormControl.Label>
385+
<SelectPanel
386+
renderAnchor={({children, ...anchorProps}) => (
387+
<Button trailingAction={TriangleDownIcon} {...anchorProps}>
388+
{children}
389+
</Button>
390+
)}
391+
placeholder="Select labels" // button text when no items are selected
392+
open={open}
393+
onOpenChange={setOpen}
394+
items={selectedItemsSortedFirst}
395+
selected={selected}
396+
onSelectedChange={setSelected}
397+
onFilterChange={setFilter}
398+
overlayProps={{width: 'small', height: 'medium'}}
399+
width="medium"
400+
notice={noticeVariants[noticeVariant]}
401+
/>
402+
</FormControl>
403+
</Stack>
404+
)
405+
}
406+
315407
const listOfItems: Array<ItemInput> = [
316408
{
317409
id: '1',

packages/react/src/SelectPanel/SelectPanel.module.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,43 @@
2525
color: var(--fgColor-muted);
2626
}
2727

28+
.Notice {
29+
display: flex;
30+
padding-top: var(--base-size-12);
31+
padding-right: var(--base-size-16);
32+
padding-bottom: var(--base-size-12);
33+
padding-left: var(--base-size-16);
34+
margin-top: var(--base-size-4);
35+
font-size: var(--text-body-size-small);
36+
flex-direction: row;
37+
border-top: var(--borderWidth-thin) solid;
38+
border-bottom: var(--borderWidth-thin) solid;
39+
gap: var(--base-size-8);
40+
}
41+
42+
.Notice a {
43+
color: inherit;
44+
text-decoration: underline;
45+
}
46+
47+
.Notice:where([data-variant='info']) {
48+
color: var(--fgColor-accent);
49+
background-color: var(--bgColor-accent-muted);
50+
border-color: var(--borderColor-accent-muted);
51+
}
52+
53+
.Notice:where([data-variant='warning']) {
54+
color: var(--fgColor-attention);
55+
background-color: var(--bgColor-attention-muted);
56+
border-color: var(--borderColor-attention-muted);
57+
}
58+
59+
.Notice:where([data-variant='error']) {
60+
color: var(--fgColor-danger);
61+
background-color: var(--bgColor-danger-muted);
62+
border-color: var(--borderColor-danger-muted);
63+
}
64+
2865
.Footer {
2966
display: flex;
3067
padding: var(--base-size-8);

packages/react/src/SelectPanel/SelectPanel.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {SearchIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react'
1+
import {AlertIcon, InfoIcon, SearchIcon, StopIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react'
22
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
33
import type {AnchoredOverlayProps} from '../AnchoredOverlay'
44
import {AnchoredOverlay} from '../AnchoredOverlay'
@@ -127,6 +127,10 @@ interface SelectPanelBaseProps {
127127
footer?: string | React.ReactElement
128128
initialLoadingType?: InitialLoadingType
129129
className?: string
130+
notice?: {
131+
text: string | React.ReactElement
132+
variant: 'info' | 'warning' | 'error'
133+
}
130134
onCancel?: () => void
131135
}
132136

@@ -189,6 +193,7 @@ export function SelectPanel({
189193
height,
190194
width,
191195
id,
196+
notice,
192197
onCancel,
193198
...listProps
194199
}: SelectPanelProps): JSX.Element {
@@ -436,6 +441,12 @@ export function SelectPanel({
436441
}
437442
const usingModernActionList = useFeatureFlag('primer_react_select_panel_with_modern_action_list')
438443

444+
const iconForNoticeVariant = {
445+
info: <InfoIcon size={16} />,
446+
warning: <AlertIcon size={16} />,
447+
error: <StopIcon size={16} />,
448+
}
449+
439450
return (
440451
<LiveRegion>
441452
<AnchoredOverlay
@@ -523,6 +534,12 @@ export function SelectPanel({
523534
/>
524535
)}
525536
</Box>
537+
{notice && (
538+
<div aria-live="polite" data-variant={notice.variant} className={classes.Notice}>
539+
{iconForNoticeVariant[notice.variant]}
540+
<div>{notice.text}</div>
541+
</div>
542+
)}
526543
<FilteredActionList
527544
filterValue={filterValue}
528545
onFilterChange={onFilterChange}

0 commit comments

Comments
 (0)