Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SelectPanel: Empty State - Explore SelectPanel.Message API and managing visibility of the component under the hood #4988

Closed
wants to merge 87 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
9cee0a4
super wip
broccolinisoup Jul 30, 2024
c7aa5de
just use the actionlist component and revert the type updates
broccolinisoup Jul 30, 2024
40a0c45
Update packages/react/src/FilteredActionList/FilteredActionList.tsx
broccolinisoup Jul 30, 2024
344f6ce
Some more progress
broccolinisoup Jul 31, 2024
fa5f5a6
revert the type changes and cast it :/
broccolinisoup Jul 31, 2024
3cd9ecc
clean
broccolinisoup Jul 31, 2024
0003e59
wip
broccolinisoup Aug 2, 2024
713b5ad
wip wip
broccolinisoup Aug 5, 2024
6fc7f2b
add stories
broccolinisoup Aug 8, 2024
a246981
fix linting
broccolinisoup Aug 8, 2024
e209f47
add tests for groups
broccolinisoup Aug 8, 2024
26cddc1
Merge branch 'main' into select-panel-action-list
broccolinisoup Aug 8, 2024
b1199fb
Merge branch 'select-panel-story-and-tests' into select-panel-action-…
broccolinisoup Aug 8, 2024
b669cff
Map groups
broccolinisoup Aug 8, 2024
1012662
Merge branch 'main' into select-panel-story-and-tests
broccolinisoup Aug 12, 2024
67dc5c9
Update story names for e2e tests
broccolinisoup Aug 12, 2024
c66feb5
oops remove unintended file
broccolinisoup Aug 12, 2024
4fbb3c5
update story name
broccolinisoup Aug 12, 2024
2d5e2b2
same - update story name
broccolinisoup Aug 12, 2024
cee7737
disable animations
broccolinisoup Aug 12, 2024
9c972c3
test(vrt): update snapshots
broccolinisoup Aug 12, 2024
17e4ca8
Merge branch 'select-panel-story-and-tests' into select-panel-action-…
broccolinisoup Aug 12, 2024
26b6fe6
Update tests since new action list has different semantics for group …
broccolinisoup Aug 12, 2024
42c50c0
Merge branch 'main' into select-panel-action-list
broccolinisoup Aug 13, 2024
0d0004f
logging
broccolinisoup Aug 14, 2024
3ba9bc7
pass the rest
broccolinisoup Aug 14, 2024
92ab9c2
extract children and use before text
broccolinisoup Aug 14, 2024
7b3d5e6
remove logging
broccolinisoup Aug 14, 2024
1cf1e09
Merge branch 'main' into select-panel-action-list
siddharthkp Aug 20, 2024
92303d0
test(vrt): update snapshots
siddharthkp Aug 20, 2024
4dc3b41
add active styles to ActionList
siddharthkp Aug 21, 2024
f673034
rename component name to be clearer
siddharthkp Aug 21, 2024
3a7d490
remove variant full from examples
siddharthkp Aug 21, 2024
ab3ea9c
tiny clean up
siddharthkp Aug 21, 2024
44ff862
fix showItemDividers
siddharthkp Aug 21, 2024
46c94e6
another tiny cleanup
siddharthkp Aug 21, 2024
ff8379b
pull MappedActionListItem to make it stable
siddharthkp Aug 22, 2024
9353d71
Merge branch 'main' into select-panel-action-list
siddharthkp Aug 22, 2024
0a126ad
test(vrt): update snapshots
siddharthkp Aug 22, 2024
91cd5a3
show active styles only when used with keyboard
siddharthkp Aug 22, 2024
40322c5
backward compat: expose id as data-id
siddharthkp Aug 22, 2024
f0150e4
Merge branch 'select-panel-action-list' of github.com:primer/react in…
siddharthkp Aug 22, 2024
f72b9d4
update snapshots
siddharthkp Aug 22, 2024
85d0237
add story for long strings
siddharthkp Aug 26, 2024
2eca9fa
fishing for errors
siddharthkp Aug 26, 2024
c875223
backward compatibility for renderItem
siddharthkp Aug 26, 2024
02a6dc9
remove todo now
siddharthkp Aug 26, 2024
25c574a
add a feature flag
siddharthkp Aug 26, 2024
5dc6adb
clean up dual filter list setup
siddharthkp Aug 27, 2024
85e0bb4
run jests test with both states of feature flags
siddharthkp Aug 27, 2024
0ee74e1
refactor snapshot tests with scenarios
siddharthkp Aug 27, 2024
798008e
remove feature flag for main
siddharthkp Aug 27, 2024
beca8db
Merge branch 'main' into select-panel-action-list
siddharthkp Aug 27, 2024
14fd206
Merge branch 'selectpanel-prepare-scenarios' into select-panel-action…
siddharthkp Aug 27, 2024
185eb17
test(vrt): update snapshots
siddharthkp Aug 28, 2024
318aa99
add feature flag to e2e matrix
siddharthkp Aug 28, 2024
e9c523c
test(vrt): update snapshots
siddharthkp Aug 28, 2024
efd1216
backward compat: allow groupMetadata to be empty array
siddharthkp Aug 28, 2024
cd9a931
Merge branch 'select-panel-action-list' of github.com:primer/react in…
siddharthkp Aug 28, 2024
619ff96
Merge branch 'main' into select-panel-action-list
siddharthkp Aug 29, 2024
e3fe7b8
sigh newline
siddharthkp Aug 29, 2024
ebac341
Create sour-cooks-dress.md
siddharthkp Aug 30, 2024
0513474
Merge branch 'main' into select-panel-action-list
siddharthkp Aug 30, 2024
2afa236
copy SelectPanel snapshots from main
siddharthkp Sep 2, 2024
0f38bf4
remove unrelated changes in this PR
siddharthkp Sep 3, 2024
f72aa55
test(vrt): update snapshots
siddharthkp Sep 3, 2024
6e9d706
set active styles for both active-descendant types
siddharthkp Sep 5, 2024
0b57206
test(vrt): update snapshots
siddharthkp Sep 5, 2024
21e14a7
WIP loading states
camertron Sep 6, 2024
88ef070
Merge upstream
camertron Sep 10, 2024
7739645
Add skeleton loading type; add tests
camertron Sep 10, 2024
c098cd9
Improve loading skeletons
camertron Sep 12, 2024
aa9a38a
Add loading type to story params; tweak skeleton widths
camertron Sep 12, 2024
80e205c
Merge upstream
camertron Sep 12, 2024
cd6b487
Add changeset
camertron Sep 12, 2024
e4b07cc
Import correct Stack component
camertron Sep 12, 2024
82be3b4
Fix circular import
camertron Sep 12, 2024
e903b0d
Fix type errors
camertron Sep 12, 2024
863c7ec
Remove unused import
camertron Sep 12, 2024
2abac5f
Update snapshots
camertron Sep 12, 2024
31c31ee
test(vrt): update snapshots
camertron Sep 12, 2024
a7aff04
Upgrade primitives
camertron Sep 17, 2024
9264b1a
Merging upstream
camertron Sep 17, 2024
0d5c1ed
Import from experimental instead of draft
camertron Sep 17, 2024
19daa03
Explore SelectPanel.Messsage API and managing the visibility in the c…
broccolinisoup Sep 19, 2024
38ca715
fix linting and staff
broccolinisoup Sep 19, 2024
9895c58
footer not depend on empty state
broccolinisoup Sep 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selfish-garlics-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

