Add virtualizer on all other menus
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
RiShareBoxLine,
|
||||
} from '@remixicon/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/Menu',
|
||||
@@ -341,3 +342,73 @@ export const Submenu: Story = {
|
||||
</MenuTrigger>
|
||||
),
|
||||
};
|
||||
|
||||
export const Virtualized: Story = {
|
||||
args: {
|
||||
...Preview.args,
|
||||
},
|
||||
render: () => {
|
||||
const [pokemon, setPokemon] = useState<
|
||||
Array<{ name: string; url: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPokemon(data.results);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching Pokemon:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MenuTrigger isOpen>
|
||||
<Button aria-label="Menu">Menu</Button>
|
||||
<Menu items={pokemon} virtualized>
|
||||
{pokemon.map((p, index) => (
|
||||
<MenuItem key={index} id={p.name}>
|
||||
{p.name.charAt(0).toLocaleUpperCase('en-US') + p.name.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const VirtualizedMaxHeight: Story = {
|
||||
args: {
|
||||
...Preview.args,
|
||||
},
|
||||
render: () => {
|
||||
const [pokemon, setPokemon] = useState<
|
||||
Array<{ name: string; url: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPokemon(data.results);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching Pokemon:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MenuTrigger isOpen>
|
||||
<Button aria-label="Menu">Menu</Button>
|
||||
<Menu items={pokemon} virtualized maxHeight="300px">
|
||||
{pokemon.map((p, index) => (
|
||||
<MenuItem key={index} id={p.name}>
|
||||
{p.name.charAt(0).toLocaleUpperCase('en-US') + p.name.slice(1)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -121,19 +121,10 @@
|
||||
}
|
||||
|
||||
.bui-MenuItemListBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 2rem;
|
||||
padding-inline: var(--bui-space-2);
|
||||
border-radius: var(--bui-radius-2);
|
||||
outline: none;
|
||||
cursor: default;
|
||||
color: var(--bui-fg-primary);
|
||||
font-size: var(--bui-font-size-3);
|
||||
gap: var(--bui-space-6);
|
||||
padding-inline: var(--bui-space-1);
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
&:hover .bui-MenuItemWrapper {
|
||||
background: var(--bui-bg-surface-2);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
Virtualizer,
|
||||
ListLayout,
|
||||
} from 'react-aria-components';
|
||||
import { ScrollArea } from '../ScrollArea';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import type {
|
||||
MenuTriggerProps,
|
||||
@@ -74,23 +73,41 @@ export const SubmenuTrigger = (props: SubmenuTriggerProps) => {
|
||||
|
||||
/** @public */
|
||||
export const Menu = (props: MenuProps<object>) => {
|
||||
const { placement = 'bottom start', ...rest } = props;
|
||||
const {
|
||||
placement = 'bottom start',
|
||||
virtualized = false,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
...rest
|
||||
} = props;
|
||||
const { classNames } = useStyles('Menu');
|
||||
const navigate = useNavigate();
|
||||
let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined');
|
||||
|
||||
const menuContent = (
|
||||
<RAMenu
|
||||
className={classNames.content}
|
||||
renderEmptyState={() => <MenuEmptyState />}
|
||||
style={{ width: newMaxWidth, maxHeight }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RAPopover className={classNames.popover} placement={placement}>
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<ScrollArea.Root>
|
||||
<ScrollArea.Viewport>
|
||||
<RAMenu className={classNames.content} {...rest}>
|
||||
{props.children}
|
||||
</RAMenu>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar orientation="vertical" style={{}}>
|
||||
<ScrollArea.Thumb />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
{virtualized ? (
|
||||
<Virtualizer
|
||||
layout={ListLayout}
|
||||
layoutOptions={{
|
||||
rowHeight: 32,
|
||||
}}
|
||||
>
|
||||
{menuContent}
|
||||
</Virtualizer>
|
||||
) : (
|
||||
menuContent
|
||||
)}
|
||||
</RouterProvider>
|
||||
</RAPopover>
|
||||
);
|
||||
@@ -101,24 +118,37 @@ export const MenuListBox = (props: MenuListBoxProps<object>) => {
|
||||
const {
|
||||
selectionMode = 'single',
|
||||
placement = 'bottom start',
|
||||
virtualized = false,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
...rest
|
||||
} = props;
|
||||
const { classNames } = useStyles('Menu');
|
||||
let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined');
|
||||
|
||||
const listBoxContent = (
|
||||
<RAListBox
|
||||
className={classNames.content}
|
||||
selectionMode={selectionMode}
|
||||
style={{ width: newMaxWidth, maxHeight }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RAPopover className={classNames.popover} placement={placement}>
|
||||
<ScrollArea.Root>
|
||||
<ScrollArea.Viewport>
|
||||
<RAListBox
|
||||
className={classNames.content}
|
||||
selectionMode={selectionMode}
|
||||
{...rest}
|
||||
/>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar orientation="vertical" style={{}}>
|
||||
<ScrollArea.Thumb />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
{virtualized ? (
|
||||
<Virtualizer
|
||||
layout={ListLayout}
|
||||
layoutOptions={{
|
||||
rowHeight: 32,
|
||||
}}
|
||||
>
|
||||
{listBoxContent}
|
||||
</Virtualizer>
|
||||
) : (
|
||||
listBoxContent
|
||||
)}
|
||||
</RAPopover>
|
||||
);
|
||||
};
|
||||
@@ -135,6 +165,7 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps<object>) => {
|
||||
const { classNames } = useStyles('Menu');
|
||||
const { contains } = useFilter({ sensitivity: 'base' });
|
||||
let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const menuContent = (
|
||||
<RAMenu
|
||||
@@ -145,6 +176,64 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps<object>) => {
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RAPopover className={classNames.popover} placement={placement}>
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<RAAutocomplete filter={contains}>
|
||||
<RASearchField className={classNames.searchField}>
|
||||
<RAInput
|
||||
className={classNames.searchFieldInput}
|
||||
aria-label="Search"
|
||||
placeholder={props.placeholder || 'Search...'}
|
||||
/>
|
||||
<RAButton className={classNames.searchFieldClear}>
|
||||
<RiCloseCircleLine />
|
||||
</RAButton>
|
||||
</RASearchField>
|
||||
{virtualized ? (
|
||||
<Virtualizer
|
||||
layout={ListLayout}
|
||||
layoutOptions={{
|
||||
rowHeight: 32,
|
||||
}}
|
||||
>
|
||||
{menuContent}
|
||||
</Virtualizer>
|
||||
) : (
|
||||
menuContent
|
||||
)}
|
||||
</RAAutocomplete>
|
||||
</RouterProvider>
|
||||
</RAPopover>
|
||||
);
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const MenuAutocompleteListbox = (
|
||||
props: MenuAutocompleteListBoxProps<object>,
|
||||
) => {
|
||||
const {
|
||||
selectionMode = 'single',
|
||||
placement = 'bottom start',
|
||||
virtualized = false,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
...rest
|
||||
} = props;
|
||||
const { classNames } = useStyles('Menu');
|
||||
const { contains } = useFilter({ sensitivity: 'base' });
|
||||
let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined');
|
||||
|
||||
const listBoxContent = (
|
||||
<RAListBox
|
||||
className={classNames.content}
|
||||
renderEmptyState={() => <MenuEmptyState />}
|
||||
selectionMode={selectionMode}
|
||||
style={{ width: newMaxWidth, maxHeight }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<RAPopover className={classNames.popover} placement={placement}>
|
||||
<RAAutocomplete filter={contains}>
|
||||
@@ -165,59 +254,16 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps<object>) => {
|
||||
rowHeight: 32,
|
||||
}}
|
||||
>
|
||||
{menuContent}
|
||||
{listBoxContent}
|
||||
</Virtualizer>
|
||||
) : (
|
||||
menuContent
|
||||
listBoxContent
|
||||
)}
|
||||
</RAAutocomplete>
|
||||
</RAPopover>
|
||||
);
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const MenuAutocompleteListbox = (
|
||||
props: MenuAutocompleteListBoxProps<object>,
|
||||
) => {
|
||||
const {
|
||||
selectionMode = 'single',
|
||||
placement = 'bottom start',
|
||||
...rest
|
||||
} = props;
|
||||
const { classNames } = useStyles('Menu');
|
||||
const { contains } = useFilter({ sensitivity: 'base' });
|
||||
|
||||
return (
|
||||
<RAPopover className={classNames.popover} placement={placement}>
|
||||
<RAAutocomplete filter={contains}>
|
||||
<RASearchField className={classNames.searchField}>
|
||||
<RAInput
|
||||
className={classNames.searchFieldInput}
|
||||
aria-label="Search"
|
||||
placeholder={props.placeholder || 'Search...'}
|
||||
/>
|
||||
<RAButton className={classNames.searchFieldClear}>
|
||||
<RiCloseCircleLine />
|
||||
</RAButton>
|
||||
</RASearchField>
|
||||
<ScrollArea.Root>
|
||||
<ScrollArea.Viewport>
|
||||
<RAListBox
|
||||
className={classNames.content}
|
||||
renderEmptyState={() => <MenuEmptyState />}
|
||||
selectionMode={selectionMode}
|
||||
{...rest}
|
||||
/>
|
||||
</ScrollArea.Viewport>
|
||||
<ScrollArea.Scrollbar orientation="vertical" style={{}}>
|
||||
<ScrollArea.Thumb />
|
||||
</ScrollArea.Scrollbar>
|
||||
</ScrollArea.Root>
|
||||
</RAAutocomplete>
|
||||
</RAPopover>
|
||||
);
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const MenuItem = (props: MenuItemProps) => {
|
||||
const { iconStart, color = 'primary', children, href, ...rest } = props;
|
||||
@@ -282,11 +328,13 @@ export const MenuListBoxItem = (props: MenuListBoxItemProps) => {
|
||||
className={classNames.itemListBox}
|
||||
{...rest}
|
||||
>
|
||||
<div className={classNames.itemContent}>
|
||||
<div className={classNames.itemListBoxCheck}>
|
||||
<RiCheckLine />
|
||||
<div className={classNames.itemWrapper}>
|
||||
<div className={classNames.itemContent}>
|
||||
<div className={classNames.itemListBoxCheck}>
|
||||
<RiCheckLine />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</RAListBoxItem>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SubmenuTrigger,
|
||||
} from './index';
|
||||
import { Button, Flex, Text } from '../..';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Selection } from 'react-aria-components';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
@@ -183,3 +183,82 @@ export const Submenu: Story = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Virtualized: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: () => {
|
||||
const [pokemon, setPokemon] = useState<
|
||||
Array<{ name: string; url: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPokemon(data.results);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching Pokemon:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MenuTrigger isOpen>
|
||||
<Button aria-label="Menu">Menu</Button>
|
||||
<MenuAutocompleteListbox
|
||||
items={pokemon}
|
||||
placeholder="Search Pokemon..."
|
||||
virtualized
|
||||
>
|
||||
{pokemon.map((p, index) => (
|
||||
<MenuListBoxItem key={index} id={p.name}>
|
||||
{p.name.charAt(0).toLocaleUpperCase('en-US') + p.name.slice(1)}
|
||||
</MenuListBoxItem>
|
||||
))}
|
||||
</MenuAutocompleteListbox>
|
||||
</MenuTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const VirtualizedMaxHeight: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: () => {
|
||||
const [pokemon, setPokemon] = useState<
|
||||
Array<{ name: string; url: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPokemon(data.results);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching Pokemon:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MenuTrigger isOpen>
|
||||
<Button aria-label="Menu">Menu</Button>
|
||||
<MenuAutocompleteListbox
|
||||
items={pokemon}
|
||||
placeholder="Search Pokemon..."
|
||||
virtualized
|
||||
maxHeight="300px"
|
||||
>
|
||||
{pokemon.map((p, index) => (
|
||||
<MenuListBoxItem key={index} id={p.name}>
|
||||
{p.name.charAt(0).toLocaleUpperCase('en-US') + p.name.slice(1)}
|
||||
</MenuListBoxItem>
|
||||
))}
|
||||
</MenuAutocompleteListbox>
|
||||
</MenuTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { MenuTrigger, MenuListBox, MenuListBoxItem } from './index';
|
||||
import { Button, Flex, Text } from '../..';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Selection } from 'react-aria-components';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
@@ -87,3 +87,73 @@ export const Controlled: Story = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Virtualized: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: () => {
|
||||
const [pokemon, setPokemon] = useState<
|
||||
Array<{ name: string; url: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPokemon(data.results);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching Pokemon:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MenuTrigger isOpen>
|
||||
<Button aria-label="Menu">Menu</Button>
|
||||
<MenuListBox items={pokemon} virtualized>
|
||||
{pokemon.map((p, index) => (
|
||||
<MenuListBoxItem key={index} id={p.name}>
|
||||
{p.name.charAt(0).toLocaleUpperCase('en-US') + p.name.slice(1)}
|
||||
</MenuListBoxItem>
|
||||
))}
|
||||
</MenuListBox>
|
||||
</MenuTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const VirtualizedMaxHeight: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: () => {
|
||||
const [pokemon, setPokemon] = useState<
|
||||
Array<{ name: string; url: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://pokeapi.co/api/v2/pokemon?limit=1000')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
setPokemon(data.results);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching Pokemon:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MenuTrigger isOpen>
|
||||
<Button aria-label="Menu">Menu</Button>
|
||||
<MenuListBox items={pokemon} virtualized maxHeight="300px">
|
||||
{pokemon.map((p, index) => (
|
||||
<MenuListBoxItem key={index} id={p.name}>
|
||||
{p.name.charAt(0).toLocaleUpperCase('en-US') + p.name.slice(1)}
|
||||
</MenuListBoxItem>
|
||||
))}
|
||||
</MenuListBox>
|
||||
</MenuTrigger>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -37,6 +37,9 @@ export interface MenuProps<T>
|
||||
extends RAMenuProps<T>,
|
||||
Omit<RAMenuProps<T>, 'children'> {
|
||||
placement?: RAPopoverProps['placement'];
|
||||
virtualized?: boolean;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -44,6 +47,9 @@ export interface MenuListBoxProps<T>
|
||||
extends RAListBoxProps<T>,
|
||||
Omit<RAListBoxProps<T>, 'children'> {
|
||||
placement?: RAPopoverProps['placement'];
|
||||
virtualized?: boolean;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -63,6 +69,9 @@ export interface MenuAutocompleteListBoxProps<T>
|
||||
Omit<RAListBoxProps<T>, 'children'> {
|
||||
placeholder?: string;
|
||||
placement?: RAPopoverProps['placement'];
|
||||
virtualized?: boolean;
|
||||
maxWidth?: string;
|
||||
maxHeight?: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
Reference in New Issue
Block a user