Skip to content

Commit

Permalink
Explore SelectPanel.Messsage API and managing the visibility in the c…
Browse files Browse the repository at this point in the history
…omponent
  • Loading branch information
broccolinisoup committed Sep 19, 2024
1 parent 0d5c1ed commit 19daa03
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export const DefaultFeatureFlags = FeatureFlagScope.create({
primer_react_css_modules_staff: false,
primer_react_css_modules_ga: false,
primer_react_action_list_item_as_button: false,
primer_react_select_panel_with_modern_action_list: false,
primer_react_select_panel_with_modern_action_list: true,
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {useFeatureFlag} from '../FeatureFlags'

export function FilteredActionList(props: FilteredActionListProps): JSX.Element {
const enabled = useFeatureFlag('primer_react_select_panel_with_modern_action_list')

Check failure on line 8 in packages/react/src/FilteredActionList/FilteredActionListEntry.tsx

View workflow job for this annotation

GitHub Actions / lint

'enabled' is assigned a value but never used

if (enabled) return <WithStableActionList {...props} />
else return <WithDeprecatedActionList {...props} />
return <WithStableActionList {...props} />
// else return <WithDeprecatedActionList {...props} />
}

FilteredActionList.displayName = 'FilteredActionList'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface FilteredActionListProps
onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement> | null) => void
textInputProps?: Partial<Omit<TextInputProps, 'onChange'>>
inputRef?: React.RefObject<HTMLInputElement>
emptyState?: boolean
children?: React.ReactNode
}

const StyledHeader = styled.div`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {VisuallyHidden} from '../internal/components/VisuallyHidden'
import type {SxProp} from '../sx'
import type {FilteredActionListLoadingType} from './FilteredActionListLoaders'
import {FilteredActionListLoadingTypes, FilteredActionListBodyLoader} from './FilteredActionListLoaders'
import Text from '../Text'

Check failure on line 21 in packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx

View workflow job for this annotation

GitHub Actions / lint

'Text' is defined but never used

import {isValidElementType} from 'react-is'
import type {RenderItemFn} from '../deprecated/ActionList/List'
import {SelectPanelMessage} from '../SelectPanel/SelectPanel'

Check failure on line 25 in packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx

View workflow job for this annotation

GitHub Actions / lint

'SelectPanelMessage' is defined but never used

const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8}

Expand All @@ -35,6 +37,7 @@ export interface FilteredActionListProps
onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement>) => void
textInputProps?: Partial<Omit<TextInputProps, 'onChange'>>
inputRef?: React.RefObject<HTMLInputElement>
emptyState?: boolean
}

const StyledHeader = styled.div`
Expand All @@ -54,6 +57,8 @@ export function FilteredActionList({
sx,
groupMetadata,
showItemDividers,
emptyState,

Check failure on line 60 in packages/react/src/FilteredActionList/FilteredActionListWithModernActionList.tsx

View workflow job for this annotation

GitHub Actions / lint

'emptyState' is defined but never used. Allowed unused args must match /^_/u
message,
...listProps
}: FilteredActionListProps): JSX.Element {
const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '')
Expand Down Expand Up @@ -150,7 +155,7 @@ export function FilteredActionList({
/>
</StyledHeader>
<VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden>
<Box ref={scrollContainerRef} overflow="auto">
<Box ref={scrollContainerRef} sx={{overflow: 'auto', height: '100%'}}>
{loading && loadingType.appearsInBody ? (
<FilteredActionListBodyLoader loadingType={loadingType} />
) : (
Expand All @@ -173,6 +178,7 @@ export function FilteredActionList({
})}
</ActionList>
)}
{message}
</Box>
</Box>
)
Expand Down
70 changes: 70 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
VersionsIcon,
} from '@primer/octicons-react'
import useSafeTimeout from '../hooks/useSafeTimeout'
import Link from '../Link'
import Text from '../Text'

Check failure on line 19 in packages/react/src/SelectPanel/SelectPanel.features.stories.tsx

View workflow job for this annotation

GitHub Actions / lint

'Text' is defined but never used

const meta = {
title: 'Components/SelectPanel/Features',
Expand Down Expand Up @@ -422,3 +424,71 @@ export const AsyncFetch: StoryObj<typeof SelectPanel> = {
},
},
}