[SelectPanel] Implement loading states
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"@primer/behaviors": "^1.7.2",
"@primer/live-region-element": "^0.7.0",
"@primer/octicons-react": "^19.9.0",
"@primer/primitives": "^9.0.3",
"@primer/primitives": "^9.1.2",
"@styled-system/css": "^5.1.5",
"@styled-system/props": "^5.1.5",
"@styled-system/theme-get": "^5.1.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Box/Box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {SxProp} from '../sx'
import sx from '../sx'
import type {ComponentProps} from '../utils/types'

type StyledBoxProps = SpaceProps &
export type StyledBoxProps = SpaceProps &
ColorProps &
TypographyProps &
LayoutProps &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {useFeatureFlag} from '../FeatureFlags'

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

if (enabled) return <WithStableActionList {...props} />
else return <WithDeprecatedActionList {...props} />
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react'
import Box from '../Box'
import Spinner from '../Spinner'
import {Stack} from '../Stack/Stack'
import {SkeletonBox} from '../experimental/Skeleton/SkeletonBox'

export class FilteredActionListLoadingType {
public name: string
public appearsInBody: boolean

constructor(name: string, appearsInBody: boolean) {
this.name = name
this.appearsInBody = appearsInBody
}
}

export const FilteredActionListLoadingTypes = {
bodySpinner: new FilteredActionListLoadingType('body-spinner', true),
bodySkeleton: new FilteredActionListLoadingType('body-skeleton', true),
input: new FilteredActionListLoadingType('input', false),
}

