Skip to content

Commit

Permalink
feat: default selected value and custom text with useSelect (#681)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: useSelect now takes `itemTextKey` or `getItemText` prop to choose what text should be rendered  in a Select option

* feat: improve useSelect hook

- It accepts a 'defaultSelectedItem' property.
- You can specify which object serves as the label in two ways:
    - Using the 'itemLabelKey' property, which accepts only keys where the value type is string.
    - Using the 'itemLabelMap' function, which takes an item as a parameter and returns a string.

* refactor: select storties

* refactor: select properties

 - rename 'itemLabelKey' to 'itemTextKey'
 - rename 'itemLabelMap' to 'getItemText'
  • Loading branch information
hamed-musallam committed Mar 14, 2024
1 parent ca1af0e commit c9125c8
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 35 deletions.
93 changes: 72 additions & 21 deletions src/components/hooks/useSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,50 @@ import { MenuItem } from '@blueprintjs/core';
import { ItemRenderer } from '@blueprintjs/select';
import { useState } from 'react';

export function useSelect<T extends { label: string }>() {
const [value, setValue] = useState<T | null>(null);
const itemRenderer = getItemRenderer(value);
type FilterType<SourceType, Type> = Pick<
SourceType,
{
[K in keyof SourceType]: SourceType[K] extends Type ? K : never;
}[keyof SourceType]
>;

interface BaseOptions<T> {
itemTextKey: keyof FilterType<T, string>;
defaultSelectedItem?: T;
}

interface RenderOptions<T> {
getItemText: (item: T) => string;
defaultSelectedItem?: T;
}

type SelectOptions<T> = BaseOptions<T> | RenderOptions<T>;

function isAccessLabelByKey<T>(
options: SelectOptions<T>,
): options is BaseOptions<T> {
return 'itemTextKey' in options;
}

function getLabel<T>(item: T, options: SelectOptions<T>) {
const isAccessLByLabelKey = isAccessLabelByKey(options);

if (!item || (isAccessLByLabelKey && !options.itemTextKey)) {
return null;
}

if (isAccessLByLabelKey) {
return item[options.itemTextKey] as string;
}

return options.getItemText(item);
}

export function useSelect<T>(options: SelectOptions<T>) {
const { defaultSelectedItem = null } = options;

const [value, setValue] = useState<T | null>(defaultSelectedItem);
const itemRenderer = getItemRenderer(value as T, options);
const onItemSelect = setValue;
const popoverProps = {
onOpened: (node: HTMLElement) => {
Expand All @@ -19,12 +60,16 @@ export function useSelect<T extends { label: string }>() {
style: { display: 'inline-block' },
};
const itemPredicate = (query: string, item: T) => {
return item.label.toLowerCase().includes(query.toLowerCase());
const label = getLabel(item, options);
if (!label) return false;
return label.toLowerCase().includes(query.toLowerCase());
};
const itemListPredicate = (query: string, items: T[]) => {
return items.filter((item) =>
item.label.toLowerCase().includes(query.toLowerCase()),
);
return items.filter((item) => {
const label = getLabel(item, options);
if (!label) return false;
return label.toLowerCase().includes(query.toLowerCase());
});
};
return {
value,
Expand All @@ -38,21 +83,27 @@ export function useSelect<T extends { label: string }>() {
};
}

function getItemRenderer<T extends { label: string }>(value: T | null) {
function getItemRenderer<T>(value: T, options: SelectOptions<T>) {
const selectedLabel = getLabel(value, options);

const render: ItemRenderer<T> = (
{ label },
item,
{ handleClick, handleFocus, modifiers, index },
) => (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
selected={value?.label === label}
key={index}
onClick={handleClick}
onFocus={handleFocus}
roleStructure="listoption"
text={label}
/>
);
) => {
const label = getLabel(item, options);
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
selected={selectedLabel === label}
key={index}
onClick={handleClick}
onFocus={handleFocus}
roleStructure="listoption"
text={label}
/>
);
};

return render;
}
52 changes: 38 additions & 14 deletions stories/components/select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ function renderMenuNested(
}

export function OnlyOptions() {
const { value, ...defaultProps } = useSelect();
const { value, ...defaultProps } = useSelect<{ label: string }>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -138,7 +140,9 @@ export function OnlyOptions() {
}

export function FiltrableOptions() {
const { value, ...defaultProps } = useSelect();
const { value, ...defaultProps } = useSelect<{ label: string }>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -156,7 +160,9 @@ export function FiltrableOptions() {
);
}
export function OnlyCategories() {
const { value, ...defaultProps } = useSelect<ItemsType>();
const { value, ...defaultProps } = useSelect<ItemsType>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -183,7 +189,9 @@ export function OnlyCategories() {
);
}
export function FilteredCategories() {
const { value, ...defaultProps } = useSelect<ItemsType>();
const { value, ...defaultProps } = useSelect<ItemsType>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -209,7 +217,9 @@ export function FilteredCategories() {
);
}
export function OptionsWithCategories() {
const { value, ...defaultProps } = useSelect<ItemsType>();
const { value, ...defaultProps } = useSelect<ItemsType>({
itemTextKey: 'label',
});

return (
<>
Expand Down Expand Up @@ -239,7 +249,9 @@ export function OptionsWithCategories() {
);
}
export function CategoriesNested() {
const { value, ...defaultProps } = useSelect<ItemsType>();
const { value, ...defaultProps } = useSelect<ItemsType>({
itemTextKey: 'label',
});
const hoverState = useState<string | undefined>(undefined);
return (
<>
Expand Down Expand Up @@ -270,7 +282,9 @@ export function CategoriesNested() {
}

export function DisabledOptions() {
const { value, ...defaultProps } = useSelect();
const { value, ...defaultProps } = useSelect<{ label: string }>({
getItemText: (item) => item.label,
});
return (
<>
<Select
Expand All @@ -291,7 +305,9 @@ export function DisabledOptions() {
}

export function DisabledInCategories() {
const { value, ...defaultProps } = useSelect<ItemsType>();
const { value, ...defaultProps } = useSelect<ItemsType>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand Down Expand Up @@ -322,7 +338,9 @@ export function DisabledInCategories() {
}

export function Disabled() {
const { value, ...defaultProps } = useSelect();
const { value, ...defaultProps } = useSelect<{ label: string }>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -343,7 +361,9 @@ export function Disabled() {
);
}
export function WithCustomStyle() {
const { value, ...defaultProps } = useSelect();
const { value, ...defaultProps } = useSelect<{ label: string }>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -365,7 +385,7 @@ export function WithCustomStyle() {
}

export function FixedValueNoopHandle() {
const defaultProps = useSelect();
const defaultProps = useSelect<{ label: string }>({ itemTextKey: 'label' });
const value = { label: 'Orange' };
return (
<>
Expand All @@ -387,7 +407,7 @@ export function FixedValueNoopHandle() {
}

export function NullValueNoopHandle() {
const defaultProps = useSelect();
const defaultProps = useSelect<{ label: string }>({ itemTextKey: 'label' });
const value = null;
return (
<>
Expand All @@ -409,7 +429,9 @@ export function NullValueNoopHandle() {
}

export function ResetButton() {
const { value, setValue, ...defaultProps } = useSelect();
const { value, setValue, ...defaultProps } = useSelect<{ label: string }>({
itemTextKey: 'label',
});
return (
<>
<Select
Expand All @@ -431,7 +453,9 @@ export function ResetButton() {

export function InDialog() {
const [isOpen, open, close] = useOnOff();
const { value, ...defaultProps } = useSelect();
const { value, ...defaultProps } = useSelect<{ label: string }>({
itemTextKey: 'label',
});
return (
<>
<Button onClick={open}>Open</Button>
Expand Down

0 comments on commit c9125c8

Please sign in to comment.