diff --git a/src/select/Select.tsx b/src/select/Select.tsx index efda77a..11d0d57 100644 --- a/src/select/Select.tsx +++ b/src/select/Select.tsx @@ -29,8 +29,7 @@ function SelectComponent( const {children, role = "listbox", customClassName, value} = props; const [selectOwnState, dispatchSelectStateAction] = useReducer(selectStateReducer, { focusedOptionIndex: -1, - isMenuOpen: false, - options: props.options || [] + isMenuOpen: false }); const selectRef = useRef(null); const selectState = generateSelectState(selectOwnState, props); @@ -47,6 +46,7 @@ function SelectComponent( return (
( ) { const selectState = useSelectContext(); const dispatchSelectStateAction = useSelectDispatchContext(); - const {onSelect, value, focusedOptionIndex, shouldCloseOnSelect, options} = selectState; + const {onSelect, value, focusedOptionIndex, shouldCloseOnSelect, options, isMenuOpen} = + selectState; const optionIndex = options.findIndex((opt) => opt?.id === option?.id); const isSelected = Array.isArray(value) ? Boolean(value.find((currentOption) => currentOption.id === option?.id)) @@ -83,7 +84,7 @@ function SelectItemComponent( payload: optionIndex }); - if (shouldCloseOnSelect) { + if (shouldCloseOnSelect && isMenuOpen) { dispatchSelectStateAction({type: "TOGGLE_MENU_VISIBILITY"}); } } diff --git a/src/select/typeahead/TypeaheadSelect.tsx b/src/select/typeahead/TypeaheadSelect.tsx index 3b1e132..849562e 100644 --- a/src/select/typeahead/TypeaheadSelect.tsx +++ b/src/select/typeahead/TypeaheadSelect.tsx @@ -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"; @@ -19,6 +18,7 @@ import { } from "../util/selectTypes"; import Select from "../Select"; import TypeheadSelectTrigger from "./trigger/TypeheadSelectTrigger"; +import {filterOptionsByKeyword} from "./util/typeaheadSelectUtils"; import "./_typeahead-select.scss"; @@ -39,8 +39,8 @@ export interface TypeaheadSelectProps< onTagRemove?: (option: Option) => void; selectedOptionLimit?: number; customClassName?: string; - shouldDisplaySelectedOptions?: boolean; shouldFilterOptionsByKeyword?: boolean; + shouldDisplaySelectedOptions?: boolean; isDisabled?: boolean; customSpinner?: React.ReactNode; shouldShowEmptyOptions?: boolean; @@ -48,24 +48,23 @@ export interface TypeaheadSelectProps< areOptionsFetching?: boolean; } -/* eslint-disable complexity */ function TypeaheadSelect({ testid, options, selectedOptions, typeaheadProps, onTagRemove, - onKeywordChange, onSelect, customClassName, selectedOptionLimit, shouldDisplaySelectedOptions = true, - shouldFilterOptionsByKeyword = true, isDisabled, + shouldFilterOptionsByKeyword, shouldShowEmptyOptions = true, canOpenDropdownMenu = true, areOptionsFetching, customSpinner, + onKeywordChange, initialKeyword = "", controlledKeyword }: TypeaheadSelectProps) { @@ -78,11 +77,14 @@ function TypeaheadSelect= selectedOptionLimit ); - const canSelectMultiple = !selectedOptionLimit || selectedOptionLimit > 1; + const canSelectMultiple = + options.length > 1 && (!selectedOptionLimit || selectedOptionLimit > 1); + const shouldCloseOnSelect = !canSelectMultiple || Boolean(selectedOptionLimit && selectedOptions.length >= selectedOptionLimit - 1); @@ -124,6 +126,7 @@ function TypeaheadSelect ))} + {shouldShowEmptyOptions && !computedDropdownOptions.length && (

) { - if (onTagRemove) { - onTagRemove(tag.context!); + if (onTagRemove && tag.context) { + onTagRemove(tag.context); setShouldFocusOnInput(true); + setMenuVisibility(false); + setKeyword(""); } } @@ -235,6 +247,5 @@ function TypeaheadSelect void; customClassName?: string; input?: React.ReactNode; + onClick?: VoidFunction; } function TypeheadSelectTrigger({ handleTagRemove, tags, customClassName, - input + input, + onClick }: TypeheadSelectTriggerProps) { return ( - + )} + {input} ); diff --git a/src/select/typeahead/trigger/_typehead-select-trigger.scss b/src/select/typeahead/trigger/_typehead-select-trigger.scss index b755350..c522d95 100644 --- a/src/select/typeahead/trigger/_typehead-select-trigger.scss +++ b/src/select/typeahead/trigger/_typehead-select-trigger.scss @@ -8,8 +8,12 @@ padding: 0; - border: 1px solid var(--default-border-color); - border-radius: var(--small-border-radius); + .typeahead-select__input { + .input { + border: none; + border-radius: 8px; + } + } } .typeahead-select-trigger__tag-list { diff --git a/src/select/typeahead/typeahead-select.test.tsx b/src/select/typeahead/typeahead-select.test.tsx index 4fbe2d1..c8b1f28 100644 --- a/src/select/typeahead/typeahead-select.test.tsx +++ b/src/select/typeahead/typeahead-select.test.tsx @@ -1,22 +1,25 @@ import React from "react"; -import {fireEvent, render, screen, within} from "@testing-library/react"; +import {fireEvent, render, screen} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import TypeaheadSelect, {TypeaheadSelectProps} from "./TypeaheadSelect"; import {testA11y} from "../../core/utils/test/testUtils"; +import {TypeaheadSelectOption} from "../util/selectTypes"; describe("", () => { - const defaultTypeaheadSelectProps: TypeaheadSelectProps = { + const defaultTypeaheadSelectProps: TypeaheadSelectProps< + TypeaheadSelectOption & {title: string} + > = { testid: "typeahead-select", options: [ {id: "1", title: "first-dropdown-option"}, {id: "2", title: "second-dropdown-option"}, {id: "3", title: "third-dropdown-option"} ], + onKeywordChange: jest.fn(), selectedOptions: [{id: "1", title: "test"}], onSelect: jest.fn(), - onKeywordChange: jest.fn(), onTagRemove: jest.fn(), typeaheadProps: { placeholder: "test placeholder", @@ -41,12 +44,12 @@ describe("", () => { }); }); - it("should update value on change", () => { + it("should update value on change", async () => { render(); const typeaheadSelect = screen.getByRole("textbox"); - userEvent.type(typeaheadSelect, "test"); + await userEvent.type(typeaheadSelect, "test"); expect(typeaheadSelect).toHaveValue("test"); }); @@ -59,20 +62,21 @@ describe("", () => { expect(typeaheadSelect).toBeDisabled(); }); - it("should set initialValue and remove when set new value", () => { + it("should set initialValue and remove when set new value", async () => { render( ); - const typeaheadSelect = screen.getByRole("textbox") as HTMLInputElement; - - expect(typeaheadSelect).toHaveValue("initial"); + const typeaheadSelectInput = screen.getByPlaceholderText( + defaultTypeaheadSelectProps.typeaheadProps.placeholder! + ); - typeaheadSelect.setSelectionRange(0, typeaheadSelect.value.length); + expect(typeaheadSelectInput).toHaveValue("initial"); - userEvent.type(typeaheadSelect, "test"); + await userEvent.clear(typeaheadSelectInput); + await userEvent.type(typeaheadSelectInput, "test"); - expect(typeaheadSelect).toHaveValue("test"); + expect(typeaheadSelectInput).toHaveValue("test"); }); it("should render custom spinner correctly", () => { @@ -91,25 +95,24 @@ describe("", () => { expect(container).toContainElement(spinner); }); - it("should render option menu when focused", () => { + it("should render option menu when focused", async () => { render( ); - const dropdownList = screen.getByTestId("test-dropdown-visibility"); + const dropdownList = screen.getByTestId("test-dropdown-visibility-on-focus"); - expect(dropdownList).not.toHaveClass("dropdown-list--is-visible"); + expect(dropdownList).not.toHaveClass("typeahead-select--is-dropdown-menu-open"); - // fireEvent.focus(screen.getByRole("listbox")); - userEvent.click(screen.getByRole("listbox")); + await userEvent.click(screen.getAllByRole("button")[0]); - expect(dropdownList).toHaveClass("dropdown-list--is-visible"); + expect(dropdownList).toHaveClass("typeahead-select--is-dropdown-menu-open"); }); - it("should run click event handle when option is selected", () => { + it("should run click event handler when option is selected", async () => { render( ", () => { /> ); - const selectedOptionList = screen.getByRole("list"); - - const dropdownList = screen.getByTestId("test-dropdown-visibility"); + const firstOption = screen.getByText(defaultTypeaheadSelectProps.options[0].title); - const firstOption = within(dropdownList).getByTestId( - "test-dropdown-visibility.item-0" - ); - - userEvent.click(firstOption); + await userEvent.click(firstOption); expect(defaultTypeaheadSelectProps.onSelect).toHaveBeenCalledTimes(1); - const secondOption = within(dropdownList).getByTestId( - "test-dropdown-visibility.item-1" - ); + const secondOption = screen.getByText(defaultTypeaheadSelectProps.options[1].title); - userEvent.click(secondOption); + await userEvent.click(secondOption); - expect(selectedOptionList).not.toContainElement(secondOption); + expect(defaultTypeaheadSelectProps.onSelect).toHaveBeenCalledTimes(2); }); - it("should not render option menu when selectedOptionLimit is reached", () => { + it("should not render option menu when selectedOptionLimit is reached", async () => { render( ", () => { const selectedOptionList = screen.getByRole("list"); - const dropdownList = screen.getByTestId("test-dropdown-visibility"); + const secondOption = screen.getByText(defaultTypeaheadSelectProps.options[1].title); - const secondOption = within(dropdownList).getByTestId( - "test-dropdown-visibility.item-1" - ); - - userEvent.click(secondOption); + await userEvent.click(secondOption); expect(selectedOptionList).not.toContainElement(secondOption); }); - it("should render when select an option flow correctly", () => { + it("should render when select an option flow correctly", async () => { render( ", () => { /> ); - userEvent.click(screen.getByRole("listbox")); + await userEvent.click(screen.getByRole("button")); - const dropdownList = screen.getByTestId("test-dropdown-visibility"); + const dropdownList = screen.getByRole("listbox"); - expect(dropdownList).toHaveClass("dropdown-list--is-visible"); + expect(dropdownList).toHaveClass("typeahead-select--is-dropdown-menu-open"); const typeaheadInput = screen.getByRole("textbox"); - userEvent.type(typeaheadInput, "second-dropdown"); + await userEvent.type(typeaheadInput, "second-dropdown"); - const searchedOption = screen.getByTestId("test-dropdown-visibility.item-1"); + const searchedOption = screen.getByText(defaultTypeaheadSelectProps.options[0].title); expect(dropdownList).toContainElement(searchedOption); fireEvent.focus(searchedOption); - userEvent.click(searchedOption); + await userEvent.click(searchedOption); - expect(dropdownList).not.toHaveClass("dropdown-list--is-visible"); + expect(dropdownList).not.toHaveClass("typeahead-select--is-dropdown-menu-open"); expect(defaultTypeaheadSelectProps.onSelect).toHaveBeenCalledTimes(1); }); - it("should not render selected option on dropdown list", () => { + it("should not render selected option on dropdown list", async () => { const {rerender} = render( ", () => { /> ); - userEvent.click(screen.getByRole("listbox")); + await userEvent.click(screen.getByRole("button")); - const dropdownList = screen.getByTestId("test-dropdown-visibility"); + const dropdownList = screen.getByRole("listbox"); - expect(dropdownList).toHaveClass("dropdown-list--is-visible"); + expect(dropdownList).toHaveClass("typeahead-select--is-dropdown-menu-open"); const typeaheadInput = screen.getByRole("textbox"); userEvent.type(typeaheadInput, "second-dropdown"); - const searchedOption = screen.getByTestId("test-dropdown-visibility.item-1"); + const searchedOption = screen.getByText(defaultTypeaheadSelectProps.options[1].title); expect(dropdownList).toContainElement(searchedOption); fireEvent.focus(searchedOption); - userEvent.click(searchedOption); + await userEvent.click(searchedOption); expect(defaultTypeaheadSelectProps.onSelect).toHaveBeenCalledTimes(1); - - expect(dropdownList).toHaveClass("dropdown-list--is-visible"); + expect(dropdownList).not.toHaveClass("select--is-visible"); rerender( ", () => { expect(dropdownList.children.length).toBe(2); - // One of items is the input another one is selected option - expect(selectedOptionList.children.length).toBe(2); + expect(selectedOptionList.children.length).toBe(1); }); }); /* eslint diff --git a/src/select/util/context/SelectContext.reducer.ts b/src/select/util/context/SelectContext.reducer.ts index 6e8f649..defa711 100644 --- a/src/select/util/context/SelectContext.reducer.ts +++ b/src/select/util/context/SelectContext.reducer.ts @@ -10,7 +10,6 @@ function selectStateReducer(state: SelectOwnState, action: SelectStateAction) { isMenuOpen: !state.isMenuOpen, focusedOptionIndex: -1 }; - break; case "SET_FOCUSED_OPTION_INDEX": diff --git a/src/select/util/selectTypes.ts b/src/select/util/selectTypes.ts index 561a75c..45dfa5b 100644 --- a/src/select/util/selectTypes.ts +++ b/src/select/util/selectTypes.ts @@ -1,13 +1,13 @@ import React from "react"; -interface Option { - id: string; +interface Option { + id: Id; isDisabled?: boolean; } -interface TypeaheadSelectOption extends Option { +type TypeaheadSelectOption = Option & { title: string; -} +}; type SelectItemElement = HTMLLIElement | HTMLDivElement; @@ -23,7 +23,7 @@ type TypeaheadSelectOptionSelectHandler< type SelectRole = "listbox" | "menu"; interface SelectProps { children: React.ReactNode; - options: (Option | null)[]; + options: Option[]; value: SelectValue; onSelect: OptionSelectHandler; role?: SelectRole; @@ -32,18 +32,24 @@ interface SelectProps { isDisabled?: boolean; shouldCloseOnSelect?: boolean; isMenuOpen?: boolean; + testid?: string; } type SelectContextValue = Pick< SelectProps, - "hasError" | "isDisabled" | "onSelect" | "shouldCloseOnSelect" | "value" | "role" + | "hasError" + | "isDisabled" + | "onSelect" + | "shouldCloseOnSelect" + | "value" + | "role" + | "options" > & SelectOwnState; interface SelectOwnState { isMenuOpen: boolean; focusedOptionIndex: number; - options: (Option | null)[]; } type SelectValue = T | T[] | null; diff --git a/src/select/util/selectUtils.ts b/src/select/util/selectUtils.ts index 08c267e..84358f2 100644 --- a/src/select/util/selectUtils.ts +++ b/src/select/util/selectUtils.ts @@ -11,13 +11,15 @@ function generateSelectState( props: SelectProps ): SelectContextValue { const selectState: SelectContextValue = { + ...state, isDisabled: props.isDisabled ?? false, hasError: props.hasError ?? false, value: props.value || null, onSelect: props.onSelect, shouldCloseOnSelect: props.shouldCloseOnSelect ?? true, role: props.role ?? "listbox", - ...state + isMenuOpen: props.isMenuOpen ?? state.isMenuOpen, + options: props.options }; return selectState; diff --git a/src/tab/Tab.tsx b/src/tab/Tab.tsx index 75c1afa..6bb66fa 100644 --- a/src/tab/Tab.tsx +++ b/src/tab/Tab.tsx @@ -27,14 +27,14 @@ interface UncontrolledTabProps { // and initialActiveTabIndex should be undefined type ControlledTabProps = | { - activeTabIndex: number; - onTabChange: (index: number) => void; - initialActiveTabIndex?: number; - } + activeTabIndex: number; + onTabChange: (index: number) => void; + initialActiveTabIndex?: number; + } | { - activeTabIndex?: number; - onTabChange?: (index: number) => void; - }; + activeTabIndex?: number; + onTabChange?: (index: number) => void; + }; export type TabProps = ControlledTabProps & UncontrolledTabProps; @@ -72,9 +72,9 @@ function Tab({

{ children[ - activeTabIndexFromProps === undefined - ? activeTabIndex - : activeTabIndexFromProps + activeTabIndexFromProps === undefined + ? activeTabIndex + : activeTabIndexFromProps ] }
diff --git a/stories/11-Typeahead.stories.tsx b/stories/11-Typeahead.stories.tsx index 0067c5e..619cc23 100644 --- a/stories/11-Typeahead.stories.tsx +++ b/stories/11-Typeahead.stories.tsx @@ -7,6 +7,7 @@ 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"; const simulateAPICall = (timeout = 1000) => new Promise((resolve) => setTimeout(resolve, timeout)); @@ -26,11 +27,11 @@ storiesOf("Typeahead", module).add("Typeahead", () => { id: "spanish", title: "Spanish" } - ], + ] as TypeaheadSelectOption[], thirdOptions: [], - selectedOptions: [] as TypeaheadSelectOption[], - secondSelectedOptions: [] as TypeaheadSelectOption[], - thirdSelectedOptions: [] as TypeaheadSelectOption[], + selectedOptions: [] as TypeaheadSelectOption[], + secondSelectedOptions: [] as TypeaheadSelectOption[], + thirdSelectedOptions: [] as TypeaheadSelectOption[], areOptionsFetching: false, keyword: "" }; diff --git a/stories/12-Tab.stories.tsx b/stories/12-Tab.stories.tsx index 5969e6a..0706010 100644 --- a/stories/12-Tab.stories.tsx +++ b/stories/12-Tab.stories.tsx @@ -49,7 +49,10 @@ storiesOf("Tab", module).add("Tab", () => ( isDisabled={state.index === 0}> Previous Tab -
diff --git a/stories/utils/constants/select/selectStoryConstants.tsx b/stories/utils/constants/select/selectStoryConstants.tsx index a8ed0c5..7daeb05 100644 --- a/stories/utils/constants/select/selectStoryConstants.tsx +++ b/stories/utils/constants/select/selectStoryConstants.tsx @@ -1,4 +1,5 @@ import React from "react"; +import {Option} from "../../../../src/select/util/selectTypes"; function CoinDropdownOptionCustomContent({ id, @@ -21,6 +22,13 @@ function CoinDropdownOptionCustomContent({ ); } +export enum Language { + TURKISH = "turkish", + ENGLISH = "english", + SPANISH = "spanish", + FRENCH = "french" +} + const initialState = { basic: { options: [ @@ -42,7 +50,7 @@ const initialState = { isDisabled: true } ], - selectedOption: null as {id: string; isDisabled?: boolean; title: string} | null + selectedOption: null as (Option & {title: string}) | null }, multiSelect: { options: [ @@ -63,8 +71,8 @@ const initialState = { title: "French - Disabled", isDisabled: true } - ] as {id: string; isDisabled?: boolean; title: string}[], - value: [] as {id: string; isDisabled?: boolean; title: string}[] + ] as (Option & {title: string})[], + value: [] as (Option & {title: string})[] }, withSubtitle: { options: [ @@ -84,7 +92,7 @@ const initialState = { subtitle: "JavaScript" } ], - selectedOption: null as {id: string; isDisabled?: boolean; subtitle: string} | null + selectedOption: null as (Option & {title: string}) | null }, withCustomContent: { options: [ @@ -127,11 +135,7 @@ const initialState = { ) } ], - selectedOption: null as { - id: string; - title?: string; - CustomContent: JSX.Element; - } | null + selectedOption: null as (Option & {title: string}) | null } }; diff --git a/stories/utils/selectStoryUtils.ts b/stories/utils/selectStoryUtils.ts index aadbb1c..25f4bad 100644 --- a/stories/utils/selectStoryUtils.ts +++ b/stories/utils/selectStoryUtils.ts @@ -1,9 +1,10 @@ -import {initialState} from "./constants/select/selectStoryConstants"; +import {Option} from "../../src/select/util/selectTypes"; +import {initialState, Language} from "./constants/select/selectStoryConstants"; function handleMultiSelect( state: typeof initialState.multiSelect, setState: React.Dispatch>, - option: {id: string; isDisabled?: boolean; title: string} + option: Option & {title: string} ) { const isSelected = state.value.findIndex((opt) => opt.id === option.id) > -1;