Skip to content

Commit

Permalink
[♻️][Select]: refactor to improve search mechanic and hidden input re…
Browse files Browse the repository at this point in the history
…ndering (#102)

* Remove get options utility usage

* Improve use-combobox-base hook

* Update version

* Update tests
  • Loading branch information
mimshins authored May 26, 2024
1 parent 8a80d2c commit 0de1c55
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 160 deletions.
4 changes: 2 additions & 2 deletions lib/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ describe("Select", () => {
});

expect(handleSubmit.mock.calls.length).toBe(2);
expect(getFormData().getAll("n")).toEqual(["1"]);
expect(getFormData().getAll("n")).toEqual(["0", "1"]);

rerender1(
<form
Expand Down Expand Up @@ -1164,7 +1164,7 @@ describe("Select", () => {
});

expect(handleSubmit.mock.calls.length).toBe(2);
expect(getFormData().get("n")).toBe(null);
expect(getFormData().get("n")).toBe("0");

rerender2(
<form
Expand Down
41 changes: 9 additions & 32 deletions lib/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ import {
} from "../utils";
import { SelectContext, type SelectContextValue } from "./context";
import { Root as RootSlot } from "./slots";
import {
getOptions as getOptionsUtil,
noValueSelected,
normalizeValues,
useElementsRegistry,
} from "./utils";
import { noValueSelected, normalizeValues, useElementsRegistry } from "./utils";

export type RenderProps = {
/**
Expand Down Expand Up @@ -415,8 +410,6 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
closeList();
}

const getOptions = () => getOptionsUtil(React.Children.toArray(children));

const context: SelectContextValue = {
readOnly,
disabled,
Expand All @@ -433,7 +426,6 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
closeListAndMaintainFocus,
setFilteredEntities,
setActiveDescendant,
getOptions,
openList,
closeList,
toggleList,
Expand Down Expand Up @@ -491,29 +483,14 @@ const SelectBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
if (selectedValues.length === 0) return null;

const renderOptions = () => {
const disabledOptions = getOptions().filter(o => o.disabled);

const isOptionDisabled = (optionValue: string) =>
disabledOptions.some(o => o.value === optionValue);

if (!multiple) {
const optionValue = selectedValues as string;

if (isOptionDisabled(optionValue)) return null;

return <option value={optionValue} />;
}

return (selectedValues as string[]).map(value => {
if (isOptionDisabled(value)) return null;

return (
<option
key={value}
value={value}
/>
);
});
if (!multiple) return <option value={selectedValues as string} />;

return (selectedValues as string[]).map(value => (
<option
key={value}
value={value}
/>
));
};

return (
Expand Down
1 change: 0 additions & 1 deletion lib/Select/components/Controller/Controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const ControllerBase = (props: Props, ref: React.Ref<HTMLInputElement>) => {
searchable: ctx?.searchable ?? false,
onInputChange: onChange,
onKeyDown,
getOptionsInfo: ctx?.getOptions ?? (() => []),
getOptionElements: () => {
const listId = ctx?.elementsRegistry.getElementId("list");
const listNode = document.getElementById(listId ?? "");
Expand Down
63 changes: 45 additions & 18 deletions lib/Select/components/Controller/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ type Props<T extends HTMLElement> = {
onBackspaceKeyDown?: React.KeyboardEventHandler<T>;
onInputChange?: React.ChangeEventHandler<HTMLInputElement>;
getOptionElements: () => HTMLElement[];
getOptionsInfo: SelectContextValue["getOptions"];
onFilteredEntities: (
entities: SelectContextValue["filteredEntities"],
) => void;
Expand All @@ -49,7 +48,6 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
onEscapeKeyDown,
onBackspaceKeyDown,
getOptionElements,
getOptionsInfo,
onActiveDescendantChange,
onListOpenChange,
onFilteredEntities,
Expand All @@ -70,15 +68,11 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
ref: focusVisibleRef,
} = useIsFocusVisible<T>();

const jumpToChar = useJumpToChar({
activeDescendantElement: activeDescendant,
getListItems: getOptionElements,
onActiveDescendantElementChange: onActiveDescendantChange,
});

const ref = React.useRef<T>();
const handleRef = useForkedRefs(ref, focusVisibleRef);

const cachedOptionElementsRef = React.useRef<HTMLElement[]>([]);

const isSelectOnly = !searchable;

const [isFocusedVisible, setIsFocusedVisible] = React.useState(() =>
Expand All @@ -97,12 +91,33 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
ref.current?.focus();
}, []);

const getCachedItems = () => {
if (cachedOptionElementsRef.current.length === 0) {
cachedOptionElementsRef.current = getOptionElements();
}

return cachedOptionElementsRef.current;
};

const jumpToChar = useJumpToChar({
activeDescendantElement: activeDescendant,
getListItems: getOptionElements,
onActiveDescendantElementChange: onActiveDescendantChange,
});

useOnChange(listOpenState, currentOpenState => {
if (disabled || readOnly) return;
if (currentOpenState) return;
if (!(ref.current instanceof HTMLInputElement)) return;

if (currentOpenState) {
cachedOptionElementsRef.current = getOptionElements();

return;
}

ref.current.value = "";
cachedOptionElementsRef.current = [];

onFilteredEntities(null);
});

Expand Down Expand Up @@ -169,7 +184,8 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
if (
item.getAttribute("aria-disabled") === "true" ||
item.hasAttribute("data-hidden") ||
item.getAttribute("aria-hidden") === "true"
item.getAttribute("aria-hidden") === "true" ||
item.hasAttribute("data-hidden")
) {
const newIdx =
(forward ? idx + 1 : idx - 1 + items.length) % items.length;
Expand All @@ -184,9 +200,20 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {
items: (HTMLElement | null)[],
forward: boolean,
) => {
const selectedItems = items.filter(item =>
item?.hasAttribute("data-selected"),
);
const selectedItems = items.filter(item => {
if (!item) return false;

if (
item.getAttribute("aria-disabled") === "true" ||
item.hasAttribute("data-hidden") ||
item.getAttribute("aria-hidden") === "true" ||
item.hasAttribute("data-hidden")
) {
return false;
}

return item.hasAttribute("data-selected");
});

return getAvailableItem(
selectedItems.length > 0 ? selectedItems : items,
Expand Down Expand Up @@ -365,15 +392,15 @@ export const useComboboxBase = <T extends HTMLElement>(props: Props<T>) => {

const query = target.value;

const options = getOptionsInfo();
const items = getCachedItems();

const entities = options
.filter(option => {
const text = option.valueLabel.toLowerCase();
const entities = items
.filter(item => {
const text = item.textContent?.trim().toLowerCase() ?? "";

return text.includes(query.toLowerCase());
})
.map(option => option.value);
.map(item => item.getAttribute("data-entity") ?? "");

onFilteredEntities(entities);
onInputChange?.(event);
Expand Down
47 changes: 27 additions & 20 deletions lib/Select/components/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import {
resolvePropWithRenderContext,
} from "../../internals";
import type { MergeElementProps, PropWithRenderContext } from "../../types";
import { componentWithForwardedRef, useDeterministicId } from "../../utils";
import {
componentWithForwardedRef,
useDeterministicId,
useIsServerHandoffComplete,
} from "../../utils";
import { SelectContext } from "../context";
import { GroupRoot as GroupRootSlot } from "../slots";
import { getOptions } from "../utils";

export type RenderProps = {
/**
Expand Down Expand Up @@ -65,6 +68,8 @@ const GroupBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {

const id = useDeterministicId(idProp, "styleless-ui__select__group");

const isServerHandoffComplete = useIsServerHandoffComplete();

const labelInfo = getLabelInfo(label, "Select.Group", {
customErrorMessage: [
"Invalid `label` property.",
Expand All @@ -75,30 +80,32 @@ const GroupBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {

const ctx = React.useContext(SelectContext);

const getOptionElements = () => {
const group = document.getElementById(id);

if (!group) return [];

return Array.from(group.querySelectorAll<HTMLElement>(`[role='option']`));
};

const isHidden = React.useMemo(() => {
let hidden = false;
if (!isServerHandoffComplete) return false;

const filtered = ctx?.filteredEntities;

if (filtered != null) {
if (filtered.length === 0) hidden = true;
else {
const options = getOptions(
React.Children.toArray(
resolvePropWithRenderContext(childrenProp, { hidden: false }),
),
true,
);

hidden = options.every(
option => !filtered.some(value => value === option.value),
);
}
}
if (filtered == null) return false;
if (filtered.length === 0) return true;

const optionElements: HTMLElement[] = getOptionElements();

return hidden;
return optionElements.every(
optionElement =>
!filtered.some(
value => value === optionElement.getAttribute("data-entity"),
),
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ctx?.filteredEntities]);
}, [ctx?.filteredEntities, isServerHandoffComplete]);

if (!ctx) {
logger("You have to use this component as a descendant of <Select.Root>.", {
Expand Down
5 changes: 0 additions & 5 deletions lib/Select/context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as React from "react";
import { type LabelInfo } from "../internals";
import type { PickAsMandatory } from "../types";
import type { RegisteredElementsKeys } from "./Select";
import type { OptionProps } from "./components";
import type { ElementsRegistry } from "./utils";

type ContextValue = {
Expand All @@ -21,9 +19,6 @@ type ContextValue = {
closeListAndMaintainFocus: () => void;
setActiveDescendant: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
setFilteredEntities: React.Dispatch<React.SetStateAction<null | string[]>>;
getOptions: () => Array<
PickAsMandatory<OptionProps, "disabled" | "value" | "valueLabel">
>;
openList: () => void;
closeList: () => void;
toggleList: () => void;
Expand Down
Loading

0 comments on commit 0de1c55

Please sign in to comment.