export const NoItems = () => {
const [selected, setSelected] = React.useState<ItemInput[]>([])
const [filteredItems, setFilteredItems] = React.useState<ItemInput[]>([])
const [open, setOpen] = useState(true)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onFilterChange = (value: string = '') => {
setTimeout(() => {
// fetch the items
setFilteredItems([])
}, 0)
}
return (
<SelectPanel
title="Set projects"
renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} aria-labelledby={` ${ariaLabelledBy}`} {...anchorProps}>
{children ?? 'Select Labels'}
</Button>
)}
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={onFilterChange}
overlayProps={{width: 'medium', height: 'large'}}
>
<SelectPanel.Message variant="noitems" title="You haven't created any projects yet">
<Link href="https://github.com/projects">Start your first project </Link> to organise your issues.
</SelectPanel.Message>
<SelectPanel.Message variant="nomatches" title={`No language found for `}>
Adjust your search term to find other languages
</SelectPanel.Message>
</SelectPanel>
)
}
export const NoMatches = () => {
const [selected, setSelected] = React.useState<ItemInput[]>([])
const [filter, setFilter] = React.useState<string>('')
const [open, setOpen] = useState(true)

const filteredItems = items.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
return (
<SelectPanel
title="Set projects"
renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} aria-labelledby={` ${ariaLabelledBy}`} {...anchorProps}>
{children ?? 'Select Labels'}
</Button>
)}
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
overlayProps={{width: 'medium', height: 'small'}}
>
<SelectPanel.Message variant="noitems" title="You haven't created any projects yet">
<Link href="https://github.com/projects">Start your first project </Link> to organise your issues.
</SelectPanel.Message>
<SelectPanel.Message variant="nomatches" title={`No language found for ${filter}`}>
Adjust your search term to find other languages
</SelectPanel.Message>
</SelectPanel>
)
}
76 changes: 67 additions & 9 deletions packages/react/src/SelectPanel/SelectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {AnchoredOverlayProps} from '../AnchoredOverlay'
import {AnchoredOverlay} from '../AnchoredOverlay'
import type {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay'
import Box from '../Box'
import Text from '../Text'
import type {FilteredActionListProps} from '../FilteredActionList'
import {FilteredActionList} from '../FilteredActionList'
import Heading from '../Heading'
Expand Down Expand Up @@ -49,11 +50,13 @@ interface SelectPanelBaseProps {
initialLoadingType?: InitialLoadingType
}

export type SelectPanelProps = SelectPanelBaseProps &
Omit<FilteredActionListProps, 'selectionVariant'> &
Pick<AnchoredOverlayProps, 'open'> &
AnchoredOverlayWrapperAnchorProps &
(SelectPanelSingleSelection | SelectPanelMultiSelection)
export type SelectPanelProps = React.PropsWithChildren<
SelectPanelBaseProps &
Omit<FilteredActionListProps, 'selectionVariant'> &
Pick<AnchoredOverlayProps, 'open'> &
AnchoredOverlayWrapperAnchorProps &
(SelectPanelSingleSelection | SelectPanelMultiSelection)
>

function isMultiSelectVariant(
selected: SelectPanelSingleSelection['selected'] | SelectPanelMultiSelection['selected'],
Expand All @@ -66,7 +69,40 @@ const focusZoneSettings: Partial<FocusZoneHookSettings> = {
disabled: true,
}

export function SelectPanel({
export type SelectPanelMessageProps = {
children: React.ReactNode
title: string
variant: 'noitems' | 'nomatches'
}
export const SelectPanelMessage: React.FC<SelectPanelMessageProps> = ({variant = 'noitems', title, children}) => {

Check failure on line 77 in packages/react/src/SelectPanel/SelectPanel.tsx

View workflow job for this annotation

GitHub Actions / lint

'variant' is assigned a value but never used
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
flexGrow: 1,
height: '100%',
gap: 1,
paddingX: 4,
textAlign: 'center',
a: {color: 'inherit', textDecoration: 'underline'},
minHeight: 'min(calc(var(--max-height) - 150px), 324px)',
// maxHeight of dialog - (header & footer)
}}
>
<Text sx={{fontSize: 1, fontWeight: 'semibold'}}>{title}</Text>
<Text
sx={{fontSize: 1, color: 'fg.muted', display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center'}}
>
{children}
</Text>
</Box>
)
}

function Panel({
open,
onOpenChange,
renderAnchor = props => {
Expand Down Expand Up @@ -94,6 +130,7 @@ export function SelectPanel({
sx,
loading,
initialLoadingType = 'spinner',
children,
...listProps
}: SelectPanelProps): JSX.Element {
const inputRef = React.useRef<HTMLInputElement>(null)
Expand Down Expand Up @@ -241,6 +278,19 @@ export function SelectPanel({
}
}

const isNoItemsState = items.length === 0 && dataLoadedOnce.current && !loading
const isNoMatchState = items.length === 0 && filterValue !== '' && dataLoadedOnce.current && !loading

const deconstructChildren = (children: React.ReactNode) => {
return React.Children.toArray(children).find(child => {
if (isNoMatchState) return child.props.variant === 'nomatches' && React.isValidElement(child)
else if (isNoItemsState) return child.props.variant === 'noitems' && React.isValidElement(child)
else return []
})
}

const message = deconstructChildren(children)

return (
<LiveRegion>
<AnchoredOverlay
Expand Down Expand Up @@ -279,6 +329,7 @@ export function SelectPanel({
</Box>
) : null}
</Box>

<FilteredActionList
filterValue={filterValue}
onFilterChange={onFilterChange}
Expand All @@ -295,11 +346,14 @@ export function SelectPanel({
inputRef={inputRef}
loading={isLoading}
loadingType={loadingType()}
emptyState={isNoItemsState}
message={message}
// inheriting height and maxHeight ensures that the FilteredActionList is never taller
// than the Overlay (which would break scrolling the items)
sx={{...sx, height: 'inherit', maxHeight: 'inherit'}}
/>
{footer && (

{footer && !isNoItemsState ? (
<Box
sx={{
display: 'flex',
Expand All @@ -310,11 +364,15 @@ export function SelectPanel({
>
{footer}
</Box>
)}
) : null}
</Box>
</AnchoredOverlay>
</LiveRegion>
)
}

SelectPanel.displayName = 'SelectPanel'
Panel.displayName = 'SelectPanel'

export const SelectPanel = Object.assign(Panel, {
Message: SelectPanelMessage,
})

0 comments on commit 19daa03

Please sign in to comment.