diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 5a177351..582428ca 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -4,3 +4,4 @@ export * from './useModifiedPopper'; export * from './useOnClickOutside'; export * from './useOnOff'; export * from './useToggle'; +export * from './useSelect'; diff --git a/src/components/hooks/useSelect.tsx b/src/components/hooks/useSelect.tsx new file mode 100644 index 00000000..b45bd29e --- /dev/null +++ b/src/components/hooks/useSelect.tsx @@ -0,0 +1,58 @@ +import { MenuItem } from '@blueprintjs/core'; +import { ItemRenderer } from '@blueprintjs/select'; +import { useState } from 'react'; + +export function useSelect() { + const [value, setValue] = useState(null); + const itemRenderer = getItemRenderer(value); + const onItemSelect = setValue; + const popoverProps = { + onOpened: (node: HTMLElement) => { + const firstUl = node.querySelector('ul'); + if (firstUl) { + firstUl.tabIndex = 0; + firstUl.focus(); + } + }, + }; + const popoverTargetProps = { + style: { display: 'inline-block' }, + }; + const itemPredicate = (query: string, item: T) => { + return item.label.toLowerCase().includes(query.toLowerCase()); + }; + const itemListPredicate = (query: string, items: T[]) => { + return items.filter((item) => + item.label.toLowerCase().includes(query.toLowerCase()), + ); + }; + return { + value, + setValue, + itemRenderer, + onItemSelect, + popoverProps, + popoverTargetProps, + itemPredicate, + itemListPredicate, + }; +} + +function getItemRenderer(value: T | null) { + const render: ItemRenderer = ( + { label }, + { handleClick, handleFocus, modifiers, index }, + ) => ( + + ); + return render; +} diff --git a/stories/components/select.stories.tsx b/stories/components/select.stories.tsx index 171d32f2..5fc83a7b 100644 --- a/stories/components/select.stories.tsx +++ b/stories/components/select.stories.tsx @@ -5,10 +5,10 @@ import { MenuDivider, MenuItem, } from '@blueprintjs/core'; -import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select'; -import { Fragment, useState } from 'react'; +import { ItemListRenderer, Select } from '@blueprintjs/select'; +import { Dispatch, Fragment, SetStateAction, useState } from 'react'; -import { Button, useOnOff } from '../../src/components'; +import { Button, useOnOff, useSelect } from '../../src/components'; export default { title: 'Forms / Select', @@ -44,31 +44,13 @@ function getGroups(items: ItemsType[]) { return { groups, withoutGroup }; } -function getItemRenderer(value: ItemsType | null) { - const render: ItemRenderer = ( - { label }, - { handleClick, handleFocus, modifiers, index }, - ) => ( - - ); - return render; -} const renderMenu: ItemListRenderer = ({ - items, itemsParentRef, + filteredItems, renderItem, menuProps, }) => { - const { groups, withoutGroup } = getGroups(items); + const { groups, withoutGroup } = getGroups(filteredItems); return ( {groups.map(({ group, items }) => ( @@ -82,38 +64,87 @@ const renderMenu: ItemListRenderer = ({ ); }; -const renderMenuNested: ItemListRenderer = ({ - items, - itemsParentRef, - renderItem, - menuProps, -}) => { - const { groups, withoutGroup } = getGroups(items); +function renderMenuNested( + value: ItemsType | null, + [hoveredGroup, setHoveredGroup]: [ + string | undefined, + Dispatch>, + ], +) { + const render: ItemListRenderer = ({ + filteredItems, + itemsParentRef, + renderItem, + menuProps, + activeItem, + }) => { + const { groups, withoutGroup } = getGroups(filteredItems); + return ( + + {groups.map(({ group, items }) => ( + item === activeItem).includes(true)} + selected={items + .map((item) => item.label === value?.label) + .includes(true)} + popoverProps={{ + isOpen: hoveredGroup + ? hoveredGroup === group + : items.map((item) => item === activeItem).includes(true), + onInteraction: (nextOpenState) => { + if (items.map((item) => item === activeItem).includes(true)) { + return; + } + if (nextOpenState) { + setHoveredGroup(group); + } else if (hoveredGroup) { + setHoveredGroup(undefined); + } + }, + }} + roleStructure="listoption" + > + {items.map(renderItem)} + + ))} + {groups.length > 0 && withoutGroup.length > 0 && } + {withoutGroup.map(renderItem)} + + ); + }; + return render; +} +export function OnlyOptions() { + const { value, ...defaultProps } = useSelect(); return ( - - {groups.map(({ group, items }) => ( - - {items.map(renderItem)} - - ))} - {groups.length > 0 && withoutGroup.length > 0 && } - {withoutGroup.map(renderItem)} - + <> + +

Value outside component is {value?.label}.

+ ); -}; +} -export function OnlyOptions() { - const [value, setValue] = useState(null); +export function FiltrableOptions() { + const { value, ...defaultProps } = useSelect(); return ( <> setValue(item)} - itemRenderer={getItemRenderer(value)} filterable={false} itemsEqual="label" itemListRenderer={renderMenu} @@ -142,7 +171,33 @@ export function OnlyCategories() { { label: 'Potato', group: 'Vegetables' }, { label: 'Tomato', group: 'Vegetables' }, ]} - popoverTargetProps={{ style: { display: 'inline-block' } }} + {...defaultProps} + > + @@ -398,21 +446,14 @@ export function InDialog() {

Hello, world!