feat(web): alphabet rail chases pages on click for unloaded letters
test-web / test (push) Successful in 32s
test-web / test (push) Successful in 32s
Operator: prior rail disabled empty buckets, so clicking a letter like 'Z' did nothing if it wasn't loaded yet. AlphabeticalGrid now accepts a paginated source and walks pages on click until the target letter surfaces. - New props: hasMore, onLoadMore. When hasMore=true, every empty bucket stays enabled (clicking will chase pages); when hasMore= false, only populated buckets are clickable. - jumpTo(bucket): if populated, scrollIntoView. Otherwise loop onLoadMore + tick() until the bucket appears or no more pages. Loader2 spinner replaces the letter on the pending button; cursor:wait + aria-busy. Other rail buttons disable while one is pending so clicks don't stack. - Library Artists + Albums pass hasMore + onLoadMore through to the grid. Existing InfiniteScrollSentinel still handles scroll-driven loading. Test: new case asserts rail enables empty buckets when hasMore=true (the click would trigger onLoadMore).
This commit is contained in:
@@ -23,27 +23,43 @@ const itemSnippet = createRawSnippet<[Item]>((getIt) => ({
|
||||
type AnyProps = any;
|
||||
|
||||
describe('AlphabeticalGrid', () => {
|
||||
test('rail renders the full #/A-Z/& set; empty buckets are disabled', () => {
|
||||
test('rail renders the full #/A-Z/& set; empty buckets are disabled when no more data could load', () => {
|
||||
render(AlphabeticalGrid, {
|
||||
props: {
|
||||
items,
|
||||
getKey: (it: Item) => it.key,
|
||||
item: itemSnippet
|
||||
// hasMore default = false → empty buckets stay disabled.
|
||||
} as AnyProps
|
||||
});
|
||||
// Populated buckets are enabled with 'Jump to <letter>' label.
|
||||
expect(screen.getByRole('button', { name: 'Jump to A' })).not.toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Jump to B' })).not.toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Jump to D' })).not.toBeDisabled();
|
||||
// Empty buckets still render but are disabled and labelled
|
||||
// 'X — no entries' so screen readers announce the empty state.
|
||||
// Empty buckets disabled when nothing more could load. aria-label
|
||||
// changes too so screen readers announce the empty state.
|
||||
expect(screen.getByRole('button', { name: 'C — no entries' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Z — no entries' })).toBeDisabled();
|
||||
// # (numbers) and & (symbols) always present too.
|
||||
expect(screen.getByRole('button', { name: 'Numbers — no entries' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Symbols — no entries' })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('with hasMore=true, empty buckets are enabled (clicking would trigger onLoadMore)', () => {
|
||||
render(AlphabeticalGrid, {
|
||||
props: {
|
||||
items,
|
||||
getKey: (it: Item) => it.key,
|
||||
item: itemSnippet,
|
||||
hasMore: true,
|
||||
onLoadMore: () => Promise.resolve()
|
||||
} as AnyProps
|
||||
});
|
||||
// Even with no C/Z items loaded, the buttons remain enabled so a
|
||||
// click can chase pages until the letter surfaces.
|
||||
expect(screen.getByRole('button', { name: 'Jump to C' })).not.toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Jump to Z' })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('renders items in given order followed by the jump rail', () => {
|
||||
const { container } = render(AlphabeticalGrid, {
|
||||
props: {
|
||||
|
||||
Reference in New Issue
Block a user