Skip to content

Commit 6de8cb5

Browse files
authored
SelectPanel: Add story to load more items on scroll (#6633)
1 parent 247b3f7 commit 6de8cb5

File tree

2 files changed

+179
-2
lines changed

2 files changed

+179
-2
lines changed

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

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {TriangleDownIcon} from '@primer/octicons-react'
22
import type {Meta} from '@storybook/react-vite'
33
import type React from 'react'
4-
import {useState} from 'react'
4+
import {useState, useEffect, useRef} from 'react'
55

66
import Box from '../Box'
77
import {Button} from '../Button'
@@ -406,3 +406,66 @@ export const AllVariants = () => {
406406
</>
407407
)
408408
}
409+
410+
const NUMBER_OF_ITEMS = 500
411+
const lotsOfItems = Array.from({length: NUMBER_OF_ITEMS}, (_, index) => {
412+
return {
413+
id: index,
414+
text: `Item ${index}`,
415+
description: `Description ${index}`,
416+
leadingVisual: getColorCircle('#a2eeef'),
417+
}
418+
})
419+
420+
export const LotsOfItems = () => {
421+
const [selected, setSelected] = useState<ItemInput[]>([])
422+
const [filter, setFilter] = useState('')
423+
const filteredItems = lotsOfItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
424+
const [open, setOpen] = useState(false)
425+
const timeBeforeOpen = useRef<number>()
426+
const timeAfterOpen = useRef<number>()
427+
const [timeTakenToOpen, setTimeTakenToOpen] = useState<number>()
428+
429+
const onOpenChange = () => {
430+
timeBeforeOpen.current = performance.now()
431+
setOpen(!open)
432+
}
433+
434+
useEffect(() => {
435+
if (open) {
436+
timeAfterOpen.current = performance.now()
437+
if (timeBeforeOpen.current) setTimeTakenToOpen(timeAfterOpen.current - timeBeforeOpen.current)
438+
}
439+
}, [open])
440+
441+
return (
442+
<>
443+
<p>
444+
Time taken to render {NUMBER_OF_ITEMS} items: {timeTakenToOpen || '(click "Select Labels" to open)'}
445+
</p>
446+
447+
<FormControl>
448+
<FormControl.Label>Labels</FormControl.Label>
449+
<SelectPanel
450+
title="Select labels"
451+
placeholder="Select labels"
452+
subtitle="Use labels to organize issues and pull requests"
453+
renderAnchor={({children, ...anchorProps}) => (
454+
<Button trailingAction={TriangleDownIcon} {...anchorProps} aria-haspopup="dialog">
455+
{children}
456+
</Button>
457+
)}
458+
open={open}
459+
onOpenChange={onOpenChange}
460+
items={filteredItems}
461+
selected={selected}
462+
onSelectedChange={setSelected}
463+
onFilterChange={setFilter}
464+
width="medium"
465+
height="large"
466+
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
467+
/>
468+
</FormControl>
469+
</>
470+
)
471+
}

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

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useState, useMemo} from 'react'
1+
import React, {useState, useMemo, useRef, useEffect} from 'react'
22
import type {Meta} from '@storybook/react-vite'
33
import {Button} from '../Button'
44
import type {ItemInput} from '../deprecated/ActionList/List'
@@ -10,6 +10,8 @@ import FormControl from '../FormControl'
1010
import {Stack} from '../Stack'
1111
import {Dialog} from '../experimental'
1212
import styles from './SelectPanel.examples.stories.module.css'
13+
import Checkbox from '../Checkbox'
14+
import Label from '../Label'
1315