export function FilteredActionListBodyLoader({loadingType}: {loadingType: FilteredActionListLoadingType}): JSX.Element {
switch (loadingType) {
case FilteredActionListLoadingTypes.bodySpinner:
return <LoadingSpinner data-testid="filtered-action-list-spinner" />
case FilteredActionListLoadingTypes.bodySkeleton:
return <LoadingSkeleton data-testid="filtered-action-list-skeleton" rows={10} />
default:
return <></>
}
}

function LoadingSpinner({...props}): JSX.Element {
return (
<Box p={3}>
<Stack direction="horizontal" justify="center">
<Spinner {...props} />
</Stack>
</Box>
)
}

function LoadingSkeleton({rows = 10, ...props}: {rows: number}): JSX.Element {
return (
<Box p={2}>
<Stack direction="vertical" justify="center" gap="condensed" {...props}>
{Array.from({length: rows}, (_, i) => (
<Stack key={i} direction="horizontal" gap="condensed" align="center">
<SkeletonBox width="16px" height="16px" />
<SkeletonBox height="10px" width={`${Math.random() * 60 + 20}%`} borderRadius={2} />
</Stack>
))}
</Stack>
</Box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {KeyboardEventHandler} from 'react'
import React, {useCallback, useEffect, useRef} from 'react'
import styled from 'styled-components'
import Box from '../Box'
import Spinner from '../Spinner'
import type {TextInputProps} from '../TextInput'
import TextInput from '../TextInput'
import {get} from '../constants'
Expand All @@ -17,6 +16,11 @@ import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
import useScrollFlash from '../hooks/useScrollFlash'
import {VisuallyHidden} from '../internal/components/VisuallyHidden'
import type {SxProp} from '../sx'
import {
type FilteredActionListLoadingType,
FilteredActionListBodyLoader,
FilteredActionListLoadingTypes,
} from './FilteredActionListLoaders'

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

Expand All @@ -25,9 +29,10 @@ export interface FilteredActionListProps
ListPropsBase,
SxProp {
loading?: boolean
loadingType?: FilteredActionListLoadingType
placeholderText?: string
filterValue?: string
onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement>) => void
onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement> | null) => void
textInputProps?: Partial<Omit<TextInputProps, 'onChange'>>
inputRef?: React.RefObject<HTMLInputElement>
}
Expand All @@ -39,6 +44,7 @@ const StyledHeader = styled.div`

export function FilteredActionList({
loading = false,
loadingType = FilteredActionListLoadingTypes.bodySpinner,
placeholderText,
filterValue: externalFilterValue,
onFilterChange,
Expand Down Expand Up @@ -124,15 +130,15 @@ export function FilteredActionList({
aria-label={placeholderText}
aria-controls={listId}
aria-describedby={inputDescriptionTextId}
loaderPosition={'leading'}
loading={loading && !loadingType.appearsInBody}
{...textInputProps}
/>
</StyledHeader>
<VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden>
<Box ref={scrollContainerRef} overflow="auto">
{loading ? (
<Box width="100%" display="flex" flexDirection="row" justifyContent="center" pt={6} pb={7}>
<Spinner />
</Box>
{loading && loadingType.appearsInBody ? (
<FilteredActionListBodyLoader loadingType={loadingType} />
) : (
<ActionList ref={listContainerRef} items={items} {...listProps} role="listbox" id={listId} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {KeyboardEventHandler} from 'react'
import React, {useCallback, useEffect, useRef} from 'react'
import styled from 'styled-components'
import Box from '../Box'
import Spinner from '../Spinner'
import type {TextInputProps} from '../TextInput'
import TextInput from '../TextInput'
import {get} from '../constants'
Expand All @@ -17,6 +16,8 @@ import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
import useScrollFlash from '../hooks/useScrollFlash'
import {VisuallyHidden} from '../internal/components/VisuallyHidden'
import type {SxProp} from '../sx'
import type {FilteredActionListLoadingType} from './FilteredActionListLoaders'
import {FilteredActionListLoadingTypes, FilteredActionListBodyLoader} from './FilteredActionListLoaders'

import {isValidElementType} from 'react-is'
import type {RenderItemFn} from '../deprecated/ActionList/List'
Expand All @@ -28,6 +29,7 @@ export interface FilteredActionListProps
ListPropsBase,
SxProp {
loading?: boolean
loadingType?: FilteredActionListLoadingType
placeholderText?: string
filterValue?: string
onFilterChange: (value: string, e: React.ChangeEvent<HTMLInputElement>) => void
Expand All @@ -42,6 +44,7 @@ const StyledHeader = styled.div`

