Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
35 changes: 26 additions & 9 deletions src-docs/src/views/selectable/selectable_custom_render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,6 +21,9 @@ export default () => {
prepend: country.flag,
append: <EuiBadge>{country.code}</EuiBadge>,
showIcons: false,
data: {
secondaryContent: 'I am secondary content, I am!',
},
};
});

Expand All @@ -38,49 +42,62 @@ export default () => {
setUseCustomContent(e.currentTarget.checked);
};

const onVirtualized = (e) => {
setIsVirtualized(e.currentTarget.checked);
};

const renderCountryOption = (option, searchValue) => {
return (
<>
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>
<br />
<EuiTextColor color="subdued">
{/* <br /> */}
<EuiTextColor style={{ display: 'block' }} color="subdued">
<small>
<EuiHighlight search={searchValue}>
I am secondary content, I am!
{option.secondaryContent}
</EuiHighlight>
</small>
</EuiTextColor>
</>
);
};

let listProps = {
isVirtualized,
};

let customProps;
if (useCustomContent) {
customProps = {
height: 240,
renderOption: renderCountryOption,
listProps: {
rowHeight: 50,
showIcons: false,
},
};
listProps = {
rowHeight: 50,
isVirtualized,
};
}

return (
<>
<EuiSwitch
label="Virtualized"
checked={isVirtualized}
onChange={onVirtualized}
/>{' '}
&emsp;
<EuiSwitch
label="Custom content"
checked={useCustomContent}
onChange={onCustom}
/>

<EuiSpacer />

<EuiSelectable
aria-label="Selectable example with custom list items"
searchable
options={options}
onChange={onChange}
listProps={listProps}
{...customProps}
>
{(list, search) => (
Expand Down
30 changes: 23 additions & 7 deletions src-docs/src/views/selectable/selectable_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ export const SelectableExample = {
similar to a title. Add one of these by setting the{' '}
<EuiCode>option.isGroupLabel</EuiCode> to true.{' '}
</p>
<h3>Row height and virtualization</h3>
<p>
When virtualization is on,{' '}
<strong>every row must be the same height</strong> in order for the
list to know how to scroll to the selected or highlighted option. It
applies the <EuiCode>listProps.rowHeight</EuiCode> (in pixels)
directly to each option hiding any overflow.
</p>
<p>
If <EuiCode>listProps.isVirtualized</EuiCode> is set to{' '}
<EuiCode>false</EuiCode>, each row will fit its contents and removes
all scrolling. Therefore, we recommend having a large enough
container to accomodate all optons.
</p>
<h3>Custom content</h3>
<p>
While it is best to stick to the{' '}
Expand All @@ -357,15 +371,17 @@ export const SelectableExample = {
<EuiCode>searchValue</EuiCode> to use for highlighting.
</p>
<p>
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 <EuiCode>32px</EuiCode> tall,
you will need to recalculate this height and apply it via{' '}
<EuiCode>listProps.rowHeight</EuiCode>.
To provide data that can be used by the{' '}
<EuiCode>renderOption</EuiCode> function that does not match the
standard option API, use <EuiCode>option.data</EuiCode> which will
make custom data available in the <EuiCode>option</EuiCode>{' '}
parameter. See the <EuiCode>secondaryContent</EuiCode> configuration
in the following example.
</p>
<p>
<strong>Every row must be the same height.</strong>
Also, if your custom content is taller than the default{' '}
<EuiCode>listProps.rowHeight</EuiCode> of <EuiCode>32px</EuiCode>{' '}
tall, you will need to pass in a custom value to this prop.
</p>
</Fragment>
),
Expand Down
102 changes: 102 additions & 0 deletions src/components/selectable/__snapshots__/selectable.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`EuiSelectable custom options with data 1`] = `
<div
class="euiSelectable"
>
<div
class="euiSelectableList"
>
<div
data-eui="EuiAutoSizer"
>
<div
class="euiSelectableList__list"
style="position:relative;height:96px;width:600px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:transform;direction:ltr"
>
<ul
style="height:96px;width:100%"
>
<li
aria-posinset="1"
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem"
id="generated-id_listbox_option-0"
role="option"
style="position:absolute;left:0;top:0;height:32px;width:100%"
title="Titan"
>
<span
class="euiSelectableListItem__content"
>
<span
class="euiSelectableListItem__icon"
data-euiicon-type="empty"
/>
<span
class="euiSelectableListItem__text"
>
<span>
VI: Titan
</span>
</span>
</span>
</li>
<li
aria-posinset="2"
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem"
id="generated-id_listbox_option-1"
role="option"
style="position:absolute;left:0;top:32px;height:32px;width:100%"
title="Enceladus"
>
<span
class="euiSelectableListItem__content"
>
<span
class="euiSelectableListItem__icon"
data-euiicon-type="empty"
/>
<span
class="euiSelectableListItem__text"
>
<span>
II: Enceladus
</span>
</span>
</span>
</li>
<li
aria-posinset="3"
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem"
id="generated-id_listbox_option-2"
role="option"
style="position:absolute;left:0;top:64px;height:32px;width:100%"
title="Pandora is one of Saturn's moons, named for a Titaness of Greek mythology"
>
<span
class="euiSelectableListItem__content"
>
<span
class="euiSelectableListItem__icon"
data-euiicon-type="empty"
/>
<span
class="euiSelectableListItem__text"
>
<span>
XVII: Pandora is one of Saturn's moons, named for a Titaness of Greek mythology
</span>
</span>
</span>
</li>
</ul>
</div>
</div>
</div>
</div>
`;

exports[`EuiSelectable is rendered 1`] = `
<div
class="euiSelectable testClass1 testClass2"
Expand Down
43 changes: 43 additions & 0 deletions src/components/selectable/selectable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,48 @@ describe('EuiSelectable', () => {
(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(
<EuiSelectable<WithData>
options={options}
renderOption={(option) => {
return (
<span>
{option.numeral}: {option.label}
</span>
);
}}
>
{(list) => list}
</EuiSelectable>
);

expect(component).toMatchSnapshot();
});
});
});
23 changes: 21 additions & 2 deletions src/components/selectable/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -434,8 +437,23 @@ export class EuiSelectable<T = {}> 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',
Expand Down Expand Up @@ -629,6 +647,7 @@ export class EuiSelectable<T = {}> extends Component<
? listAccessibleName
: searchable && { 'aria-label': placeholderName })}
{...cleanedListProps}
{...virtualizedProps}
/>
)}
</EuiI18n>
Expand Down
Loading