1416
const meta: Meta<typeof SelectPanel> = {
1517
title: 'Components/SelectPanel/Examples',
@@ -474,3 +476,115 @@ export const WithDefaultMessage = () => {
474476
</FormControl>
475477
)
476478
}
479+
480+
const NUMBER_OF_ITEMS = 500
481+
const lotsOfItems = Array.from({length: NUMBER_OF_ITEMS}, (_, index) => {
482+
return {
483+
id: index,
484+
text: `Item ${index}`,
485+
description: `Description ${index}`,
486+
leadingVisual: getColorCircle('#a2eeef'),
487+
}
488+
})
489+
490+
export const RenderMoreOnScroll = () => {
491+
const [selected, setSelected] = useState<ItemInput[]>([])
492+
const [open, setOpen] = useState(false)
493+
const [renderSubset, setRenderSubset] = React.useState(true)
494+
495+
const [filter, setFilter] = useState('')
496+
const filteredItems = lotsOfItems.filter(item => item.text.toLowerCase().startsWith(filter.toLowerCase()))
497+
498+
const [numberOfItemsInSubset, setNumberOfItemsInSubset] = useState(50)
499+
const subsetOfFiltereredItemsToRender = filteredItems.slice(0, renderSubset ? numberOfItemsInSubset : NUMBER_OF_ITEMS)
500+
501+
useEffect(function loadMoreItemsOnScrollEnd() {
502+
const scrollContainer = document.querySelector('#select-labels-panel-dialog [role="listbox"]')?.parentElement
503+
504+
const handler = (event: Event) => {
505+
const container = event.target as HTMLElement
506+
if (container.scrollTop === container.scrollHeight - container.offsetHeight) {
507+
// has scrolled to bottom
508+
setNumberOfItemsInSubset(numberOfItemsInSubset + 50)
509+
}
510+
}
511+
512+
// eslint-disable-next-line github/prefer-observers
513+
if (scrollContainer) scrollContainer.addEventListener('scroll', handler)
514+
return () => scrollContainer?.removeEventListener('scroll', handler)
515+
})
516+
517+
/* perf measurement logic start */
518+
const timeBeforeOpen = useRef<number>()
519+
const timeAfterOpen = useRef<number>()
520+
const [timeTakenToOpen, setTimeTakenToOpen] = useState<number>()
521+
522+
const onOpenChange = () => {
523+
if (!open) timeBeforeOpen.current = performance.now()
524+
setOpen(!open)
525+
}
526+
527+
useEffect(
528+
function measureTimeAfterOpen() {
529+
if (open) {
530+
timeAfterOpen.current = performance.now()
531+
if (timeBeforeOpen.current) setTimeTakenToOpen(timeAfterOpen.current - timeBeforeOpen.current)
532+
}
533+
},
534+
[open],
535+
)
536+
/* end */
537+
538+
return (
539+
<form>
540+
<FormControl>
541+
<FormControl.Label>Render subset of items on initial open</FormControl.Label>
542+
<FormControl.Caption>
543+
{renderSubset ? 'Loads more items on scroll' : `Loads all ${NUMBER_OF_ITEMS} items at once`}
544+
</FormControl.Caption>
545+
<Checkbox
546+
checked={renderSubset}
547+
onChange={() => {
548+
setRenderSubset(!renderSubset)
549+
setTimeTakenToOpen(undefined)
550+
setNumberOfItemsInSubset(50)
551+
}}
552+
/>
553+
</FormControl>
554+
<p>
555+
Time taken (ms) to render initial {renderSubset ? 50 : NUMBER_OF_ITEMS} items:{' '}
556+
{timeTakenToOpen ? <Label>{timeTakenToOpen.toFixed(2)} ms</Label> : '(click "Select Labels" to open)'}
557+
</p>
558+
<p>
559+
Known bug: Scroll resets to top when the items change. Works well with feature flag{' '}
560+
<Label>primer_react_select_panel_remove_active_descendant</Label>
561+
</p>
562+
563+
<FormControl>
564+
<FormControl.Label>Labels</FormControl.Label>
565+
<SelectPanel
566+
title="Select labels"
567+
placeholder="Select labels"
568+
subtitle="Use labels to organize issues and pull requests"
569+
renderAnchor={({children, ...anchorProps}) => (
570+
<Button trailingAction={TriangleDownIcon} {...anchorProps} aria-haspopup="dialog">
571+
{children}
572+
</Button>
573+
)}
574+
open={open}
575+
onOpenChange={onOpenChange}
576+
items={subsetOfFiltereredItemsToRender}
577+
selected={selected}
578+
onSelectedChange={setSelected}
579+
onFilterChange={setFilter}
580+
width="medium"
581+
height="large"
582+
message={filteredItems.length === 0 ? NoResultsMessage(filter) : undefined}
583+
overlayProps={{
584+
id: 'select-labels-panel-dialog',
585+
}}
586+
/>
587+
</FormControl>
588+
</form>
589+
)
590+
}

0 commit comments

Comments
 (0)