Skip to content

Commit

Permalink
Update and expand use of EuiSelectable option type generic (#3983)
Browse files Browse the repository at this point in the history
* update euiselectable option type generic

* better tests

* CL
  • Loading branch information
thompsongl authored Sep 1, 2020
1 parent 7cf5d62 commit 35fa2eb
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Bug fixes**

- Fixed bug in `EuiComboBox` where the input was dropping to the next line when a `EuiBadge` had a very long text ([#3968](https://github.com/elastic/eui/pull/3968))
- Fixed type mismatch between `EuiSelectable` options extended via `EuiSelectableOption` and internal option types ([#3983](https://github.com/elastic/eui/pull/3983))

## [`28.3.0`](https://github.com/elastic/eui/tree/v28.3.0)

Expand Down
34 changes: 17 additions & 17 deletions src/components/selectable/matching_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,36 @@

import { EuiSelectableOption } from './selectable_option';

const getSearchableLabel = (
option: EuiSelectableOption,
const getSearchableLabel = <T>(
option: EuiSelectableOption<T>,
normalize: boolean = true
): string => {
const searchableLabel = option.searchableLabel || option.label;
return normalize ? searchableLabel.trim().toLowerCase() : searchableLabel;
};

const getSelectedOptionForSearchValue = (
const getSelectedOptionForSearchValue = <T>(
searchValue: string,
selectedOptions: EuiSelectableOption[]
selectedOptions: Array<EuiSelectableOption<T>>
) => {
const normalizedSearchValue = searchValue.toLowerCase();
return selectedOptions.find(
option => getSearchableLabel(option) === normalizedSearchValue
option => getSearchableLabel<T>(option) === normalizedSearchValue
);
};

const collectMatchingOption = (
accumulator: EuiSelectableOption[],
option: EuiSelectableOption,
const collectMatchingOption = <T>(
accumulator: Array<EuiSelectableOption<T>>,
option: EuiSelectableOption<T>,
normalizedSearchValue: string,
isPreFiltered?: boolean,
selectedOptions?: EuiSelectableOption[]
selectedOptions?: Array<EuiSelectableOption<T>>
) => {
// Don't show options that have already been requested if
// the selectedOptions list exists
if (selectedOptions) {
const selectedOption = getSelectedOptionForSearchValue(
getSearchableLabel(option, false),
const selectedOption = getSelectedOptionForSearchValue<T>(
getSearchableLabel<T>(option, false),
selectedOptions
);
if (selectedOption) {
Expand All @@ -68,17 +68,17 @@ const collectMatchingOption = (
return;
}

const normalizedOption = getSearchableLabel(option);
const normalizedOption = getSearchableLabel<T>(option);
if (normalizedOption.includes(normalizedSearchValue)) {
accumulator.push(option);
}
};

export const getMatchingOptions = (
export const getMatchingOptions = <T>(
/**
* All available options to match against
*/
options: EuiSelectableOption[],
options: Array<EuiSelectableOption<T>>,
/**
* String to match option.label || option.searchableLabel against
*/
Expand All @@ -91,13 +91,13 @@ export const getMatchingOptions = (
* To exclude selected options from the search list,
* pass the array of selected options
*/
selectedOptions?: EuiSelectableOption[]
selectedOptions?: Array<EuiSelectableOption<T>>
) => {
const normalizedSearchValue = searchValue.toLowerCase();
const matchingOptions: EuiSelectableOption[] = [];
const matchingOptions: Array<EuiSelectableOption<T>> = [];

options.forEach(option => {
collectMatchingOption(
collectMatchingOption<T>(
matchingOptions,
option,
normalizedSearchValue,
Expand Down
75 changes: 73 additions & 2 deletions src/components/selectable/selectable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
*/

import React from 'react';
import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';
import { mount, render } from 'enzyme';
import { requiredProps } from '../../test';

import { EuiSelectable } from './selectable';
import { EuiSelectableOption } from './selectable_option';
Expand Down Expand Up @@ -129,4 +129,75 @@ describe('EuiSelectable', () => {
expect(component).toMatchSnapshot();
});
});

describe('custom options', () => {
test('optional properties', () => {
type OptionalOption = EuiSelectableOption<{ value?: string }>;
const options: OptionalOption[] = [
{
label: 'Titan',
'data-test-subj': 'titanOption',
value: 'titan',
},
{
label: 'Enceladus',
value: 'enceladus',
},
{
label:
"Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
},
];

const onChange = (options: OptionalOption[]) => {
jest.fn(() => options);
};

const component = mount(
<EuiSelectable<OptionalOption> options={options} onChange={onChange}>
{list => list}
</EuiSelectable>
);

expect(
(component.find('EuiSelectableList').props() as any).visibleOptions
).toEqual(options);
});

test('required properties', () => {
type ExtendedOption = EuiSelectableOption<{ value: string }>;
const options: ExtendedOption[] = [
{
label: 'Titan',
'data-test-subj': 'titanOption',
value: 'titan',
},
{
label: 'Enceladus',
value: 'enceladus',
},
{
label:
"Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
value: 'pandora',
},
];

const onChange = (options: ExtendedOption[]) => {
jest.fn(() => options);
};

const component = mount(
<EuiSelectable<ExtendedOption> options={options} onChange={onChange}>
{list => list}
</EuiSelectable>
);

component.update();

expect(
(component.find('EuiSelectableList').props() as any).visibleOptions
).toEqual(options);
});
});
});
52 changes: 26 additions & 26 deletions src/components/selectable/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type EuiSelectableOptionsListPropsWithDefaults = RequiredEuiSelectableOptionsLis
Partial<OptionalEuiSelectableOptionsListProps>;

// `searchProps` can only be specified when `searchable` is true
type EuiSelectableSearchableProps = ExclusiveUnion<
type EuiSelectableSearchableProps<T> = ExclusiveUnion<
{
searchable: false;
},
Expand All @@ -63,13 +63,13 @@ type EuiSelectableSearchableProps = ExclusiveUnion<
/**
* Passes props down to the `EuiFieldSearch`
*/
searchProps?: Partial<EuiSelectableSearchProps>;
searchProps?: Partial<EuiSelectableSearchProps<T>>;
}
>;

export type EuiSelectableProps = CommonProps &
export type EuiSelectableProps<T = {}> = CommonProps &
Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> &
EuiSelectableSearchableProps & {
EuiSelectableSearchableProps<T> & {
/**
* Function that takes the `list` node and then
* the `search` node (if `searchable` is applied)
Expand All @@ -78,16 +78,16 @@ export type EuiSelectableProps = CommonProps &
list: ReactElement<
typeof EuiSelectableMessage | typeof EuiSelectableList
>,
search: ReactElement<EuiSelectableSearch> | undefined
search: ReactElement<EuiSelectableSearch<T>> | undefined
) => ReactNode;
/**
* Array of EuiSelectableOption objects. See #EuiSelectableOptionProps
*/
options: Array<Exclude<EuiSelectableOption, 'id'>>;
options: Array<EuiSelectableOption<T>>;
/**
* Passes back the altered `options` array with selected options as
*/
onChange?: (options: EuiSelectableOption[]) => void;
onChange?: (options: Array<EuiSelectableOption<T>>) => void;
/**
* Sets the single selection policy of
* `false`: allows multiple selection
Expand Down Expand Up @@ -118,7 +118,7 @@ export type EuiSelectableProps = CommonProps &
* Returns `(option, searchValue)`
*/
renderOption?: (
option: EuiSelectableOption,
option: EuiSelectableOption<T>,
searchValue: string
) => ReactNode;
/**
Expand All @@ -138,33 +138,33 @@ export type EuiSelectableProps = CommonProps &
emptyMessage?: ReactElement | string;
};

export interface EuiSelectableState {
export interface EuiSelectableState<T> {
activeOptionIndex?: number;
searchValue: string;
visibleOptions: EuiSelectableOption[];
visibleOptions: Array<EuiSelectableOption<T>>;
isFocused: boolean;
}

export class EuiSelectable extends Component<
EuiSelectableProps,
EuiSelectableState
export class EuiSelectable<T = {}> extends Component<
EuiSelectableProps<T>,
EuiSelectableState<T>
> {
static defaultProps = {
options: [],
singleSelection: false,
searchable: false,
};

private optionsListRef = createRef<EuiSelectableList>();
private optionsListRef = createRef<EuiSelectableList<T>>();
rootId = htmlIdGenerator();
constructor(props: EuiSelectableProps) {
constructor(props: EuiSelectableProps<T>) {
super(props);

const { options, singleSelection } = props;

const initialSearchValue = '';

const visibleOptions = getMatchingOptions(options, initialSearchValue);
const visibleOptions = getMatchingOptions<T>(options, initialSearchValue);

// ensure that the currently selected single option is active if it is in the visibleOptions
const selectedOptions = options.filter(option => option.checked);
Expand All @@ -183,14 +183,14 @@ export class EuiSelectable extends Component<
};
}

static getDerivedStateFromProps(
nextProps: EuiSelectableProps,
prevState: EuiSelectableState
static getDerivedStateFromProps<T>(
nextProps: EuiSelectableProps<T>,
prevState: EuiSelectableState<T>
) {
const { options } = nextProps;
const { activeOptionIndex, searchValue } = prevState;

const matchingOptions = getMatchingOptions(options, searchValue);
const matchingOptions = getMatchingOptions<T>(options, searchValue);

const stateUpdate = { visibleOptions: matchingOptions, activeOptionIndex };

Expand Down Expand Up @@ -318,7 +318,7 @@ export class EuiSelectable extends Component<
};

onSearchChange = (
visibleOptions: EuiSelectableOption[],
visibleOptions: Array<EuiSelectableOption<T>>,
searchValue: string
) => {
this.setState(
Expand All @@ -342,9 +342,9 @@ export class EuiSelectable extends Component<
});
};

onOptionClick = (options: EuiSelectableOption[]) => {
onOptionClick = (options: Array<EuiSelectableOption<T>>) => {
this.setState(state => ({
visibleOptions: getMatchingOptions(options, state.searchValue),
visibleOptions: getMatchingOptions<T>(options, state.searchValue),
activeOptionIndex: this.state.activeOptionIndex,
}));
if (this.props.onChange) {
Expand Down Expand Up @@ -492,7 +492,7 @@ export class EuiSelectable extends Component<
*/
const getAccessibleName = (
props:
| Partial<EuiSelectableSearchProps>
| Partial<EuiSelectableSearchProps<T>>
| EuiSelectableOptionsListPropsWithDefaults
| undefined,
messageContentId?: string
Expand Down Expand Up @@ -534,7 +534,7 @@ export class EuiSelectable extends Component<
const search = searchable ? (
<EuiI18n token="euiSelectable.placeholderName" default="Filter options">
{(placeholderName: string) => (
<EuiSelectableSearch
<EuiSelectableSearch<T>
key="listSearch"
options={options}
onChange={this.onSearchChange}
Expand Down Expand Up @@ -565,7 +565,7 @@ export class EuiSelectable extends Component<
) : (
<EuiI18n token="euiSelectable.placeholderName" default="Filter options">
{(placeholderName: string) => (
<EuiSelectableList
<EuiSelectableList<T>
key="list"
options={options}
visibleOptions={visibleOptions}
Expand Down
Loading

0 comments on commit 35fa2eb

Please sign in to comment.