mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 04:55:06 +09:00
Improve Combobox behavior by rendering the selectedItem in the prefix as text
This commit is contained in:
1 parent
ebe6adc781
commit
1c19a195cc
2 files changed
+77
-34
No files matched your search
@@ -8,8 +8,12 @@ export default {
|
||||
};
|
||||
|
||||
export const Simple: ComponentStory<typeof Combobox> = () => {
|
||||
const items = [{ label: "Apple" }, { label: "Banana" }, { label: "Orange" }];
|
||||
const [value, setValue] = React.useState(items[0]);
|
||||
const items = [
|
||||
{ label: "Apple" },
|
||||
{ label: "Banana" },
|
||||
{ label: "Orange" },
|
||||
] as const;
|
||||
const [value, setValue] = React.useState<typeof items[number] | undefined>();
|
||||
return (
|
||||
<Combobox
|
||||
name="fruit"
|
||||
@@ -26,7 +30,9 @@ export const CustomInput: ComponentStory<typeof Combobox> = () => {
|
||||
{ label: "Banana", value: "banana" },
|
||||
{ label: "Orange", value: "orange" },
|
||||
];
|
||||
const [value, setValue] = React.useState(items[0]);
|
||||
const [value, setValue] = React.useState<typeof items[0] | undefined>(
|
||||
items[0]
|
||||
);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
useState,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
type ComponentProps,
|
||||
type ForwardRefRenderFunction,
|
||||
} from "react";
|
||||
@@ -64,7 +64,7 @@ const ListboxItem = styled("li", itemCss, {
|
||||
|
||||
type ListProps<Item> = {
|
||||
containerProps: ComponentProps<typeof Listbox>;
|
||||
items: Array<Item>;
|
||||
items: ReadonlyArray<Item>;
|
||||
getItemProps: (
|
||||
options: UseComboboxGetItemPropsOptions<Item>
|
||||
) => ComponentProps<typeof ListboxItem>;
|
||||
@@ -113,11 +113,11 @@ export const ComboboxPopperContent = PopperContent;
|
||||
type ComboboxProps<Item> = {
|
||||
name: string;
|
||||
label?: string;
|
||||
items: Array<Item>;
|
||||
items: ReadonlyArray<Item>;
|
||||
value?: Item;
|
||||
onItemSelect?: (value: Item) => void;
|
||||
onItemSelect?: (value?: Item) => void;
|
||||
onItemHighlight?: (value?: Item) => void;
|
||||
itemToString?: (item: Item | null) => string;
|
||||
itemToString?: (item?: Item | null) => string;
|
||||
renderTextField?: (
|
||||
props: ComponentProps<typeof ComboboxTextField>
|
||||
) => JSX.Element;
|
||||
@@ -132,9 +132,7 @@ export const Combobox = <Item extends BaseItem>({
|
||||
value,
|
||||
name,
|
||||
itemToString = (item) =>
|
||||
typeof item === "object" && item !== null && "label" in item
|
||||
? item.label
|
||||
: item ?? "",
|
||||
item != null && "label" in item ? item.label : item ?? "",
|
||||
onItemSelect,
|
||||
onItemHighlight,
|
||||
renderTextField = (props) => <ComboboxTextField {...props} />,
|
||||
@@ -143,6 +141,39 @@ export const Combobox = <Item extends BaseItem>({
|
||||
renderPopperContent = (props) => <ComboboxPopperContent {...props} />,
|
||||
}: ComboboxProps<Item>) => {
|
||||
const [foundItems, setFoundItems] = useState(items);
|
||||
const stateReducer = useCallback((state, actionAndChanges) => {
|
||||
const { type, changes } = actionAndChanges;
|
||||
switch (type) {
|
||||
// on item selection.
|
||||
case useCombobox.stateChangeTypes.ItemClick:
|
||||
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||
case useCombobox.stateChangeTypes.InputBlur:
|
||||
case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
|
||||
return {
|
||||
...changes,
|
||||
// if we had an item selected.
|
||||
...(changes.selectedItem && {
|
||||
// we will set the input value to be empty since we display it using prefix
|
||||
inputValue: "",
|
||||
}),
|
||||
};
|
||||
// Clear input value on escape
|
||||
case useCombobox.stateChangeTypes.InputKeyDownEscape:
|
||||
return {
|
||||
...changes,
|
||||
inputValue: "",
|
||||
selectedItem: null,
|
||||
};
|
||||
// Reset selectedItem when there is selection and we type something
|
||||
case useCombobox.stateChangeTypes.InputChange:
|
||||
return {
|
||||
...changes,
|
||||
selectedItem: null,
|
||||
};
|
||||
default:
|
||||
return changes; // otherwise business as usual.
|
||||
}
|
||||
}, []);
|
||||
const {
|
||||
isOpen,
|
||||
getToggleButtonProps,
|
||||
@@ -154,23 +185,20 @@ export const Combobox = <Item extends BaseItem>({
|
||||
getItemProps,
|
||||
selectedItem,
|
||||
} = useCombobox({
|
||||
onInputValueChange({ inputValue }) {
|
||||
if (inputValue) {
|
||||
const options =
|
||||
typeof items[0] === "object" && "label" in items[0]
|
||||
? { keys: ["label"] }
|
||||
: undefined;
|
||||
const foundItems = matchSorter(items, inputValue, options);
|
||||
setFoundItems(foundItems);
|
||||
}
|
||||
},
|
||||
items: foundItems,
|
||||
selectedItem: value,
|
||||
items: foundItems as Item[],
|
||||
selectedItem: value ?? null, // Avoid downshift warning about switching controlled mode
|
||||
stateReducer,
|
||||
itemToString,
|
||||
onInputValueChange({ inputValue }) {
|
||||
const options =
|
||||
typeof items[0] === "object" && "label" in items[0]
|
||||
? { keys: ["label"] }
|
||||
: undefined;
|
||||
const foundItems = matchSorter(items, inputValue ?? "", options);
|
||||
setFoundItems(foundItems);
|
||||
},
|
||||
onSelectedItemChange({ selectedItem }) {
|
||||
if (selectedItem) {
|
||||
onItemSelect?.(selectedItem);
|
||||
}
|
||||
onItemSelect?.(selectedItem ?? undefined);
|
||||
},
|
||||
onHighlightedIndexChange({ highlightedIndex }) {
|
||||
if (highlightedIndex !== undefined) {
|
||||
@@ -179,13 +207,18 @@ export const Combobox = <Item extends BaseItem>({
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen === false) {
|
||||
setFoundItems(items);
|
||||
}
|
||||
}, [isOpen, items]);
|
||||
|
||||
const inputProps: Record<string, unknown> = getInputProps({ name });
|
||||
const inputProps: Record<string, unknown> = getInputProps({
|
||||
name,
|
||||
onKeyDown: (event) => {
|
||||
// When we press Backspace and the input is empty,
|
||||
// we should to clear the selection
|
||||
// @todo: it would be much better to handle this in reducer but downshift doesn't handle this event
|
||||
if (event.key === "Backspace" && selectedItem !== null) {
|
||||
onItemSelect?.(undefined);
|
||||
}
|
||||
},
|
||||
prefix: itemToString?.(selectedItem ?? undefined),
|
||||
});
|
||||
const toggleProps: Record<string, unknown> = getToggleButtonProps();
|
||||
const comboboxProps: Record<string, unknown> = getComboboxProps();
|
||||
const menuProps: Record<string, unknown> = getMenuProps();
|
||||
@@ -195,7 +228,11 @@ export const Combobox = <Item extends BaseItem>({
|
||||
<Popper>
|
||||
<Box {...comboboxProps}>
|
||||
<PopperAnchor>
|
||||
{renderTextField({ inputProps, toggleProps, highlightedItem })}
|
||||
{renderTextField({
|
||||
inputProps,
|
||||
toggleProps,
|
||||
highlightedItem,
|
||||
})}
|
||||
</PopperAnchor>
|
||||
{renderPopperContent({
|
||||
style: { zIndex: 1 },
|
||||
|
||||
Reference in new issue
Block a user