diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47d55dd0fe8..5b089c8b4cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## [`main`](https://github.com/elastic/eui/tree/main)
+- Added virtulized rendering option to `EuiSelectableList` with `isVirtualized` ([#5521](https://github.com/elastic/eui/pull/5521))
+- Added expanded option properties to `EuiSelectableOption` with `data` ([#5521](https://github.com/elastic/eui/pull/5521))
+
**Breaking changes**
- Changed `EuiSearchBar` to preserve phrases with leading and trailing spaces, instead of dropping surrounding whitespace ([#5514](https://github.com/elastic/eui/pull/5514))
diff --git a/src-docs/src/views/selectable/selectable_custom_render.js b/src-docs/src/views/selectable/selectable_custom_render.js
index 75e4f2050e8..ef4c3dcea12 100644
--- a/src-docs/src/views/selectable/selectable_custom_render.js
+++ b/src-docs/src/views/selectable/selectable_custom_render.js
@@ -12,6 +12,7 @@ import { createDataStore } from '../tables/data_store';
export default () => {
const [useCustomContent, setUseCustomContent] = useState(false);
+ const [isVirtualized, setIsVirtualized] = useState(true);
const countries = createDataStore().countries.map((country) => {
return {
@@ -20,6 +21,9 @@ export default () => {
prepend: country.flag,
append: {country.code},
showIcons: false,
+ data: {
+ secondaryContent: 'I am secondary content, I am!',
+ },
};
});
@@ -38,15 +42,19 @@ export default () => {
setUseCustomContent(e.currentTarget.checked);
};
+ const onVirtualized = (e) => {
+ setIsVirtualized(e.currentTarget.checked);
+ };
+
const renderCountryOption = (option, searchValue) => {
return (
<>
{option.label}
-
-
+ {/*
*/}
+
- I am secondary content, I am!
+ {option.secondaryContent}
@@ -54,33 +62,42 @@ export default () => {
);
};
+ let listProps = {
+ isVirtualized,
+ };
+
let customProps;
if (useCustomContent) {
customProps = {
height: 240,
renderOption: renderCountryOption,
- listProps: {
- rowHeight: 50,
- showIcons: false,
- },
+ };
+ listProps = {
+ rowHeight: 50,
+ isVirtualized,
};
}
return (
<>
+ {' '}
+
-
-
{(list, search) => (
diff --git a/src-docs/src/views/selectable/selectable_example.js b/src-docs/src/views/selectable/selectable_example.js
index 9f36c676b1f..cfc07fc6491 100644
--- a/src-docs/src/views/selectable/selectable_example.js
+++ b/src-docs/src/views/selectable/selectable_example.js
@@ -347,6 +347,20 @@ export const SelectableExample = {
similar to a title. Add one of these by setting the{' '}
option.isGroupLabel to true.{' '}
+ Row height and virtualization
+
+ When virtualization is on,{' '}
+ every row must be the same height in order for the
+ list to know how to scroll to the selected or highlighted option. It
+ applies the listProps.rowHeight (in pixels)
+ directly to each option hiding any overflow.
+
+
+ If listProps.isVirtualized is set to{' '}
+ false, each row will fit its contents and removes
+ all scrolling. Therefore, we recommend having a large enough
+ container to accomodate all optons.
+
Custom content
While it is best to stick to the{' '}
@@ -357,15 +371,17 @@ export const SelectableExample = {
searchValue to use for highlighting.
- In order for the list to know how to scroll to the selected or
- highlighted option, it must also know the height of the rows. It
- applies this pixel height directly to options. If your custom
- content is taller than the default of 32px tall,
- you will need to recalculate this height and apply it via{' '}
- listProps.rowHeight.
+ To provide data that can be used by the{' '}
+ renderOption function that does not match the
+ standard option API, use option.data which will
+ make custom data available in the option{' '}
+ parameter. See the secondaryContent configuration
+ in the following example.
- Every row must be the same height.
+ Also, if your custom content is taller than the default{' '}
+ listProps.rowHeight of 32px{' '}
+ tall, you will need to pass in a custom value to this prop.
),
diff --git a/src/components/selectable/__snapshots__/selectable.test.tsx.snap b/src/components/selectable/__snapshots__/selectable.test.tsx.snap
index 35ce39017b3..dfb568309cf 100644
--- a/src/components/selectable/__snapshots__/selectable.test.tsx.snap
+++ b/src/components/selectable/__snapshots__/selectable.test.tsx.snap
@@ -1,5 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`EuiSelectable custom options with data 1`] = `
+
+
+
+
+
+ -
+
+
+
+
+ VI: Titan
+
+
+
+
+ -
+
+
+
+
+ II: Enceladus
+
+
+
+
+ -
+
+
+
+
+ XVII: Pandora is one of Saturn's moons, named for a Titaness of Greek mythology
+
+
+
+
+
+
+
+
+
+`;
+
exports[`EuiSelectable is rendered 1`] = `
{
(component.find('EuiSelectableList').props() as any).visibleOptions
).toEqual(options);
});
+
+ test('with data', () => {
+ type WithData = {
+ numeral?: string;
+ };
+ const options = [
+ {
+ label: 'Titan',
+ data: {
+ numeral: 'VI',
+ },
+ },
+ {
+ label: 'Enceladus',
+ data: {
+ numeral: 'II',
+ },
+ },
+ {
+ label:
+ "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
+ data: {
+ numeral: 'XVII',
+ },
+ },
+ ];
+ const component = render(
+
+ options={options}
+ renderOption={(option) => {
+ return (
+
+ {option.numeral}: {option.label}
+
+ );
+ }}
+ >
+ {(list) => list}
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
});
});
diff --git a/src/components/selectable/selectable.tsx b/src/components/selectable/selectable.tsx
index b2cd6af939d..5d4a63e8550 100644
--- a/src/components/selectable/selectable.tsx
+++ b/src/components/selectable/selectable.tsx
@@ -18,7 +18,10 @@ import classNames from 'classnames';
import { CommonProps, ExclusiveUnion } from '../common';
import { EuiSelectableSearch } from './selectable_search';
import { EuiSelectableMessage } from './selectable_message';
-import { EuiSelectableList } from './selectable_list';
+import {
+ EuiSelectableList,
+ EuiSelectableOptionsListVirtualizedProps,
+} from './selectable_list';
import { EuiLoadingSpinner } from '../loading';
import { EuiSpacer } from '../spacer';
import { getMatchingOptions } from './matching_options';
@@ -434,8 +437,23 @@ export class EuiSelectable extends Component<
const {
'aria-label': listAriaLabel,
'aria-describedby': listAriaDescribedby,
+ isVirtualized,
+ rowHeight,
...cleanedListProps
- } = listProps || unknownAccessibleName;
+ } = (listProps || unknownAccessibleName) as typeof listProps &
+ typeof unknownAccessibleName;
+
+ let virtualizedProps: EuiSelectableOptionsListVirtualizedProps;
+
+ if (isVirtualized === false) {
+ virtualizedProps = {
+ isVirtualized,
+ };
+ } else if (rowHeight != null) {
+ virtualizedProps = {
+ rowHeight,
+ };
+ }
const classes = classNames(
'euiSelectable',
@@ -629,6 +647,7 @@ export class EuiSelectable extends Component<
? listAccessibleName
: searchable && { 'aria-label': placeholderName })}
{...cleanedListProps}
+ {...virtualizedProps}
/>
)}
diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap
index 0a69ca20b06..e695d3effb2 100644
--- a/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap
+++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap
@@ -1087,6 +1087,171 @@ exports[`EuiSelectableListItem props height is full 1`] = `
`;
+exports[`EuiSelectableListItem props isVirtualized can be false 1`] = `
+
+
+
+ -
+
+
+
+
+ Titan
+
+
+
+
+ -
+
+
+
+
+ Enceladus
+
+
+
+
+ -
+
+
+
+
+ Mimas
+
+
+
+
+ -
+
+
+
+
+ Pandora is one of Saturn's moons, named for a Titaness of Greek mythology
+
+
+
+
+ -
+
+
+
+
+ Tethys
+
+
+
+
+ -
+
+
+
+
+ Hyperion
+
+
+
+
+
+
+
+`;
+
exports[`EuiSelectableListItem props renderOption 1`] = `
{
expect(component).toMatchSnapshot();
});
+
+ test('isVirtualized can be false', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
});
});
diff --git a/src/components/selectable/selectable_list/selectable_list.tsx b/src/components/selectable/selectable_list/selectable_list.tsx
index a4cd93b029c..98085662232 100644
--- a/src/components/selectable/selectable_list/selectable_list.tsx
+++ b/src/components/selectable/selectable_list/selectable_list.tsx
@@ -6,7 +6,13 @@
* Side Public License, v 1.
*/
-import React, { Component, HTMLAttributes, ReactNode, memo } from 'react';
+import React, {
+ Component,
+ HTMLAttributes,
+ ReactNode,
+ memo,
+ CSSProperties,
+} from 'react';
import classNames from 'classnames';
import {
FixedSizeList,
@@ -14,7 +20,7 @@ import {
ListChildComponentProps as ReactWindowListChildComponentProps,
areEqual,
} from 'react-window';
-import { CommonProps } from '../../common';
+import { CommonProps, ExclusiveUnion } from '../../common';
import { EuiAutoSizer } from '../../auto_sizer';
import { EuiHighlight } from '../../highlight';
import { EuiSelectableOption } from '../selectable_option';
@@ -24,10 +30,29 @@ import {
} from './selectable_list_item';
interface ListChildComponentProps
- extends ReactWindowListChildComponentProps {
+ extends Omit {
data: Array>;
+ style?: CSSProperties;
}
+export type EuiSelectableOptionsListVirtualizedProps = ExclusiveUnion<
+ {
+ /**
+ * Use virtualized rendering for list items with `react-window`.
+ * Sets each row's height to the value of `rowHeight`.
+ */
+ isVirtualized?: true;
+ /**
+ * The height of each option in pixels. Defaults to `32`.
+ * Has no effect if `isVirtualized=false`.
+ */
+ rowHeight: number;
+ },
+ {
+ isVirtualized: false;
+ }
+>;
+
// Consumer Configurable Props via `EuiSelectable.listProps`
export type EuiSelectableOptionsListProps = CommonProps &
HTMLAttributes & {
@@ -37,10 +62,6 @@ export type EuiSelectableOptionsListProps = CommonProps &
* directly to that option
*/
activeOptionIndex?: number;
- /**
- * The height of each option in pixels. Defaults to `32`
- */
- rowHeight: number;
/**
* Show the check/cross selection indicator icons
*/
@@ -61,7 +82,7 @@ export type EuiSelectableOptionsListProps = CommonProps &
* The default content when `true` is `↩ to select/deselect/include/exclude`
*/
onFocusBadge?: EuiSelectableListItemProps['onFocusBadge'];
- };
+ } & EuiSelectableOptionsListVirtualizedProps;
export type EuiSelectableListProps = EuiSelectableOptionsListProps & {
/**
@@ -109,6 +130,7 @@ export class EuiSelectableList extends Component> {
static defaultProps = {
rowHeight: 32,
searchValue: '',
+ isVirtualized: true,
};
listRef: FixedSizeList | null = null;
@@ -186,6 +208,7 @@ export class EuiSelectableList extends Component> {
ListRow = memo(({ data, index, style }: ListChildComponentProps) => {
const option = data[index];
+ const { data: optionData, ..._option } = option;
const {
label,
isGroupLabel,
@@ -196,6 +219,7 @@ export class EuiSelectableList extends Component> {
ref,
key,
searchableLabel,
+ data: _data,
...optionRest
} = option;
@@ -241,7 +265,11 @@ export class EuiSelectableList extends Component> {
{...(optionRest as EuiSelectableListItemProps)}
>
{this.props.renderOption ? (
- this.props.renderOption(option, this.props.searchValue)
+ this.props.renderOption(
+ // @ts-ignore complex
+ { ..._option, ...optionData },
+ this.props.searchValue
+ )
) : (
{label}
)}
@@ -273,6 +301,7 @@ export class EuiSelectableList extends Component> {
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
'aria-describedby': ariaDescribedby,
+ isVirtualized,
...rest
} = this.props;
@@ -293,9 +322,9 @@ export class EuiSelectableList extends Component> {
if (numVisibleMoreThanMax) {
// Show only half of the last one to indicate there's more to scroll to
- calculatedHeight = (maxVisibleOptions - 0.5) * rowHeight;
+ calculatedHeight = (maxVisibleOptions - 0.5) * rowHeight!;
} else {
- calculatedHeight = numVisibleOptions * rowHeight;
+ calculatedHeight = numVisibleOptions * rowHeight!;
}
}
@@ -310,26 +339,47 @@ export class EuiSelectableList extends Component> {
return (
-
- {({ width, height }) => (
-
- {this.ListRow}
-
- )}
-
+ {isVirtualized ? (
+
+ {({ width, height }) => (
+
+ {this.ListRow}
+
+ )}
+
+ ) : (
+
+
+ {optionArray.map((_, index) =>
+ React.createElement(
+ this.ListRow,
+ {
+ key: index,
+ data: optionArray,
+ index,
+ },
+ null
+ )
+ )}
+
+
+ )}
);
}
diff --git a/src/components/selectable/selectable_option.tsx b/src/components/selectable/selectable_option.tsx
index 7b4ca6bfbcb..17fdaeafb54 100644
--- a/src/components/selectable/selectable_option.tsx
+++ b/src/components/selectable/selectable_option.tsx
@@ -53,6 +53,11 @@ export type EuiSelectableOptionBase = CommonProps & {
* Option item `id`s are coordinated at a higher level for a11y reasons.
*/
id?: never;
+ /**
+ * Option data to pass through to the `renderOptions` element.
+ * Bypass `EuiSelectableItem` and avoid DOM attribute warnings.
+ */
+ data?: { [key: string]: any };
};
type _EuiSelectableGroupLabelOption = Omit<