Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] Add example of EuiSelectable in an EuiInputPopover #7683

Merged
merged 7 commits into from
Apr 16, 2024
Merged
1 change: 1 addition & 0 deletions changelogs/upcoming/7683.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiSelectable`'s `isPreFiltered` prop to allow passing a configuration object, which allows disabling search highlighting in addition to search filtering
292 changes: 189 additions & 103 deletions src-docs/src/views/selectable/selectable_sizing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,102 +9,154 @@ import {
EuiPopoverFooter,
EuiPopoverTitle,
EuiSelectable,
EuiSelectableOption,
type EuiSelectableOption,
type EuiSelectableProps,
EuiSpacer,
EuiTitle,
EuiInputPopover,
} from '../../../../src';

export default () => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const OPTIONS: EuiSelectableOption[] = [
{ label: 'Titan' },
{ label: 'Enceladus' },
{ label: 'Mimas', checked: 'on' },
{ label: 'Dione' },
{ label: 'Iapetus' },
{ label: 'Phoebe' },
{ label: 'Rhea' },
{ label: 'Pandora' },
{ label: 'Tethys' },
{ label: 'Hyperion' },
{ label: 'Pan' },
{ label: 'Atlas' },
{ label: 'Prometheus' },
{ label: 'Janus' },
{ label: 'Epimetheus' },
{ label: 'Amalthea' },
{ label: 'Thebe' },
{ label: 'Io' },
{ label: 'Europa' },
{ label: 'Ganymede' },
{ label: 'Callisto' },
{ label: 'Himalia' },
{ label: 'Phobos' },
{ label: 'Deimos' },
{ label: 'Puck' },
{ label: 'Miranda' },
{ label: 'Ariel' },
{ label: 'Umbriel' },
{ label: 'Titania' },
{ label: 'Oberon' },
{ label: 'Despina' },
{ label: 'Galatea' },
{ label: 'Larissa' },
{ label: 'Triton' },
{ label: 'Nereid' },
{ label: 'Charon' },
{ label: 'Styx' },
{ label: 'Nix' },
{ label: 'Kerberos' },
{ label: 'Hydra' },
];

const [options, setOptions] = useState<EuiSelectableOption[]>([
{ label: 'Titan' },
{ label: 'Enceladus' },
{ label: 'Mimas', checked: 'on' },
{ label: 'Dione' },
{ label: 'Iapetus', checked: 'on' },
{ label: 'Phoebe' },
{ label: 'Rhea' },
{ label: 'Pandora' },
{ label: 'Tethys' },
{ label: 'Hyperion' },
{ label: 'Pan' },
{ label: 'Atlas' },
{ label: 'Prometheus' },
{ label: 'Janus' },
{ label: 'Epimetheus' },
{ label: 'Amalthea' },
{ label: 'Thebe' },
{ label: 'Io' },
{ label: 'Europa' },
{ label: 'Ganymede' },
{ label: 'Callisto' },
{ label: 'Himalia' },
{ label: 'Phobos' },
{ label: 'Deimos' },
{ label: 'Puck' },
{ label: 'Miranda' },
{ label: 'Ariel' },
{ label: 'Umbriel' },
{ label: 'Titania' },
{ label: 'Oberon' },
{ label: 'Despina' },
{ label: 'Galatea' },
{ label: 'Larissa' },
{ label: 'Triton' },
{ label: 'Nereid' },
{ label: 'Charon' },
{ label: 'Styx' },
{ label: 'Nix' },
{ label: 'Kerberos' },
{ label: 'Hydra' },
]);
export default () => {
const [options, setOptions] = useState<EuiSelectableOption[]>(OPTIONS);
const onChange = (options: EuiSelectableOption[]) => {
setOptions(options);
};

return (
<>
<EuiPopover
panelPaddingSize="none"
button={
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
Show popover
</EuiButton>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
<SelectablePopover options={options} onChange={onChange} />
<EuiSpacer />

<SelectableFlyout options={options} onChange={onChange} />
<EuiSpacer />

<EuiTitle size="xxs">
<h4>In an input popover</h4>
</EuiTitle>
<EuiSpacer size="s" />
<SelectableInputPopover />
<EuiSpacer />

<EuiTitle size="xxs">
<h4>
Using <EuiCode language="js">listProps.bordered=true</EuiCode> and{' '}
<EuiCode language="js">
listProps.paddingSize=&quot;none&quot;
</EuiCode>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<EuiSelectable
aria-label="Bordered selectable example"
options={options}
onChange={onChange}
style={{ width: 300 }}
listProps={{ bordered: true, paddingSize: 'none' }}
>
<EuiSelectable
searchable
searchProps={{
placeholder: 'Filter list',
compressed: true,
}}
options={options}
onChange={onChange}
{(list) => list}
</EuiSelectable>
</>
);
};

const SelectablePopover = (
props: Pick<EuiSelectableProps, 'options' | 'onChange'>
) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const { options, onChange } = props;

return (
<EuiPopover
panelPaddingSize="none"
button={
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
{(list, search) => (
<div style={{ width: 240 }}>
<EuiPopoverTitle paddingSize="s">{search}</EuiPopoverTitle>
{list}
<EuiPopoverFooter paddingSize="s">
<EuiButton size="s" fullWidth>
Manage this list
</EuiButton>
</EuiPopoverFooter>
</div>
)}
</EuiSelectable>
</EuiPopover>
Show popover
</EuiButton>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
>
<EuiSelectable
aria-label="Selectable + popover example"
searchable
searchProps={{
placeholder: 'Filter list',
compressed: true,
}}
options={options}
onChange={onChange}
>
{(list, search) => (
<div style={{ width: 240 }}>
<EuiPopoverTitle paddingSize="s">{search}</EuiPopoverTitle>
{list}
<EuiPopoverFooter paddingSize="s">
<EuiButton size="s" fullWidth>
Manage this list
</EuiButton>
</EuiPopoverFooter>
</div>
)}
</EuiSelectable>
</EuiPopover>
);
};

<EuiSpacer />
const SelectableFlyout = (
props: Pick<EuiSelectableProps, 'options' | 'onChange'>
) => {
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const { options, onChange } = props;

return (
<>
<EuiButton onClick={() => setIsFlyoutVisible(true)}>
Show flyout
</EuiButton>
Expand All @@ -116,7 +168,7 @@ export default () => {
aria-labelledby="selectableFlyout"
>
<EuiSelectable
aria-label="Popover example"
aria-label="Selectable + flyout example"
searchable
options={options}
onChange={onChange}
Expand All @@ -142,29 +194,63 @@ export default () => {
</EuiFlyoutFooter>
</EuiFlyout>
)}
</>
);
};

<EuiSpacer />

<EuiTitle size="xxs">
<h4>
Using <EuiCode language="js">listProps.bordered=true</EuiCode> and{' '}
<EuiCode language="js">
listProps.paddingSize=&quot;none&quot;
</EuiCode>
</h4>
</EuiTitle>
const SelectableInputPopover = () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
const [options, setOptions] = useState<EuiSelectableOption[]>(OPTIONS);
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState('');
const [isSearching, setIsSearching] = useState(true);

<EuiSpacer />
return (
<EuiSelectable
aria-label="Selectable + input popover example"
options={options}
onChange={(newOptions, event, changedOption) => {
setOptions(newOptions);
setIsOpen(false);

<EuiSelectable
aria-label="Bordered selectable example"
options={options}
onChange={onChange}
style={{ width: 300 }}
listProps={{ bordered: true, paddingSize: 'none' }}
>
{(list) => list}
</EuiSelectable>
</>
if (changedOption.checked === 'on') {
setInputValue(changedOption.label);
setIsSearching(false);
} else {
setInputValue('');
}
}}
singleSelection
searchable
searchProps={{
value: inputValue,
onChange: (value) => {
setInputValue(value);
setIsSearching(true);
},
onKeyDown: (event) => {
if (event.key === 'Tab') return setIsOpen(false);
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
if (event.key !== 'Escape') return setIsOpen(true);
},
onClick: () => setIsOpen(true),
onFocus: () => setIsOpen(true),
}}
isPreFiltered={isSearching ? false : { highlightSearch: false }} // Shows the full list when not actively typing to search
listProps={{
css: { '.euiSelectableList__list': { maxBlockSize: 200 } },
Copy link
Contributor Author

@cee-chen cee-chen Apr 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although the requests we've received have had it, I didn't add isVirtualization: false or group labels/custom rendered descriptions to this example because I wanted to keep it simple and focused on the input popover. If needed we can extend this example via CodeSandbox.

}}
>
{(list, search) => (
<EuiInputPopover
closePopover={() => setIsOpen(false)}
disableFocusTrap
closeOnScroll
isOpen={isOpen}
input={search!}
panelPaddingSize="none"
>
{list}
</EuiInputPopover>
)}
</EuiSelectable>
);
};
22 changes: 15 additions & 7 deletions src/components/selectable/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,17 @@ export type EuiSelectableProps<T = {}> = CommonProps &
*/
errorMessage?: ReactElement | string | null;
/**
* Control whether or not options get filtered internally or if consumer will filter
* Default: false
* Control whether or not options get filtered internally (i.e., whether filtering is
* handled by EUI or by you, the consumer).
* If set to `true`, all passed `options` will be displayed regardless of the user's
* search input.
*
* Additionally allows passing a configuration object which enables turning off
* search highlighting if needed.
*
* @default false
*/
isPreFiltered?: boolean;
isPreFiltered?: boolean | { highlightSearch?: false };
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
/**
* Optional screen reader instructions to announce upon focus/interaction. This text is read out
* after the `EuiSelectable` label and a brief pause, but before the default keyboard instructions for
Expand Down Expand Up @@ -222,7 +229,7 @@ export class EuiSelectable<T = {}> extends Component<
const visibleOptions = getMatchingOptions<T>(
options,
initialSearchValue,
isPreFiltered
!!isPreFiltered
);
searchProps?.onChange?.(initialSearchValue, visibleOptions);

Expand Down Expand Up @@ -262,7 +269,7 @@ export class EuiSelectable<T = {}> extends Component<
stateUpdate.visibleOptions = getMatchingOptions<T>(
options,
stateUpdate.searchValue ?? '',
isPreFiltered
!!isPreFiltered
);

if (
Expand Down Expand Up @@ -482,7 +489,7 @@ export class EuiSelectable<T = {}> extends Component<
const visibleOptions = getMatchingOptions(
options,
searchValue,
isPreFiltered
!!isPreFiltered
);

this.setState({ visibleOptions });
Expand Down Expand Up @@ -712,7 +719,7 @@ export class EuiSelectable<T = {}> extends Component<
listId={this.optionsListRef.current ? this.listId : undefined} // Only pass the listId if it exists on the page
aria-activedescendant={this.makeOptionId(activeOptionIndex)} // the current faux-focused option
placeholder={placeholderName}
isPreFiltered={isPreFiltered ?? false}
isPreFiltered={!!isPreFiltered}
inputRef={(node) => {
this.inputRef = node;
searchProps?.inputRef?.(node);
Expand Down Expand Up @@ -781,6 +788,7 @@ export class EuiSelectable<T = {}> extends Component<
options={options}
visibleOptions={visibleOptions}
searchValue={searchValue}
isPreFiltered={isPreFiltered}
activeOptionIndex={activeOptionIndex}
setActiveOptionIndex={(index, cb) => {
this.setState({ activeOptionIndex: index }, cb);
Expand Down
Loading
Loading