export function FilteredActionList({
loading = false,
loadingType = FilteredActionListLoadingTypes.bodySpinner,
placeholderText,
filterValue: externalFilterValue,
onFilterChange,
Expand All @@ -51,6 +54,7 @@ export function FilteredActionList({
sx,
groupMetadata,
showItemDividers,
message,
...listProps
}: FilteredActionListProps): JSX.Element {
const [filterValue, setInternalFilterValue] = useProvidedStateOrCreate(externalFilterValue, undefined, '')
Expand Down Expand Up @@ -141,15 +145,15 @@ export function FilteredActionList({
aria-label={placeholderText}
aria-controls={listId}
aria-describedby={inputDescriptionTextId}
loaderPosition={'leading'}
loading={loading && !loadingType.appearsInBody}
{...textInputProps}
/>
</StyledHeader>
<VisuallyHidden id={inputDescriptionTextId}>Items will be filtered as you type</VisuallyHidden>
<Box ref={scrollContainerRef} overflow="auto">
{loading ? (
<Box width="100%" display="flex" flexDirection="row" justifyContent="center" pt={6} pb={7}>
<Spinner />
</Box>
<Box ref={scrollContainerRef} sx={{overflow: 'auto', height: '100%'}}>
{loading && loadingType.appearsInBody ? (
<FilteredActionListBodyLoader loadingType={loadingType} />
) : (
<ActionList ref={listContainerRef} showDividers={showItemDividers} {...listProps} role="listbox" id={listId}>
{groupMetadata?.length
Expand All @@ -170,6 +174,7 @@ export function FilteredActionList({
})}
</ActionList>
)}
{message}
</Box>
</Box>
)
Expand Down
124 changes: 123 additions & 1 deletion packages/react/src/SelectPanel/SelectPanel.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useState, useRef, useMemo} from 'react'
import type {Meta} from '@storybook/react'
import type {Meta, StoryObj} from '@storybook/react'
import Box from '../Box'
import {Button} from '../Button'
import type {ItemInput, GroupedListProps} from '../deprecated/ActionList/List'
Expand All @@ -14,6 +14,8 @@ import {
TypographyIcon,
VersionsIcon,
} from '@primer/octicons-react'
import useSafeTimeout from '../hooks/useSafeTimeout'
import Link from '../Link'

const meta = {
title: 'Components/SelectPanel/Features',
Expand Down Expand Up @@ -369,3 +371,123 @@ export const WithGroups = () => {
/>
)
}

export const AsyncFetch: StoryObj<typeof SelectPanel> = {
render: ({initialLoadingType}) => {
const [selected, setSelected] = React.useState<ItemInput[]>([])
const [filteredItems, setFilteredItems] = React.useState<ItemInput[]>([])
const [open, setOpen] = useState(false)
const filterTimerId = useRef<number | null>(null)
const {safeSetTimeout, safeClearTimeout} = useSafeTimeout()
const onFilterChange = (value: string) => {
if (filterTimerId.current) {
safeClearTimeout(filterTimerId.current)
}

filterTimerId.current = safeSetTimeout(() => {
setFilteredItems(items.filter(item => item.text.toLowerCase().startsWith(value.toLowerCase())))
}, 2000) as unknown as number
}

return (
<SelectPanel
title="Select labels"
subtitle="Use labels to organize issues and pull requests"
renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => (
<Button
trailingAction={TriangleDownIcon}
aria-labelledby={` ${ariaLabelledBy}`}
{...anchorProps}
aria-haspopup="dialog"
>
{children ?? 'Select Labels'}
</Button>
)}
placeholderText="Filter labels"
open={open}
onOpenChange={setOpen}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={onFilterChange}
showItemDividers={true}
initialLoadingType={initialLoadingType}
/>
)
},
argTypes: {
initialLoadingType: {
control: 'select',
options: ['spinner', 'skeleton'],
defaultValue: 'spinner',
},
},
}

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 `}>
Copy link
Member

@siddharthkp siddharthkp Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, interesting.

  1. Does this mean we would figure out which Message to render and ignore the rest?
  2. Am I correct in assuming we would append the query string at the end of this title?title={'No language found for '}>

Copy link
Member Author

@broccolinisoup broccolinisoup Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Actually it is similar to what you have mentioned in your comment - to have the data for both state. Here is more of a variant version and yours is coming from a message object if I understand it right. Tbh, I don't like this API very much. It is cumbersome. I am just entertaining various options 😄

Consumer don't have to figure out which message to render, we will have defaults but if they need to customise this is how they can do (in this API option)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yeah!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this seems great! My only suggestion is to add individual slots (children) for the title and description.

Adjust your search term to find other languages
</SelectPanel.Message>
Comment on lines +454 to +459
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spiking about API options for the empty states. After having an initial conversation with @siddharthkp

Option 1 (As above code)

Pros

  • Straightforward. Consumers don't need to deal with calculating items length etc to display the empty state messages and which empty state they are going to display i.e. no items vs no matches

Cons

  • No flexibility to manage the visibility of the messages since everything is under the hood. There is a chance that it might not work for every usage if consumers have more params in determining the empty states. They can still manage it via this API but it would defeat its main motivation which is handling the complex stuff under the hood.

Option 2

We could simplify the API and let consumers worry about managing the empty state. Like below

<SelectPanel.Message variant="empty" title={noItemsState? `No items created yet' : No language found for'`}>
      {noItemsState? `Start creating your items here'  : Adjust your search term to find other languages}
</SelectPanel.Message>

Pros

  • Simple API

Cons

  • Consumers might choose to use the same messages for both empty states.
  • A bit more work for consumers to deal with managing the state.

Alternatively we can use a prop rather than children for providing the messages.

<SelectPanel
  message={<SelectPanel.Message variant="empty" title={noItemsState? `No items created yet' : No language found for'`}>
      {noItemsState? `Start creating your items here'  : Adjust your search term to find other languages}
</SelectPanel.Message>}
</SelectPanel>

Curious to hear what you think 🙌🏻 @siddharthkp @camertron

Copy link
Member

@siddharthkp siddharthkp Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like option 2 a lot more, it's more intuitive for me to assume what I ever I give in JSX will be rendered.

A bit more work for consumers to deal with managing the state.

For me, the way to remove extra work would be be more configuration based and asked for messages object

<SelectPanel
  message={{
     noInitialItems: {title: 'No items created yet', body: <><Button>Start creating items</Button></>},
     noFilteredItems: {title: `No items found for ${query}`, body: <>Adjust your search term to find other languages</>}
  }}
>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the classic question that always comes up when designing component APIs: how much freedom should we give consumers? In my experience, the more freedom we give consumers, the greater the chance for incongruous outcomes.

This case is even trickier than most because we can almost get away with a props-only approach. Unfortunately the designs show that consumers should be able to add links to the message description, and for error messages it would be nice to eventually be able to add a "Try again" button, etc etc. So I think we have to allow arbitrary content for both error messages and no items/matches.

That being said, IMO we can still establish some guardrails. The designs have two bits of text, a title and a description, with specific font colors and sizes. Whatever API we choose, let's make it easy to do the default thing, eg. provide plaintext values for these fields, and style them appropriately.

As far as behavior goes, I'm firmly in the camp of having the component manage showing/hiding the messages based on the current filter. Eg. if there is no filter text, show the "no items" message; if there is filter text, show the "no matches" message. I would really prefer to reduce the amount of knowledge a consumer has to have about how select panels are supposed to work in order to use them correctly. Requiring consumers to make decisions about when messages are shown or hidden exacerbates this knowledge gap and will lead to problems. Not only will they have to understand the API, but also implicit rules about how to use it appropriately that they may or may not have read in the documentation. The API should be clear enough to (largely) speak for itself.

@broccolinisoup and I brainstormed this during our 1:1 on Monday and came up with an approach where each message has two slots, a title and a description. I imagine it would look something like this:

<SelectPanel.Message variant="no-items">
  <SelectPanel.Message.Title>
    No projects found.
  </SelectPanel.Message.Title>
  <SelectPanel.Message.Description>
    Would you like to <Link href="/projects/create">create one</Link>?
  </SelectPanel.Message.Description>
</SelectPanel.Message>

That's kind of verbose. Maybe we can simplify it?

Hopefully that resonates with you @siddharthkp! Always willing to have my mind changed tho 😄

I like option 2 a lot more, it's more intuitive for me to assume what I ever I give in JSX will be rendered.

Yeah, I get that. Hopefully an approach like the one I outlined above will still give consumers the ability to pass in custom JSX and have it be rendered as expected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you thank you both!! I gathered everything we chatted and wrote down as a discussion #5031 so that we can have a little bit nicer place to continue our discussion and make a decision.

</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>
)
}
Loading
Loading