From 5ca483cd7384f27495c6e041fb7e243dfe8f2dff Mon Sep 17 00:00:00 2001 From: Theo Date: Fri, 12 Apr 2019 18:04:26 +0100 Subject: [PATCH 1/4] `matching_options` converted to TS+tests --- .../combo_box/matching_options.test.ts | 171 ++++++++++++++++++ src/components/combo_box/matching_options.ts | 105 +++++++++++ 2 files changed, 276 insertions(+) create mode 100644 src/components/combo_box/matching_options.test.ts create mode 100644 src/components/combo_box/matching_options.ts diff --git a/src/components/combo_box/matching_options.test.ts b/src/components/combo_box/matching_options.test.ts new file mode 100644 index 00000000000..a4117425411 --- /dev/null +++ b/src/components/combo_box/matching_options.test.ts @@ -0,0 +1,171 @@ +import { + EuiComboBoxOption, + flattenOptionGroups, + getSelectedOptionForSearchValue, + getMatchingOptions, +} from './matching_options'; + +const options: EuiComboBoxOption[] = [ + { + label: 'Titan', + 'data-test-subj': 'titanOption', + }, + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + { + label: 'Mimas', + }, +]; + +describe('flattenOptionGroups', () => { + test('it flattens one level of options', () => { + // Assemble + const input = [ + { + label: 'Titan', + 'data-test-subj': 'titanOption', + }, + { + label: 'Enceladus', + options: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + }, + { + label: 'Mimas', + }, + ]; + const expected = options; + // Act + const got = flattenOptionGroups(input); + // Assert + expect(got).toMatchObject(expected); + }); +}); + +describe('getSelectedOptionForSearchValue', () => { + test('gets the first matching selected option for search value', () => { + // Assemble + const expected: EuiComboBoxOption = { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }; + // Act + const got = getSelectedOptionForSearchValue('saturn', options); + // Assert + expect(got).toMatchObject(expected); + }); +}); + +describe('getSelectedOptionForSearchValue', () => { + test('returns undefined when no matching option found for search value', () => { + // Act + const got = getSelectedOptionForSearchValue('Pluto', options); + // Assert + expect(got).toBeUndefined(); + }); + test('gets the first matching selected option for search value', () => { + // Assemble + const expected: EuiComboBoxOption = { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }; + // Act + const got = getSelectedOptionForSearchValue('saturn', options); + // Assert + expect(got).toMatchObject(expected); + }); +}); + +interface GetMatchingOptionsTestCase { + options: EuiComboBoxOption[]; + selectedOptions: EuiComboBoxOption[]; + searchValue: string; + isPreFiltered: boolean; + showPrevSelected: boolean; + expected: EuiComboBoxOption[]; +} + +const testCases: GetMatchingOptionsTestCase[] = [ + { + options, + selectedOptions: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + searchValue: 'saturn', + isPreFiltered: false, + showPrevSelected: false, + expected: [], + }, + { + options, + selectedOptions: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + searchValue: 'saturn', + isPreFiltered: true, + showPrevSelected: false, + expected: [ + { 'data-test-subj': 'titanOption', label: 'Titan' }, + { label: 'Mimas' }, + ], + }, + { + options, + selectedOptions: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + searchValue: 'saturn', + isPreFiltered: false, + showPrevSelected: true, + expected: [{ 'data-test-subj': 'saturnOption', label: 'Saturn' }], + }, + { + options, + selectedOptions: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + searchValue: 'saturn', + isPreFiltered: true, + showPrevSelected: true, + expected: [ + { 'data-test-subj': 'titanOption', label: 'Titan' }, + { 'data-test-subj': 'saturnOption', label: 'Saturn' }, + { label: 'Mimas' }, + ], + }, +]; + +describe('getMatchingOptions', () => { + test.each(testCases)( + '.getMatchingOptions(%o)', + (testCase: GetMatchingOptionsTestCase) => { + expect( + getMatchingOptions( + testCase.options, + testCase.selectedOptions, + testCase.searchValue, + testCase.isPreFiltered, + testCase.showPrevSelected + ) + ).toMatchObject(testCase.expected); + } + ); +}); diff --git a/src/components/combo_box/matching_options.ts b/src/components/combo_box/matching_options.ts new file mode 100644 index 00000000000..535c7d5fcc4 --- /dev/null +++ b/src/components/combo_box/matching_options.ts @@ -0,0 +1,105 @@ +export interface EuiComboBoxOption { + label: string; + [prop: string]: any; +} + +export const flattenOptionGroups = (optionsOrGroups: EuiComboBoxOption[]) => { + return optionsOrGroups.reduce( + (options: EuiComboBoxOption[], optionOrGroup: EuiComboBoxOption) => { + if (optionOrGroup.options) { + options.push(...optionOrGroup.options); + } else { + options.push(optionOrGroup); + } + return options; + }, + [] + ); +}; + +export const getSelectedOptionForSearchValue = ( + searchValue: string, + selectedOptions: EuiComboBoxOption[] +) => { + const normalizedSearchValue = searchValue.toLowerCase(); + return selectedOptions.find( + (option: any) => option.label.toLowerCase() === normalizedSearchValue + ); +}; + +const collectMatchingOption = ( + accumulator: EuiComboBoxOption[], + option: EuiComboBoxOption, + selectedOptions: EuiComboBoxOption[], + normalizedSearchValue: string, + isPreFiltered: boolean, + showPrevSelected: boolean +) => { + // Only show options which haven't yet been selected unless requested. + const selectedOption = getSelectedOptionForSearchValue( + option.label, + selectedOptions + ); + if (selectedOption && !showPrevSelected) { + return false; + } + + // If the options have already been pre-filtered then we can skip filtering against the search value. + if (isPreFiltered) { + accumulator.push(option); + return; + } + + if (!normalizedSearchValue) { + accumulator.push(option); + return; + } + + const normalizedOption = option.label.trim().toLowerCase(); + if (normalizedOption.includes(normalizedSearchValue)) { + accumulator.push(option); + } +}; + +export const getMatchingOptions = ( + options: EuiComboBoxOption[], + selectedOptions: EuiComboBoxOption[], + searchValue: string, + isPreFiltered: boolean, + showPrevSelected: boolean +) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + const matchingOptions: EuiComboBoxOption[] = []; + + options.forEach(option => { + if (option.options) { + const matchingOptionsForGroup: EuiComboBoxOption[] = []; + option.options.forEach((groupOption: EuiComboBoxOption) => { + collectMatchingOption( + matchingOptionsForGroup, + groupOption, + selectedOptions, + normalizedSearchValue, + isPreFiltered, + showPrevSelected + ); + }); + if (matchingOptionsForGroup.length > 0) { + // Add option for group label + matchingOptions.push({ label: option.label, isGroupLabelOption: true }); + // Add matching options for group + matchingOptions.push(...matchingOptionsForGroup); + } + } else { + collectMatchingOption( + matchingOptions, + option, + selectedOptions, + normalizedSearchValue, + isPreFiltered, + showPrevSelected + ); + } + }); + return matchingOptions; +}; From 132caf1c5829b663a5780d9e81aa4f9d6bd3ee9e Mon Sep 17 00:00:00 2001 From: Theo Date: Mon, 15 Apr 2019 13:58:36 +0100 Subject: [PATCH 2/4] `matching_options` deleted matching_options.js --- src/components/combo_box/matching_options.js | 69 -------------------- 1 file changed, 69 deletions(-) delete mode 100644 src/components/combo_box/matching_options.js diff --git a/src/components/combo_box/matching_options.js b/src/components/combo_box/matching_options.js deleted file mode 100644 index 34a8241128e..00000000000 --- a/src/components/combo_box/matching_options.js +++ /dev/null @@ -1,69 +0,0 @@ -export const flattenOptionGroups = optionsOrGroups => { - return optionsOrGroups.reduce((options, optionOrGroup) => { - if (optionOrGroup.options) { - options.push(...optionOrGroup.options); - } else { - options.push(optionOrGroup); - } - return options; - }, []); -}; - -export const getSelectedOptionForSearchValue = (searchValue, selectedOptions) => { - const normalizedSearchValue = searchValue.toLowerCase(); - return selectedOptions.find(option => option.label.toLowerCase() === normalizedSearchValue); -}; - -const collectMatchingOption = (accumulator, option, selectedOptions, normalizedSearchValue, isPreFiltered, showPrevSelected) => { - // Only show options which haven't yet been selected unless requested. - const selectedOption = getSelectedOptionForSearchValue(option.label, selectedOptions); - if (selectedOption && !showPrevSelected) { - return false; - } - - // If the options have already been prefiltered then we can skip filtering against the search value. - if (isPreFiltered) { - accumulator.push(option); - return; - } - - if (!normalizedSearchValue) { - accumulator.push(option); - return; - } - - const normalizedOption = option.label.trim().toLowerCase(); - if (normalizedOption.includes(normalizedSearchValue)) { - accumulator.push(option); - } -}; - -export const getMatchingOptions = (options, selectedOptions, searchValue, isPreFiltered, showPrevSelected) => { - const normalizedSearchValue = searchValue.trim().toLowerCase(); - const matchingOptions = []; - - options.forEach(option => { - if (option.options) { - const matchingOptionsForGroup = []; - option.options.forEach(groupOption => { - collectMatchingOption( - matchingOptionsForGroup, - groupOption, - selectedOptions, - normalizedSearchValue, - isPreFiltered, - showPrevSelected - ); - }); - if (matchingOptionsForGroup.length > 0) { - // Add option for group label - matchingOptions.push({ label: option.label, isGroupLabelOption: true }); - // Add matching options for group - matchingOptions.push(...matchingOptionsForGroup); - } - } else { - collectMatchingOption(matchingOptions, option, selectedOptions, normalizedSearchValue, isPreFiltered, showPrevSelected); - } - }); - return matchingOptions; -}; From 20047ac37a2a258795efa9cadffa0854437fb94f Mon Sep 17 00:00:00 2001 From: Theo Date: Mon, 15 Apr 2019 20:41:19 +0100 Subject: [PATCH 3/4] `matching_options` code review fixes for `EuiComboBoxOptionProps` --- src/components/combo_box/index.d.ts | 119 ++++++++++-------- .../combo_box/matching_options.test.ts | 14 +-- src/components/combo_box/matching_options.ts | 34 ++--- 3 files changed, 90 insertions(+), 77 deletions(-) diff --git a/src/components/combo_box/index.d.ts b/src/components/combo_box/index.d.ts index 25dd0b601df..76aa5733580 100644 --- a/src/components/combo_box/index.d.ts +++ b/src/components/combo_box/index.d.ts @@ -6,73 +6,84 @@ import { EuiComboBoxOptionsListPosition, EuiComboBoxOptionsListProps, } from '@elastic/eui'; -import { RefCallback } from '../common'; +import { RefCallback, CommonProps } from '../common'; declare module '@elastic/eui' { - export type EuiComboBoxOptionProps = ButtonHTMLAttributes & { - label: string, - isGroupLabelOption?: boolean, - } + export type EuiComboBoxOptionProps = CommonProps & + ButtonHTMLAttributes & { + label: string; + isGroupLabelOption?: boolean; + options?: EuiComboBoxOptionProps[]; + }; - export type EuiComboBoxOptionsListPosition = 'top' | 'bottom' + export type EuiComboBoxOptionsListPosition = 'top' | 'bottom'; export interface EuiComboBoxOption { - option: EuiComboBoxOptionProps, - children?: ReactNode, - className?: string, - optionRef?: RefCallback, - onClick: (option: EuiComboBoxOptionProps) => any, - onEnterKey: (option: EuiComboBoxOptionProps) => any, - disabled?: boolean, + option: EuiComboBoxOptionProps; + children?: ReactNode; + className?: string; + optionRef?: RefCallback; + onClick: (option: EuiComboBoxOptionProps) => any; + onEnterKey: (option: EuiComboBoxOptionProps) => any; + disabled?: boolean; } export interface EuiComboBoxOptionsListProps { - options?: Array, - isLoading?: boolean, - selectedOptions?: Array, - onCreateOption?: any, - searchValue?: string, - matchingOptions?: Array, - optionRef?: EuiComboBoxOption['optionRef'], - onOptionClick?: EuiComboBoxOption['onClick'], - onOptionEnterKey?: EuiComboBoxOption['onEnterKey'], - areAllOptionsSelected?: boolean, - getSelectedOptionForSearchValue?: (searchValue: string, selectedOptions: Array) => EuiComboBoxOptionProps, - updatePosition: (parameter?: UIEvent | EuiPanelProps['panelRef']) => any, - position?: EuiComboBoxOptionsListPosition, - listRef: EuiPanelProps['panelRef'], - renderOption?: (option: EuiComboBoxOptionProps, searchValue: string, OPTION_CONTENT_CLASSNAME: string) => ReactNode, - width?: number, - scrollToIndex?: number, - onScroll?: ListProps['onScroll'], - rowHeight?: number, - fullWidth?: boolean, + options?: EuiComboBoxOptionProps[]; + isLoading?: boolean; + selectedOptions?: any[]; + onCreateOption?: any; + searchValue?: string; + matchingOptions?: EuiComboBoxOptionProps[]; + optionRef?: EuiComboBoxOption['optionRef']; + onOptionClick?: EuiComboBoxOption['onClick']; + onOptionEnterKey?: EuiComboBoxOption['onEnterKey']; + areAllOptionsSelected?: boolean; + getSelectedOptionForSearchValue?: ( + searchValue: string, + selectedOptions: any[] + ) => EuiComboBoxOptionProps; + updatePosition: (parameter?: UIEvent | EuiPanelProps['panelRef']) => any; + position?: EuiComboBoxOptionsListPosition; + listRef: EuiPanelProps['panelRef']; + renderOption?: ( + option: EuiComboBoxOptionProps, + searchValue: string, + OPTION_CONTENT_CLASSNAME: string + ) => ReactNode; + width?: number; + scrollToIndex?: number; + onScroll?: ListProps['onScroll']; + rowHeight?: number; + fullWidth?: boolean; } - export const EuiComboBoxOptionsList: FunctionComponent; + export const EuiComboBoxOptionsList: FunctionComponent< + EuiComboBoxOptionsListProps + >; export type EuiComboBoxSingleSelectionShape = { asPlainText?: boolean; }; export interface EuiComboBoxProps { - id?: string, - isDisabled?: boolean, - className?: string, - placeholder?: string, - isLoading?: boolean, - async?: boolean, - singleSelection?: EuiComboBoxSingleSelectionShape | boolean, - noSuggestions?: boolean, - options?: EuiComboBoxOptionsListProps['options'], - selectedOptions?: EuiComboBoxOptionsListProps['selectedOptions'], - onBlur?: FocusEventHandler, - onChange?: (options: Array) => any, - onFocus?: FocusEventHandler, - onSearchChange?: (searchValue: string) => any, - onCreateOption?: EuiComboBoxOptionsListProps['onCreateOption'], - renderOption?: EuiComboBoxOptionsListProps['renderOption'], - isInvalid?: boolean, - rowHeight?: number, - isClearable?: boolean, - fullWidth?: boolean, + id?: string; + isDisabled?: boolean; + className?: string; + placeholder?: string; + isLoading?: boolean; + async?: boolean; + singleSelection?: EuiComboBoxSingleSelectionShape | boolean; + noSuggestions?: boolean; + options?: EuiComboBoxOptionsListProps['options']; + selectedOptions?: EuiComboBoxOptionsListProps['selectedOptions']; + onBlur?: FocusEventHandler; + onChange?: (options: Array) => any; + onFocus?: FocusEventHandler; + onSearchChange?: (searchValue: string) => any; + onCreateOption?: EuiComboBoxOptionsListProps['onCreateOption']; + renderOption?: EuiComboBoxOptionsListProps['renderOption']; + isInvalid?: boolean; + rowHeight?: number; + isClearable?: boolean; + fullWidth?: boolean; inputRef?: (element: HTMLInputElement) => void; } export const EuiComboBox: FunctionComponent; diff --git a/src/components/combo_box/matching_options.test.ts b/src/components/combo_box/matching_options.test.ts index a4117425411..09ff2c77e7e 100644 --- a/src/components/combo_box/matching_options.test.ts +++ b/src/components/combo_box/matching_options.test.ts @@ -1,11 +1,11 @@ +import { EuiComboBoxOptionProps } from '@elastic/eui'; import { - EuiComboBoxOption, flattenOptionGroups, getSelectedOptionForSearchValue, getMatchingOptions, } from './matching_options'; -const options: EuiComboBoxOption[] = [ +const options: EuiComboBoxOptionProps[] = [ { label: 'Titan', 'data-test-subj': 'titanOption', @@ -51,7 +51,7 @@ describe('flattenOptionGroups', () => { describe('getSelectedOptionForSearchValue', () => { test('gets the first matching selected option for search value', () => { // Assemble - const expected: EuiComboBoxOption = { + const expected: EuiComboBoxOptionProps = { label: 'Saturn', 'data-test-subj': 'saturnOption', }; @@ -71,7 +71,7 @@ describe('getSelectedOptionForSearchValue', () => { }); test('gets the first matching selected option for search value', () => { // Assemble - const expected: EuiComboBoxOption = { + const expected: EuiComboBoxOptionProps = { label: 'Saturn', 'data-test-subj': 'saturnOption', }; @@ -83,12 +83,12 @@ describe('getSelectedOptionForSearchValue', () => { }); interface GetMatchingOptionsTestCase { - options: EuiComboBoxOption[]; - selectedOptions: EuiComboBoxOption[]; + options: EuiComboBoxOptionProps[]; + selectedOptions: EuiComboBoxOptionProps[]; searchValue: string; isPreFiltered: boolean; showPrevSelected: boolean; - expected: EuiComboBoxOption[]; + expected: EuiComboBoxOptionProps[]; } const testCases: GetMatchingOptionsTestCase[] = [ diff --git a/src/components/combo_box/matching_options.ts b/src/components/combo_box/matching_options.ts index 535c7d5fcc4..5dbd44b5f31 100644 --- a/src/components/combo_box/matching_options.ts +++ b/src/components/combo_box/matching_options.ts @@ -1,11 +1,13 @@ -export interface EuiComboBoxOption { - label: string; - [prop: string]: any; -} +import { EuiComboBoxOptionProps } from '@elastic/eui'; -export const flattenOptionGroups = (optionsOrGroups: EuiComboBoxOption[]) => { +export const flattenOptionGroups = ( + optionsOrGroups: EuiComboBoxOptionProps[] +) => { return optionsOrGroups.reduce( - (options: EuiComboBoxOption[], optionOrGroup: EuiComboBoxOption) => { + ( + options: EuiComboBoxOptionProps[], + optionOrGroup: EuiComboBoxOptionProps + ) => { if (optionOrGroup.options) { options.push(...optionOrGroup.options); } else { @@ -19,18 +21,18 @@ export const flattenOptionGroups = (optionsOrGroups: EuiComboBoxOption[]) => { export const getSelectedOptionForSearchValue = ( searchValue: string, - selectedOptions: EuiComboBoxOption[] + selectedOptions: EuiComboBoxOptionProps[] ) => { const normalizedSearchValue = searchValue.toLowerCase(); return selectedOptions.find( - (option: any) => option.label.toLowerCase() === normalizedSearchValue + option => option.label.toLowerCase() === normalizedSearchValue ); }; const collectMatchingOption = ( - accumulator: EuiComboBoxOption[], - option: EuiComboBoxOption, - selectedOptions: EuiComboBoxOption[], + accumulator: EuiComboBoxOptionProps[], + option: EuiComboBoxOptionProps, + selectedOptions: EuiComboBoxOptionProps[], normalizedSearchValue: string, isPreFiltered: boolean, showPrevSelected: boolean @@ -62,19 +64,19 @@ const collectMatchingOption = ( }; export const getMatchingOptions = ( - options: EuiComboBoxOption[], - selectedOptions: EuiComboBoxOption[], + options: EuiComboBoxOptionProps[], + selectedOptions: EuiComboBoxOptionProps[], searchValue: string, isPreFiltered: boolean, showPrevSelected: boolean ) => { const normalizedSearchValue = searchValue.trim().toLowerCase(); - const matchingOptions: EuiComboBoxOption[] = []; + const matchingOptions: EuiComboBoxOptionProps[] = []; options.forEach(option => { if (option.options) { - const matchingOptionsForGroup: EuiComboBoxOption[] = []; - option.options.forEach((groupOption: EuiComboBoxOption) => { + const matchingOptionsForGroup: EuiComboBoxOptionProps[] = []; + option.options.forEach((groupOption: EuiComboBoxOptionProps) => { collectMatchingOption( matchingOptionsForGroup, groupOption, From e99c321a94d21cc5023551be3fd3f7bb9866633e Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 17 Apr 2019 14:17:03 +0100 Subject: [PATCH 4/4] `matching_options` updated changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512c293aae1..b37a9586504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `10.0.1`. +- Converted `matching_options` to TS ([#1828](https://github.com/elastic/eui/pull/1828)) ## [`10.0.1`](https://github.com/elastic/eui/tree/v10.0.1) -- Convert `EuiText`, `EuiTextColor` and `EuiTextAlign` to TS ([#1791](https://github.com/elastic/eui/pull/1791)) +- Converted `EuiText`, `EuiTextColor` and `EuiTextAlign` to TS ([#1791](https://github.com/elastic/eui/pull/1791)) - Updated `IconColor` type to better distinguish between accepted types ([#1842](https://github.com/elastic/eui/pull/1842)) ## [`10.0.0`](https://github.com/elastic/eui/tree/v10.0.0)