Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 64 additions & 1 deletion packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {TriangleDownIcon} from '@primer/octicons-react'
import type {Meta} from '@storybook/react-vite'
import type React from 'react'
import {useState} from 'react'
import {useState, useEffect, useRef} from 'react'

import Box from '../Box'
import {Button} from '../Button'
Expand Down Expand Up @@ -406,3 +406,66 @@ export const AllVariants = () => {
</>
)
}

const NUMBER_OF_ITEMS = 500
const lotsOfItems = Array.from({length: NUMBER_OF_ITEMS}, (_, index) => {
return {
id: index,
text: `Item ${index}`,
description: `Description ${index}`,
leadingVisual: getColorCircle('#a2eeef'),
}
})

export const LotsOfItems = () => {
const [selected, setSelected] = useState<ItemInput[]>([])
const [filter, setFilter] = useState('')
const filteredItems = lotsOfItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
const [open, setOpen] = useState(false)
const timeBeforeOpen = useRef<number>()
const timeAfterOpen = useRef<number>()
const [timeTakenToOpen, setTimeTakenToOpen] = useState<number>()

const onOpenChange = () => {
timeBeforeOpen.current = performance.now()
setOpen(!open)
}

useEffect(() => {
if (open) {
timeAfterOpen.current = performance.now()
if (timeBeforeOpen.current) setTimeTakenToOpen(timeAfterOpen.current - timeBeforeOpen.current)
}
}, [open])

return (
<>
<p>
Time taken to render {NUMBER_OF_ITEMS} items: {timeTakenToOpen || '(click "Select Labels" to open)'}
</p>

<FormControl>
<FormControl.Label>Labels</FormControl.Label>
<SelectPanel
title="Select labels"
placeholder="Select labels"
subtitle="Use labels to organize issues and pull requests"
renderAnchor={({children, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} {...anchorProps} aria-haspopup="dialog">
{children}
</Button>
)}
open={open}
onOpenChange={onOpenChange}
items={filteredItems}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
width="medium"
height="large"
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
/>
</FormControl>
</>
)
}
116 changes: 115 additions & 1 deletion packages/react/src/SelectPanel/SelectPanel.examples.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState, useMemo} from 'react'
import React, {useState, useMemo, useRef, useEffect} from 'react'
import type {Meta} from '@storybook/react-vite'
import {Button} from '../Button'
import type {ItemInput} from '../deprecated/ActionList/List'
Expand All @@ -10,6 +10,8 @@ import FormControl from '../FormControl'
import {Stack} from '../Stack'
import {Dialog} from '../experimental'
import styles from './SelectPanel.examples.stories.module.css'
import Checkbox from '../Checkbox'
import Label from '../Label'

const meta: Meta<typeof SelectPanel> = {
title: 'Components/SelectPanel/Examples',
Expand Down Expand Up @@ -474,3 +476,115 @@ export const WithDefaultMessage = () => {
</FormControl>
)
}

const NUMBER_OF_ITEMS = 500
const lotsOfItems = Array.from({length: NUMBER_OF_ITEMS}, (_, index) => {
return {
id: index,
text: `Item ${index}`,
description: `Description ${index}`,
leadingVisual: getColorCircle('#a2eeef'),
}
})

export const RenderMoreOnScroll = () => {
const [selected, setSelected] = useState<ItemInput[]>([])
const [open, setOpen] = useState(false)
const [renderSubset, setRenderSubset] = React.useState(true)

const [filter, setFilter] = useState('')
const filteredItems = lotsOfItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))

const [numberOfItemsInSubset, setNumberOfItemsInSubset] = useState(50)
const subsetOfFiltereredItemsToRender = filteredItems.slice(0, renderSubset ? numberOfItemsInSubset : NUMBER_OF_ITEMS)

useEffect(function loadMoreItemsOnScrollEnd() {
const scrollContainer = document.querySelector('#select-labels-panel-dialog [role="listbox"]')?.parentElement

const handler = (event: Event) => {
const container = event.target as HTMLElement
if (container.scrollTop === container.scrollHeight - container.offsetHeight) {
// has scrolled to bottom
setNumberOfItemsInSubset(numberOfItemsInSubset + 50)
}
}

// eslint-disable-next-line github/prefer-observers
if (scrollContainer) scrollContainer.addEventListener('scroll', handler)
return () => scrollContainer?.removeEventListener('scroll', handler)
})

/* perf measurement logic start */
const timeBeforeOpen = useRef<number>()
const timeAfterOpen = useRef<number>()
const [timeTakenToOpen, setTimeTakenToOpen] = useState<number>()

const onOpenChange = () => {
if (!open) timeBeforeOpen.current = performance.now()
setOpen(!open)
}

useEffect(
function measureTimeAfterOpen() {
if (open) {
timeAfterOpen.current = performance.now()
if (timeBeforeOpen.current) setTimeTakenToOpen(timeAfterOpen.current - timeBeforeOpen.current)
}
},
[open],
)
/* end */

return (
<form>
<FormControl>
<FormControl.Label>Render subset of items on initial open</FormControl.Label>
<FormControl.Caption>
{renderSubset ? 'Loads more items on scroll' : `Loads all ${NUMBER_OF_ITEMS} items at once`}
</FormControl.Caption>
<Checkbox
checked={renderSubset}
onChange={() => {
setRenderSubset(!renderSubset)
setTimeTakenToOpen(undefined)
setNumberOfItemsInSubset(50)
}}
/>
</FormControl>
<p>
Time taken (ms) to render initial {renderSubset ? 50 : NUMBER_OF_ITEMS} items:{' '}
{timeTakenToOpen ? <Label>{timeTakenToOpen.toFixed(2)} ms</Label> : '(click "Select Labels" to open)'}
</p>
<p>
Known bug: Scroll resets to top when the items change. Works well with feature flag{' '}
<Label>primer_react_select_panel_remove_active_descendant</Label>
</p>

<FormControl>
<FormControl.Label>Labels</FormControl.Label>
<SelectPanel
title="Select labels"
placeholder="Select labels"
subtitle="Use labels to organize issues and pull requests"
renderAnchor={({children, ...anchorProps}) => (
<Button trailingAction={TriangleDownIcon} {...anchorProps} aria-haspopup="dialog">
{children}
</Button>
)}
open={open}
onOpenChange={onOpenChange}
items={subsetOfFiltereredItemsToRender}
selected={selected}
onSelectedChange={setSelected}
onFilterChange={setFilter}
width="medium"
height="large"
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
overlayProps={{
id: 'select-labels-panel-dialog',
}}
/>
</FormControl>
</form>
)
}
Loading