diff --git a/.changeset/slick-teams-check.md b/.changeset/slick-teams-check.md new file mode 100644 index 00000000000..c265f8ac6b4 --- /dev/null +++ b/.changeset/slick-teams-check.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add Notice to SelectPanel diff --git a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx index d1dfc3751ce..637340a5268 100644 --- a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx @@ -5,17 +5,23 @@ import {Button} from '../Button' import type {ItemInput, GroupedListProps} from '../deprecated/ActionList/List' import {SelectPanel, type SelectPanelProps} from './SelectPanel' import { + AlertIcon, FilterIcon, GearIcon, + InfoIcon, NoteIcon, ProjectIcon, SearchIcon, + StopIcon, TriangleDownIcon, TypographyIcon, VersionsIcon, } from '@primer/octicons-react' import useSafeTimeout from '../hooks/useSafeTimeout' import FormControl from '../FormControl' +import Link from '../Link' +import {SegmentedControl} from '../SegmentedControl' +import {Stack} from '../Stack' const meta = { title: 'Components/SelectPanel/Features', @@ -312,6 +318,92 @@ export const WithFooter = () => { ) } +export const WithNotice = () => { + const [selected, setSelected] = useState(items.slice(1, 3)) + const [filter, setFilter] = useState('') + const filteredItems = items.filter( + item => + // design guidelines say to always show selected items in the list + selected.some(selectedItem => selectedItem.text === item.text) || + // then filter the rest + item.text.toLowerCase().startsWith(filter.toLowerCase()), + ) + // design guidelines say to sort selected items first + const selectedItemsSortedFirst = filteredItems.sort((a, b) => { + const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) + const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) + if (aIsSelected && !bIsSelected) return -1 + if (!aIsSelected && bIsSelected) return 1 + return 0 + }) + const [open, setOpen] = useState(false) + const [noticeVariant, setNoticeVariant] = useState(0) + + const noticeVariants: Array<{text: string | React.ReactElement; variant: 'info' | 'warning' | 'error'}> = [ + { + variant: 'info', + text: 'Try a different search term.', + }, + { + variant: 'warning', + text: ( + <> + You have reached the limit of assignees on your free account.{' '} + Upgrade your account. + + ), + }, + { + variant: 'error', + text: ( + <> + We couldn't load all collaborators. Try again or if the problem persists,{' '} + contact support + + ), + }, + ] + + return ( + + + Notice variant + + + Info notice + + + Warning notice + + + Error notice + + + + + SelectPanel with notice + ( + + )} + placeholder="Select labels" // button text when no items are selected + open={open} + onOpenChange={setOpen} + items={selectedItemsSortedFirst} + selected={selected} + onSelectedChange={setSelected} + onFilterChange={setFilter} + overlayProps={{width: 'small', height: 'medium'}} + width="medium" + notice={noticeVariants[noticeVariant]} + /> + + + ) +} + const listOfItems: Array = [ { id: '1', diff --git a/packages/react/src/SelectPanel/SelectPanel.module.css b/packages/react/src/SelectPanel/SelectPanel.module.css index fef33c5de77..9b3acb44a17 100644 --- a/packages/react/src/SelectPanel/SelectPanel.module.css +++ b/packages/react/src/SelectPanel/SelectPanel.module.css @@ -25,6 +25,43 @@ color: var(--fgColor-muted); } +.Notice { + display: flex; + padding-top: var(--base-size-12); + padding-right: var(--base-size-16); + padding-bottom: var(--base-size-12); + padding-left: var(--base-size-16); + margin-top: var(--base-size-4); + font-size: var(--text-body-size-small); + flex-direction: row; + border-top: var(--borderWidth-thin) solid; + border-bottom: var(--borderWidth-thin) solid; + gap: var(--base-size-8); +} + +.Notice a { + color: inherit; + text-decoration: underline; +} + +.Notice:where([data-variant='info']) { + color: var(--fgColor-accent); + background-color: var(--bgColor-accent-muted); + border-color: var(--borderColor-accent-muted); +} + +.Notice:where([data-variant='warning']) { + color: var(--fgColor-attention); + background-color: var(--bgColor-attention-muted); + border-color: var(--borderColor-attention-muted); +} + +.Notice:where([data-variant='error']) { + color: var(--fgColor-danger); + background-color: var(--bgColor-danger-muted); + border-color: var(--borderColor-danger-muted); +} + .Footer { display: flex; padding: var(--base-size-8); diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index bc729f2c7ae..87755a2b0e3 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -1,4 +1,4 @@ -import {SearchIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react' +import {AlertIcon, InfoIcon, SearchIcon, StopIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react' import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' @@ -127,6 +127,10 @@ interface SelectPanelBaseProps { footer?: string | React.ReactElement initialLoadingType?: InitialLoadingType className?: string + notice?: { + text: string | React.ReactElement + variant: 'info' | 'warning' | 'error' + } onCancel?: () => void } @@ -189,6 +193,7 @@ export function SelectPanel({ height, width, id, + notice, onCancel, ...listProps }: SelectPanelProps): JSX.Element { @@ -436,6 +441,12 @@ export function SelectPanel({ } const usingModernActionList = useFeatureFlag('primer_react_select_panel_with_modern_action_list') + const iconForNoticeVariant = { + info: , + warning: , + error: , + } + return ( )} + {notice && ( +
+ {iconForNoticeVariant[notice.variant]} +
{notice.text}
+
+ )}