Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(select): Create a generic Id type for Option interface #214

Closed
wants to merge 9 commits into from
32 changes: 7 additions & 25 deletions src/select/typeahead/TypeaheadSelect.tsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have made a refactor for that component. It looks too complex :(

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import TypeaheadInput, {
} from "../../form/input/typeahead/TypeaheadInput";
import {mapOptionsToTagShapes} from "../../tag/util/tagUtils";
import {TagShape} from "../../tag/Tag";
import {filterOptionsByKeyword} from "./util/typeaheadSelectUtils";
import {filterOutItemsByKey} from "../../core/utils/array/arrayUtils";
import Spinner from "../../spinner/Spinner";
import {KEYBOARD_EVENT_KEY} from "../../core/utils/keyboard/keyboardEventConstants";
Expand All @@ -32,15 +31,15 @@ export interface TypeaheadSelectProps<
TypeaheadInputProps,
"id" | "placeholder" | "name" | "onFocus" | "type"
>;
contentRenderer: (option: T) => React.ReactNode;
onKeywordChange: (value: string) => void;
testid?: string;
onKeywordChange?: (value: string) => void;
initialKeyword?: string;
controlledKeyword?: string;
onTagRemove?: (option: Option) => void;
selectedOptionLimit?: number;
customClassName?: string;
shouldDisplaySelectedOptions?: boolean;
shouldFilterOptionsByKeyword?: boolean;
isDisabled?: boolean;
customSpinner?: React.ReactNode;
shouldShowEmptyOptions?: boolean;
Expand All @@ -57,10 +56,10 @@ function TypeaheadSelect<T extends TypeaheadSelectOption = TypeaheadSelectOption
onTagRemove,
onKeywordChange,
onSelect,
contentRenderer,
customClassName,
selectedOptionLimit,
shouldDisplaySelectedOptions = true,
shouldFilterOptionsByKeyword = true,
isDisabled,
shouldShowEmptyOptions = true,
canOpenDropdownMenu = true,
Expand All @@ -77,7 +76,7 @@ function TypeaheadSelect<T extends TypeaheadSelectOption = TypeaheadSelectOption
const [keyword, setKeyword] = useState(initialKeyword);
const inputValue = typeof controlledKeyword === "string" ? controlledKeyword : keyword;

const tags = mapOptionsToTagShapes(selectedOptions);
const tags = mapOptionsToTagShapes(selectedOptions, contentRenderer);
const shouldDisplayOnlyTags = Boolean(
selectedOptionLimit && selectedOptions.length >= selectedOptionLimit
);
Expand Down Expand Up @@ -144,7 +143,7 @@ function TypeaheadSelect<T extends TypeaheadSelectOption = TypeaheadSelectOption
type={typeaheadProps.type}
placeholder={typeaheadProps.placeholder}
value={inputValue}
onQueryChange={handleKeywordChange}
onQueryChange={onKeywordChange}
onKeyDown={handleKeyDown}
rightIcon={
areOptionsFetching ? spinnerContent : <CaretDownIcon aria-hidden={true} />
Expand All @@ -159,9 +158,10 @@ function TypeaheadSelect<T extends TypeaheadSelectOption = TypeaheadSelectOption
<Select.Content>
{computedDropdownOptions.map((option) => (
<Select.Item key={option.id} option={option}>
{option.title}
{contentRenderer(option)}
</Select.Item>
))}

{shouldShowEmptyOptions && !computedDropdownOptions.length && (
<p
data-testid={`${testid}.empty-message`}
Expand Down Expand Up @@ -203,24 +203,6 @@ function TypeaheadSelect<T extends TypeaheadSelectOption = TypeaheadSelectOption
}
}

function handleKeywordChange(value: string) {
if (shouldFilterOptionsByKeyword) {
const unselectedOptions = options.filter(
(option) => selectedOptions.indexOf(option) < 0
);

setComputedDropdownOptions(filterOptionsByKeyword(unselectedOptions, value));
}

if (onKeywordChange) {
onKeywordChange(value);
}

if (typeof controlledKeyword === "undefined") {
setKeyword(value);
}
}

function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
const {key} = event;

Expand Down
8 changes: 6 additions & 2 deletions src/select/typeahead/typeahead-select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import "@testing-library/jest-dom";

import TypeaheadSelect, {TypeaheadSelectProps} from "./TypeaheadSelect";
import {testA11y} from "../../core/utils/test/testUtils";
import {TypeaheadSelectOption} from "../util/selectTypes";

describe("<TypeaheadSelect />", () => {
const defaultTypeaheadSelectProps: TypeaheadSelectProps = {
const defaultTypeaheadSelectProps: TypeaheadSelectProps<
TypeaheadSelectOption & {title: string}
> = {
testid: "typeahead-select",
options: [
{id: "1", title: "first-dropdown-option"},
Expand All @@ -21,7 +24,8 @@ describe("<TypeaheadSelect />", () => {
typeaheadProps: {
placeholder: "test placeholder",
name: "test typeahead"
}
},
contentRenderer: (option) => option.title
};

it("should render correctly", () => {
Expand Down
19 changes: 0 additions & 19 deletions src/select/typeahead/util/typeaheadSelectUtils.ts

This file was deleted.

8 changes: 3 additions & 5 deletions src/select/util/selectTypes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import React from "react";

interface Option {
id: string;
interface Option<Id = string> {
id: Id;
isDisabled?: boolean;
}

interface TypeaheadSelectOption extends Option {
title: string;
}
type TypeaheadSelectOption<Id = string> = Option<Id>;

type SelectItemElement = HTMLLIElement | HTMLDivElement;

Expand Down
12 changes: 8 additions & 4 deletions src/tag/util/tagUtils.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import React from "react";

import {TypeaheadSelectOption} from "../../select/util/selectTypes";
import {TagShape} from "../Tag";

function mapOptionToTagShape<T extends TypeaheadSelectOption = TypeaheadSelectOption>(
option: T
option: T,
content: React.ReactNode
): TagShape<T> {
return {
id: option.id,
content: option.title,
content,
context: option
};
}

function mapOptionsToTagShapes<T extends TypeaheadSelectOption = TypeaheadSelectOption>(
options: T[]
options: T[],
contentRenderer: (option: T) => React.ReactNode
) {
return options.map(mapOptionToTagShape);
return options.map((option) => mapOptionToTagShape(option, contentRenderer(option)));
}

export {mapOptionsToTagShapes};
81 changes: 62 additions & 19 deletions stories/11-Typeahead.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import StoryFragment from "./utils/StoryFragment";
import FormField from "../src/form/field/FormField";
import TypeaheadSelect from "../src/select/typeahead/TypeaheadSelect";
import {TypeaheadSelectOption} from "../src/select/util/selectTypes";
import {Language} from "./utils/constants/select/selectStoryConstants";
import {filterOptionsByKeyword} from "./utils/typeaheadSelectStoryUtils";

const simulateAPICall = (timeout = 1000) =>
new Promise((resolve) => setTimeout(resolve, timeout));
Expand All @@ -26,28 +28,25 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
id: "spanish",
title: "Spanish"
}
],
] as (TypeaheadSelectOption<Language> & {title: string})[],
thirdOptions: [],
selectedOptions: [] as TypeaheadSelectOption[],
secondSelectedOptions: [] as TypeaheadSelectOption[],
thirdSelectedOptions: [] as TypeaheadSelectOption[],
selectedOptions: [] as (TypeaheadSelectOption<Language> & {title: string})[],
secondSelectedOptions: [] as (TypeaheadSelectOption<Language> & {title: string})[],
thirdSelectedOptions: [] as (TypeaheadSelectOption<Language> & {title: string})[],
areOptionsFetching: false,
keyword: ""
};

const modelInitialState = {
options: [
{
id: "2005",
title: "2005"
id: "2005"
},
{
id: "2015",
title: "2015"
id: "2015"
},
{
id: "2021",
title: "2021"
id: "2021"
}
],
thirdOptions: [] as TypeaheadSelectOption[],
Expand All @@ -65,14 +64,25 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
{(state, setState) => (
<FormField label={"Select Languages"}>
<TypeaheadSelect
options={initialState.options}
options={state.options}
selectedOptions={state.selectedOptions}
contentRenderer={(option) => option.title}
onSelect={(option) =>
setState({
...state,
selectedOptions: [...state.selectedOptions, option]
})
}
onKeywordChange={(keyword) =>
setState({
...state,
options: filterOptionsByKeyword(
initialState.options,
keyword,
"title"
)
})
}
onTagRemove={handleRemoveTag(state, setState)}
typeaheadProps={{
placeholder: "Select Languages",
Expand All @@ -88,14 +98,25 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
<FormField label={"Select Languages (Max selectable 2)"}>
<TypeaheadSelect
selectedOptionLimit={2}
options={initialState.options}
options={state.options}
contentRenderer={(option) => option.title}
selectedOptions={state.secondSelectedOptions}
onSelect={(option) =>
setState({
...state,
secondSelectedOptions: [...state.secondSelectedOptions, option]
})
}
onKeywordChange={(keyword) =>
setState({
...state,
options: filterOptionsByKeyword(
initialState.options,
keyword,
"title"
)
})
}
onTagRemove={handleRemoveTag(state, setState, "secondSelectedOptions")}
typeaheadProps={{
placeholder: "Select Languages",
Expand All @@ -112,8 +133,19 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
<TypeaheadSelect
selectedOptionLimit={2}
isDisabled={true}
options={initialState.options}
options={state.options}
contentRenderer={(option) => option.title}
selectedOptions={state.secondSelectedOptions}
onKeywordChange={(keyword) =>
setState({
...state,
options: filterOptionsByKeyword(
initialState.options,
keyword,
"title"
)
})
}
onSelect={(option) =>
setState({
...state,
Expand All @@ -137,17 +169,17 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
"Select Languages (API Fetch Simulation) - with keyword by TypeaheadSelect"
}>
<TypeaheadSelect
shouldFilterOptionsByKeyword={false}
areOptionsFetching={state.areOptionsFetching}
options={state.thirdOptions}
contentRenderer={(option) => option.title}
selectedOptions={state.thirdSelectedOptions}
onSelect={(option) =>
setState({
...state,
thirdSelectedOptions: [...state.thirdSelectedOptions, option]
})
}
onKeywordChange={handleKeywordChange(setState)}
onKeywordChange={handleAsyncKeywordChange(setState)}
onTagRemove={handleRemoveTag(state, setState, "thirdSelectedOptions")}
typeaheadProps={{
placeholder: "Select Languages",
Expand All @@ -163,9 +195,9 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
<FormField
label={"Select Languages (API Fetch Simulation) - with keyword by parent"}>
<TypeaheadSelect
shouldFilterOptionsByKeyword={false}
areOptionsFetching={state.areOptionsFetching}
options={state.thirdOptions}
contentRenderer={(option) => option.title}
selectedOptions={state.thirdSelectedOptions}
onSelect={(option) =>
setState({
Expand All @@ -174,7 +206,7 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
keyword: ""
})
}
onKeywordChange={handleKeywordChange(setState)}
onKeywordChange={handleAsyncKeywordChange(setState)}
controlledKeyword={state.keyword}
onTagRemove={handleRemoveTag(state, setState, "thirdSelectedOptions")}
typeaheadProps={{
Expand All @@ -190,14 +222,25 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
{(state, setState) => (
<FormField label={"Select Model Year"}>
<TypeaheadSelect
options={modelInitialState.options}
options={state.options}
selectedOptions={state.selectedOptions}
onSelect={(option) =>
setState({
...state,
selectedOptions: [...state.selectedOptions, option]
})
}
contentRenderer={(option) => option.id}
onKeywordChange={(keyword) =>
setState({
...state,
options: filterOptionsByKeyword(
modelInitialState.options,
keyword,
"id"
)
})
}
onTagRemove={handleRemoveTag(state, setState)}
typeaheadProps={{
placeholder: "Select Model",
Expand All @@ -222,7 +265,7 @@ storiesOf("Typeahead", module).add("Typeahead", () => {
});
}

function handleKeywordChange(setState) {
function handleAsyncKeywordChange(setState) {
return async (keyword) => {
if (keyword) {
setState((prevState) => ({
Expand Down
Loading