From 4b296dda2af3d1a54c2d86d0d3311f6afd282790 Mon Sep 17 00:00:00 2001
From: kmcfaul <45077788+kmcfaul@users.noreply.github.com>
Date: Thu, 18 May 2023 10:04:09 -0400
Subject: [PATCH] feat(Select): add examples, add popper interface, fix focus
(#8992)
* feat(Select): add examples, add popper interface, fix focus
* feedback round 1
* fix component reference in docs
* remove unneeded toggle focus
* remove popperprops extension, add beta to keys
* remove container
* update snap for removal of container div
* update button
* remove unneeded ref, add focus flag & comments
* toggleRef option
* update dropdown examples
* snap
* update desc
* update wording
* use single prop
* move toggle object to interface, add to docs
* prop req and remove extra prop
---
.../__snapshots__/DatePicker.test.tsx.snap | 56 +++--
.../src/components/Dropdown/Dropdown.tsx | 36 ++-
.../components/Dropdown/examples/Dropdown.md | 11 +-
.../Dropdown/examples/DropdownBasic.tsx | 8 +-
.../examples/DropdownWithDescriptions.tsx | 8 +-
.../Dropdown/examples/DropdownWithGroups.tsx | 8 +-
.../examples/DropdownWithKebabToggle.tsx | 8 +-
.../src/components/Select/Select.tsx | 86 +++++--
.../src/components/Select/SelectGroup.tsx | 3 +
.../src/components/Select/SelectOption.tsx | 24 +-
.../src/components/Select/examples/Select.md | 51 +++-
.../Select/examples/SelectBasic.tsx | 44 ++--
.../Select/examples/SelectCheckbox.tsx | 4 +-
.../Select/examples/SelectFooter.tsx | 57 +++++
.../Select/examples/SelectGrouped.tsx | 20 +-
.../Select/examples/SelectMultiTypeahead.tsx | 19 +-
.../examples/SelectMultiTypeaheadCheckbox.tsx | 204 ++++++++++++++++
.../SelectMultiTypeaheadCreatable.tsx | 222 ++++++++++++++++++
.../examples/SelectOptionVariations.tsx | 70 ++++++
.../Select/examples/SelectTypeahead.tsx | 26 +-
.../examples/SelectTypeaheadCreatable.tsx | 220 +++++++++++++++++
.../Select/examples/SelectViewMore.tsx | 146 ++++++++++++
.../demos/ComposableMenu/ComposableMenu.md | 14 ++
.../react-core/src/helpers/Popper/Popper.tsx | 2 +-
24 files changed, 1202 insertions(+), 145 deletions(-)
create mode 100644 packages/react-core/src/components/Select/examples/SelectFooter.tsx
create mode 100644 packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx
create mode 100644 packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx
create mode 100644 packages/react-core/src/components/Select/examples/SelectOptionVariations.tsx
create mode 100644 packages/react-core/src/components/Select/examples/SelectTypeaheadCreatable.tsx
create mode 100644 packages/react-core/src/components/Select/examples/SelectViewMore.tsx
diff --git a/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap b/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap
index b3a9a269c4b..735e0f44695 100644
--- a/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap
+++ b/packages/react-core/src/components/DatePicker/__tests__/__snapshots__/DatePicker.test.tsx.snap
@@ -114,39 +114,37 @@ exports[`With popover opened 1`] = `
>
Month
-
-
-
+
+
-`;
\ No newline at end of file
+`;
diff --git a/packages/react-core/src/components/Dropdown/Dropdown.tsx b/packages/react-core/src/components/Dropdown/Dropdown.tsx
index fa2688d35f8..f3d2b3b4d4a 100644
--- a/packages/react-core/src/components/Dropdown/Dropdown.tsx
+++ b/packages/react-core/src/components/Dropdown/Dropdown.tsx
@@ -19,6 +19,13 @@ export interface DropdownPopperProps {
enableFlip?: boolean;
}
+export interface DropdownToggleProps {
+ /** Dropdown toggle node. */
+ toggleNode: React.ReactNode;
+ /** Reference to the toggle. */
+ toggleRef?: React.RefObject;
+}
+
/**
* See the Menu documentation for additional props that may be passed.
*/
@@ -27,16 +34,14 @@ export interface DropdownProps extends MenuProps, OUIAProps {
children?: React.ReactNode;
/** Classes applied to root element of dropdown. */
className?: string;
- /** Renderer for a custom dropdown toggle. Forwards a ref to the toggle. */
- toggle: (toggleRef: React.RefObject) => React.ReactNode;
+ /** Dropdown toggle. The toggle should either be a renderer function which forwards the given toggle ref, or a direct ReactNode that should be passed along with the toggleRef property. */
+ toggle: DropdownToggleProps | ((toggleRef: React.RefObject) => React.ReactNode);
/** Flag to indicate if menu is opened.*/
isOpen?: boolean;
+ /** Flag indicating the toggle should be focused after a selection. If this use case is too restrictive, the optional toggleRef property with a node toggle may be used to control focus. */
+ shouldFocusToggleOnSelect?: boolean;
/** Function callback called when user selects item. */
- onSelect?: (
- event?: React.MouseEvent,
- itemId?: string | number,
- toggleRef?: React.RefObject
- ) => void;
+ onSelect?: (event?: React.MouseEvent, itemId?: string | number) => void;
/** Callback to allow the dropdown component to change the open state of the menu.
* Triggered by clicking outside of the menu, or by pressing either tab or escape. */
onOpenChange?: (isOpen: boolean) => void;
@@ -62,6 +67,7 @@ const DropdownBase: React.FunctionComponent = ({
onSelect,
isOpen,
toggle,
+ shouldFocusToggleOnSelect = false,
onOpenChange,
isPlain,
isScrollable,
@@ -73,10 +79,15 @@ const DropdownBase: React.FunctionComponent = ({
...props
}: DropdownProps) => {
const localMenuRef = React.useRef();
- const toggleRef = React.useRef();
+ const localToggleRef = React.useRef();
const ouiaProps = useOUIAProps(Dropdown.displayName, ouiaId, ouiaSafe);
const menuRef = (innerRef as React.RefObject) || localMenuRef;
+ const toggleRef =
+ typeof toggle === 'function' || (typeof toggle !== 'function' && !toggle.toggleRef)
+ ? localToggleRef
+ : (toggle?.toggleRef as React.RefObject);
+
React.useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape if onOpenChange is provided
@@ -119,13 +130,16 @@ const DropdownBase: React.FunctionComponent = ({
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
- }, [isOpen, menuRef, onOpenChange]);
+ }, [isOpen, menuRef, toggleRef, onOpenChange]);
const menu = (
onSelect && onSelect(event, itemId, toggleRef)}
+ onSelect={(event, itemId) => {
+ onSelect && onSelect(event, itemId);
+ shouldFocusToggleOnSelect && toggleRef.current.focus();
+ }}
isPlain={isPlain}
isScrollable={isScrollable}
{...props}
@@ -136,7 +150,7 @@ const DropdownBase: React.FunctionComponent = ({
);
return (
{
setIsOpen(!isOpen);
};
- const onSelect = (
- _event: React.MouseEvent | undefined,
- itemId: string | number | undefined,
- toggleRef: React.RefObject
- ) => {
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
// eslint-disable-next-line no-console
console.log('selected', itemId);
setIsOpen(false);
- toggleRef?.current.focus();
};
return (
@@ -30,6 +25,7 @@ export const DropdownBasic: React.FunctionComponent = () => {
)}
ouiaId="BasicDropdown"
+ shouldFocusToggleOnSelect
>
diff --git a/packages/react-core/src/components/Dropdown/examples/DropdownWithDescriptions.tsx b/packages/react-core/src/components/Dropdown/examples/DropdownWithDescriptions.tsx
index ce30700be51..2b7b4d7d6a6 100644
--- a/packages/react-core/src/components/Dropdown/examples/DropdownWithDescriptions.tsx
+++ b/packages/react-core/src/components/Dropdown/examples/DropdownWithDescriptions.tsx
@@ -8,15 +8,10 @@ export const DropdownWithDescriptions: React.FunctionComponent = () => {
setIsOpen(!isOpen);
};
- const onSelect = (
- _event: React.MouseEvent | undefined,
- itemId: string | number | undefined,
- toggleRef: React.RefObject
- ) => {
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
// eslint-disable-next-line no-console
console.log('selected', itemId);
setIsOpen(false);
- toggleRef?.current.focus();
};
return (
@@ -29,6 +24,7 @@ export const DropdownWithDescriptions: React.FunctionComponent = () => {
Dropdown
)}
+ shouldFocusToggleOnSelect
>
diff --git a/packages/react-core/src/components/Dropdown/examples/DropdownWithGroups.tsx b/packages/react-core/src/components/Dropdown/examples/DropdownWithGroups.tsx
index a846e06e089..619528ed40f 100644
--- a/packages/react-core/src/components/Dropdown/examples/DropdownWithGroups.tsx
+++ b/packages/react-core/src/components/Dropdown/examples/DropdownWithGroups.tsx
@@ -16,15 +16,10 @@ export const DropdownWithGroups: React.FunctionComponent = () => {
setIsOpen(!isOpen);
};
- const onSelect = (
- _event: React.MouseEvent | undefined,
- itemId: string | number | undefined,
- toggleRef: React.RefObject
- ) => {
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
// eslint-disable-next-line no-console
console.log('selected', itemId);
setIsOpen(false);
- toggleRef?.current.focus();
};
return (
@@ -37,6 +32,7 @@ export const DropdownWithGroups: React.FunctionComponent = () => {
Dropdown
)}
+ shouldFocusToggleOnSelect
>
diff --git a/packages/react-core/src/components/Dropdown/examples/DropdownWithKebabToggle.tsx b/packages/react-core/src/components/Dropdown/examples/DropdownWithKebabToggle.tsx
index a3fb6202e75..0b41bc43b10 100644
--- a/packages/react-core/src/components/Dropdown/examples/DropdownWithKebabToggle.tsx
+++ b/packages/react-core/src/components/Dropdown/examples/DropdownWithKebabToggle.tsx
@@ -9,15 +9,10 @@ export const DropdownWithKebab: React.FunctionComponent = () => {
setIsOpen(!isOpen);
};
- const onSelect = (
- _event: React.MouseEvent | undefined,
- itemId: string | number | undefined,
- toggleRef: React.RefObject
- ) => {
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
// eslint-disable-next-line no-console
console.log('selected', itemId);
setIsOpen(false);
- toggleRef?.current.focus();
};
return (
@@ -36,6 +31,7 @@ export const DropdownWithKebab: React.FunctionComponent = () => {
)}
+ shouldFocusToggleOnSelect
>
diff --git a/packages/react-core/src/components/Select/Select.tsx b/packages/react-core/src/components/Select/Select.tsx
index 3df1f197043..4a492d812d5 100644
--- a/packages/react-core/src/components/Select/Select.tsx
+++ b/packages/react-core/src/components/Select/Select.tsx
@@ -1,9 +1,35 @@
import React from 'react';
import { css } from '@patternfly/react-styles';
import { Menu, MenuContent, MenuProps } from '../Menu';
-import { Popper, PopperProps } from '../../helpers/Popper/Popper';
+import { Popper } from '../../helpers/Popper/Popper';
import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers';
+export interface SelectPopperProps {
+ /** Vertical direction of the popper. If enableFlip is set to true, this will set the initial direction before the popper flips. */
+ direction?: 'up' | 'down';
+ /** Horizontal position of the popper */
+ position?: 'right' | 'left' | 'center';
+ /** Custom width of the popper. If the value is "trigger", it will set the width to the select toggle's width */
+ width?: string | 'trigger';
+ /** Minimum width of the popper. If the value is "trigger", it will set the min width to the select toggle's width */
+ minWidth?: string | 'trigger';
+ /** Maximum width of the popper. If the value is "trigger", it will set the max width to the select toggle's width */
+ maxWidth?: string | 'trigger';
+ /** Enable to flip the popper when it reaches the boundary */
+ enableFlip?: boolean;
+}
+
+export interface SelectToggleProps {
+ /** Select toggle node. */
+ toggleNode: React.ReactNode;
+ /** Reference to the toggle. */
+ toggleRef?: React.RefObject;
+}
+
+/**
+ * See the Menu documentation for additional props that may be passed.
+ */
+
export interface SelectProps extends MenuProps, OUIAProps {
/** Anything which can be rendered in a select */
children?: React.ReactNode;
@@ -13,13 +39,17 @@ export interface SelectProps extends MenuProps, OUIAProps {
isOpen?: boolean;
/** Single itemId for single select menus, or array of itemIds for multi select. You can also specify isSelected on the SelectOption. */
selected?: any | any[];
- /** Renderer for a custom select toggle. Forwards a ref to the toggle. */
- toggle: (toggleRef: React.RefObject) => React.ReactNode;
+ /** Select toggle. The toggle should either be a renderer function which forwards the given toggle ref, or a direct ReactNode that should be passed along with the toggleRef property. */
+ toggle: SelectToggleProps | ((toggleRef: React.RefObject) => React.ReactNode);
+ /** Flag indicating the toggle should be focused after a selection. If this use case is too restrictive, the optional toggleRef property with a node toggle may be used to control focus. */
+ shouldFocusToggleOnSelect?: boolean;
/** Function callback when user selects an option. */
onSelect?: (event?: React.MouseEvent, itemId?: string | number) => void;
/** Callback to allow the select component to change the open state of the menu.
- * Triggered by clicking outside of the menu, or by pressing either tab or escape. */
+ * Triggered by clicking outside of the menu, or by pressing any keys specificed in onOpenChangeKeys. */
onOpenChange?: (isOpen: boolean) => void;
+ /** @beta Keys that trigger onOpenChange, defaults to tab and escape. It is highly recommended to include Escape in the array, while Tab may be omitted if the menu contains non-menu items that are focusable. */
+ onOpenChangeKeys?: string[];
/** Indicates if the select should be without the outer box-shadow */
isPlain?: boolean;
/** @hide Forwarded ref */
@@ -29,7 +59,7 @@ export interface SelectProps extends MenuProps, OUIAProps {
/** @beta Determines the accessible role of the select. For a checkbox select pass in "menu". */
role?: string;
/** Additional properties to pass to the popper */
- popperProps?: Partial;
+ popperProps?: SelectPopperProps;
}
const SelectBase: React.FunctionComponent = ({
@@ -39,7 +69,9 @@ const SelectBase: React.FunctionComponent = ({
isOpen,
selected,
toggle,
+ shouldFocusToggleOnSelect = false,
onOpenChange,
+ onOpenChangeKeys = ['Escape', 'Tab'],
isPlain,
innerRef,
zIndex = 9999,
@@ -48,18 +80,24 @@ const SelectBase: React.FunctionComponent = ({
...props
}: SelectProps & OUIAProps) => {
const localMenuRef = React.useRef();
- const toggleRef = React.useRef();
- const containerRef = React.useRef();
+ const localToggleRef = React.useRef();
const menuRef = (innerRef as React.RefObject) || localMenuRef;
+ const toggleRef =
+ typeof toggle === 'function' || (typeof toggle !== 'function' && !toggle.toggleRef)
+ ? localToggleRef
+ : (toggle?.toggleRef as React.RefObject);
+
React.useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape if onOpenChange is provided
if (
- (isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
- toggleRef.current?.contains(event.target as Node)
+ isOpen &&
+ onOpenChange &&
+ (menuRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node))
) {
- if (event.key === 'Escape' || event.key === 'Tab') {
+ if (onOpenChangeKeys.includes(event.key)) {
+ event.preventDefault();
onOpenChange(false);
toggleRef.current?.focus();
}
@@ -90,14 +128,17 @@ const SelectBase: React.FunctionComponent = ({
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
- }, [isOpen, menuRef, onOpenChange]);
+ }, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);
const menu = (
onSelect && onSelect(event, itemId)}
+ onSelect={(event, itemId) => {
+ onSelect && onSelect(event, itemId);
+ shouldFocusToggleOnSelect && toggleRef.current.focus();
+ }}
isPlain={isPlain}
selected={selected}
{...getOUIAProps(
@@ -111,18 +152,15 @@ const SelectBase: React.FunctionComponent = ({
);
return (
-
+
);
};
diff --git a/packages/react-core/src/components/Select/SelectGroup.tsx b/packages/react-core/src/components/Select/SelectGroup.tsx
index 645f5b2aa00..744de685390 100644
--- a/packages/react-core/src/components/Select/SelectGroup.tsx
+++ b/packages/react-core/src/components/Select/SelectGroup.tsx
@@ -2,6 +2,9 @@ import React from 'react';
import { css } from '@patternfly/react-styles';
import { MenuGroupProps, MenuGroup } from '../Menu';
+/**
+ * See the MenuGroup section of the Menu documentation for additional props that may be passed.
+ */
export interface SelectGroupProps extends Omit {
/** Anything which can be rendered in a select group */
children: React.ReactNode;
diff --git a/packages/react-core/src/components/Select/SelectOption.tsx b/packages/react-core/src/components/Select/SelectOption.tsx
index 72f4fd2e0ae..5c941f39e7e 100644
--- a/packages/react-core/src/components/Select/SelectOption.tsx
+++ b/packages/react-core/src/components/Select/SelectOption.tsx
@@ -2,11 +2,17 @@ import React from 'react';
import { css } from '@patternfly/react-styles';
import { MenuItemProps, MenuItem } from '../Menu';
+/**
+ * See the MenuItem section of the Menu documentation for additional props that may be passed.
+ */
+
export interface SelectOptionProps extends Omit {
/** Anything which can be rendered in a select option */
children?: React.ReactNode;
/** Classes applied to root element of select option */
className?: string;
+ /** @hide Forwarded ref */
+ innerRef?: React.Ref;
/** Identifies the component in the Select onSelect callback */
itemId?: any;
/** Indicates the option has a checkbox */
@@ -17,15 +23,29 @@ export interface SelectOptionProps extends Omit {
isSelected?: boolean;
/** Indicates the option is focused */
isFocused?: boolean;
+ /** Render an external link icon on focus or hover, and set the link's
+ * "target" attribute to a value of "_blank".
+ */
+ isExternalLink?: boolean;
+ /** Render option with icon */
+ icon?: React.ReactNode;
+ /** Description of the option */
+ description?: React.ReactNode;
}
-export const SelectOption: React.FunctionComponent = ({
+const SelectOptionBase: React.FunctionComponent = ({
children,
className,
+ innerRef,
...props
}: SelectOptionProps) => (
-
+
{children}
);
+
+export const SelectOption = React.forwardRef((props: SelectOptionProps, ref: React.Ref) => (
+
+));
+
SelectOption.displayName = 'SelectOption';
diff --git a/packages/react-core/src/components/Select/examples/Select.md b/packages/react-core/src/components/Select/examples/Select.md
index 782430b7170..3ebabaca5ba 100644
--- a/packages/react-core/src/components/Select/examples/Select.md
+++ b/packages/react-core/src/components/Select/examples/Select.md
@@ -3,35 +3,82 @@ id: Select
section: components
subsection: menus
cssPrefix: pf-c-select
-propComponents: ['Select', 'SelectOption', 'SelectGroup', 'SelectList', 'MenuToggle']
+propComponents:
+ ['Select', 'SelectOption', 'SelectGroup', 'SelectList', 'MenuToggle', 'SelectToggleProps', 'SelectPopperProps']
ouia: true
---
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
## Examples
+`Select` builds off of the Menu component suite to wrap commonly used properties and functions for a select menu. See the [Menu documentation](/components/menus/menu) for a full list of properties that may be passed through `Select` to further customize the select menu, or the [custom menu examples](/components/menus/custom-menus) for additional examples of fully functional menus.
+
### Single
```ts file="./SelectBasic.tsx"
+
+```
+
+### Option variations
+
+Showcases different option variants and customizations that are commonly used in a select menu. For a more complete list, see the [Menu documentation](/components/menus/menu).
+
+```ts file="./SelectOptionVariations.tsx"
+
```
### Grouped single
```ts file="./SelectGrouped.tsx"
+
```
### Checkbox
```ts file="./SelectCheckbox.tsx"
+
```
### Typeahead
```ts file="./SelectTypeahead.tsx"
+
+```
+
+### Typeahead with create option
+
+```ts file="./SelectTypeaheadCreatable.tsx"
+
```
-### Multiple Typeahead
+### Multiple typeahead with chips
```ts file="./SelectMultiTypeahead.tsx"
+
+```
+
+### Multiple typeahead with create option
+
+```ts file="./SelectMultiTypeaheadCreatable.tsx"
+
+```
+
+### Multiple typeahead with checkboxes
+
+```ts file="./SelectMultiTypeaheadCheckbox.tsx"
+
+```
+
+### View more
+
+```ts file="./SelectViewMore.tsx"
+
+```
+
+### Footer
+
+```ts file="./SelectFooter.tsx"
+
```
diff --git a/packages/react-core/src/components/Select/examples/SelectBasic.tsx b/packages/react-core/src/components/Select/examples/SelectBasic.tsx
index f4084a53049..9a7f56af78c 100644
--- a/packages/react-core/src/components/Select/examples/SelectBasic.tsx
+++ b/packages/react-core/src/components/Select/examples/SelectBasic.tsx
@@ -1,10 +1,10 @@
import React from 'react';
-import { Select, SelectOption, SelectList, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
+import { Select, SelectOption, SelectList, MenuToggle, MenuToggleElement, Checkbox } from '@patternfly/react-core';
export const SelectBasic: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
const [selected, setSelected] = React.useState('Select a value');
- const menuRef = React.useRef(null);
+ const [isDisabled, setIsDisabled] = React.useState(false);
const onToggleClick = () => {
setIsOpen(!isOpen);
@@ -23,6 +23,7 @@ export const SelectBasic: React.FunctionComponent = () => {
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
+ isDisabled={isDisabled}
style={
{
width: '200px'
@@ -34,20 +35,29 @@ export const SelectBasic: React.FunctionComponent = () => {
);
return (
- setIsOpen(isOpen)}
- toggle={toggle}
- >
-
- Option 1
- Option 2
- Option 3
-
-
+
+ setIsDisabled(checked)}
+ style={{ marginBottom: 20 }}
+ />
+ setIsOpen(isOpen)}
+ toggle={toggle}
+ shouldFocusToggleOnSelect
+ >
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
);
};
diff --git a/packages/react-core/src/components/Select/examples/SelectCheckbox.tsx b/packages/react-core/src/components/Select/examples/SelectCheckbox.tsx
index 70b6a1b9d3b..c6c2fbe0e85 100644
--- a/packages/react-core/src/components/Select/examples/SelectCheckbox.tsx
+++ b/packages/react-core/src/components/Select/examples/SelectCheckbox.tsx
@@ -4,7 +4,6 @@ import { Select, SelectOption, SelectList, MenuToggle, MenuToggleElement, Badge
export const SelectCheckbox: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
const [selectedItems, setSelectedItems] = React.useState([]);
- const menuRef = React.useRef(null);
const onToggleClick = () => {
setIsOpen(!isOpen);
@@ -15,7 +14,7 @@ export const SelectCheckbox: React.FunctionComponent = () => {
console.log('selected', itemId);
if (selectedItems.includes(itemId as number)) {
- setSelectedItems(selectedItems.filter(id => id !== itemId));
+ setSelectedItems(selectedItems.filter((id) => id !== itemId));
} else {
setSelectedItems([...selectedItems, itemId as number]);
}
@@ -41,7 +40,6 @@ export const SelectCheckbox: React.FunctionComponent = () => {
{
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState('Select a value');
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const toggle = (toggleRef) => (
+
+ {selected}
+
+ );
+
+ function onSelect(event: React.MouseEvent | undefined, itemId: string | number | undefined) {
+ if (typeof itemId === 'undefined') {
+ return;
+ }
+
+ setSelected(itemId.toString());
+ }
+
+ return (
+ setIsOpen(isOpen)}
+ onOpenChangeKeys={['Escape']}
+ toggle={toggle}
+ id="menu-with-footer"
+ onSelect={onSelect}
+ selected={selected}
+ >
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+ Footer action
+
+
+
+ );
+};
diff --git a/packages/react-core/src/components/Select/examples/SelectGrouped.tsx b/packages/react-core/src/components/Select/examples/SelectGrouped.tsx
index 184188499ca..03621ee9ffb 100644
--- a/packages/react-core/src/components/Select/examples/SelectGrouped.tsx
+++ b/packages/react-core/src/components/Select/examples/SelectGrouped.tsx
@@ -1,10 +1,17 @@
import React from 'react';
-import { Select, SelectOption, SelectList, SelectGroup, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
+import {
+ Select,
+ SelectOption,
+ SelectList,
+ SelectGroup,
+ MenuToggle,
+ MenuToggleElement,
+ Divider
+} from '@patternfly/react-core';
-export const SelectBasic: React.FunctionComponent = () => {
+export const SelectGrouped: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
const [selected, setSelected] = React.useState('Select a value');
- const menuRef = React.useRef(null);
const onToggleClick = () => {
setIsOpen(!isOpen);
@@ -35,13 +42,13 @@ export const SelectBasic: React.FunctionComponent = () => {
return (
setIsOpen(isOpen)}
+ onOpenChange={(isOpen) => setIsOpen(isOpen)}
toggle={toggle}
+ shouldFocusToggleOnSelect
>
@@ -50,6 +57,7 @@ export const SelectBasic: React.FunctionComponent = () => {
Option 3
+
Option 4
diff --git a/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx b/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx
index 6bf61e9fc1f..73f23291e29 100644
--- a/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx
+++ b/packages/react-core/src/components/Select/examples/SelectMultiTypeahead.tsx
@@ -31,8 +31,6 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
const [selectOptions, setSelectOptions] = React.useState(initialSelectOptions);
const [focusedItemIndex, setFocusedItemIndex] = React.useState(null);
const [activeItem, setActiveItem] = React.useState(null);
-
- const menuRef = React.useRef(null);
const textInputRef = React.useRef();
React.useEffect(() => {
@@ -40,10 +38,8 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
// Filter menu items based on the text input value when one exists
if (inputValue) {
- newSelectOptions = initialSelectOptions.filter(menuItem =>
- String(menuItem.children)
- .toLowerCase()
- .includes(inputValue.toLowerCase())
+ newSelectOptions = initialSelectOptions.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase())
);
// When no options are found after filtering, display 'No results found'
@@ -87,13 +83,13 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
}
setFocusedItemIndex(indexToFocus);
- const focusedItem = selectOptions.filter(option => !option.isDisabled)[indexToFocus];
+ const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
setActiveItem(`select-multi-typeahead-${focusedItem.itemId.replace(' ', '-')}`);
}
};
const onInputKeyDown = (event: React.KeyboardEvent) => {
- const enabledMenuItems = selectOptions.filter(menuItem => !menuItem.isDisabled);
+ const enabledMenuItems = selectOptions.filter((menuItem) => !menuItem.isDisabled);
const [firstMenuItem] = enabledMenuItems;
const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
@@ -101,7 +97,7 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
// Select the first available option
case 'Enter':
if (!isOpen) {
- setIsOpen(prevIsOpen => !prevIsOpen);
+ setIsOpen((prevIsOpen) => !prevIsOpen);
} else if (isOpen && focusedItem.itemId !== 'no results') {
onSelect(focusedItem.itemId as string);
}
@@ -133,7 +129,7 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
if (itemId && itemId !== 'no results') {
setSelected(
- selected.includes(itemId) ? selected.filter(selection => selection !== itemId) : [...selected, itemId]
+ selected.includes(itemId) ? selected.filter((selection) => selection !== itemId) : [...selected, itemId]
);
}
@@ -161,7 +157,7 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
{selected.map((selection, index) => (
{
+ onClick={(ev) => {
ev.stopPropagation();
onSelect(selection);
}}
@@ -193,7 +189,6 @@ export const SelectMultiTypeahead: React.FunctionComponent = () => {
return (
onSelect(selection as string)}
diff --git a/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx
new file mode 100644
index 00000000000..b57aa0e8193
--- /dev/null
+++ b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx
@@ -0,0 +1,204 @@
+import React from 'react';
+import {
+ Select,
+ SelectOption,
+ SelectList,
+ SelectOptionProps,
+ MenuToggle,
+ MenuToggleElement,
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ Button
+} from '@patternfly/react-core';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+
+const initialSelectOptions: SelectOptionProps[] = [
+ { itemId: 'Alabama', children: 'Alabama' },
+ { itemId: 'Florida', children: 'Florida' },
+ { itemId: 'New Jersey', children: 'New Jersey' },
+ { itemId: 'New Mexico', children: 'New Mexico' },
+ { itemId: 'New York', children: 'New York' },
+ { itemId: 'North Carolina', children: 'North Carolina' }
+];
+
+export const SelectMultiTypeaheadCheckbox: React.FunctionComponent = () => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [inputValue, setInputValue] = React.useState('');
+ const [selected, setSelected] = React.useState([]);
+ const [selectOptions, setSelectOptions] = React.useState(initialSelectOptions);
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState(null);
+ const [activeItem, setActiveItem] = React.useState(null);
+ const [placeholder, setPlaceholder] = React.useState('0 items selected');
+ const textInputRef = React.useRef();
+
+ React.useEffect(() => {
+ let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
+
+ // Filter menu items based on the text input value when one exists
+ if (inputValue) {
+ newSelectOptions = initialSelectOptions.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase())
+ );
+
+ // When no options are found after filtering, display 'No results found'
+ if (!newSelectOptions.length) {
+ newSelectOptions = [
+ { isDisabled: false, children: `No results found for "${inputValue}"`, itemId: 'no results' }
+ ];
+ }
+
+ // Open the menu when the input value changes and the new value is not empty
+ if (!isOpen) {
+ setIsOpen(true);
+ }
+ }
+
+ setSelectOptions(newSelectOptions);
+ setFocusedItemIndex(null);
+ setActiveItem(null);
+ }, [inputValue]);
+
+ const handleMenuArrowKeys = (key: string) => {
+ let indexToFocus;
+
+ if (isOpen) {
+ if (key === 'ArrowUp') {
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
+ indexToFocus = selectOptions.length - 1;
+ } else {
+ indexToFocus = focusedItemIndex - 1;
+ }
+ }
+
+ if (key === 'ArrowDown') {
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = focusedItemIndex + 1;
+ }
+ }
+
+ setFocusedItemIndex(indexToFocus);
+ const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
+ setActiveItem(`select-multi-typeahead-checkbox-${focusedItem.itemId.replace(' ', '-')}`);
+ }
+ };
+
+ const onInputKeyDown = (event: React.KeyboardEvent) => {
+ const enabledMenuItems = selectOptions.filter((menuItem) => !menuItem.isDisabled);
+ const [firstMenuItem] = enabledMenuItems;
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
+
+ switch (event.key) {
+ // Select the first available option
+ case 'Enter':
+ if (!isOpen) {
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ } else if (isOpen && focusedItem.itemId !== 'no results') {
+ onSelect(focusedItem.itemId as string);
+ }
+ break;
+ case 'Tab':
+ case 'Escape':
+ setIsOpen(false);
+ setActiveItem(null);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ event.preventDefault();
+ handleMenuArrowKeys(event.key);
+ break;
+ }
+ };
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onTextInputChange = (_event: React.FormEvent, value: string) => {
+ setInputValue(value);
+ };
+
+ const onSelect = (itemId: string) => {
+ // eslint-disable-next-line no-console
+ console.log('selected', itemId);
+
+ if (itemId && itemId !== 'no results') {
+ setSelected(
+ selected.includes(itemId) ? selected.filter((selection) => selection !== itemId) : [...selected, itemId]
+ );
+ }
+
+ textInputRef.current?.focus();
+ };
+
+ React.useEffect(() => {
+ setPlaceholder(`${selected.length} items selected`);
+ }, [selected]);
+
+ const toggle = (toggleRef: React.Ref) => (
+
+
+
+
+ {selected.length > 0 && (
+ {
+ setInputValue('');
+ setSelected([]);
+ textInputRef?.current?.focus();
+ }}
+ aria-label="Clear input value"
+ >
+
+
+ )}
+
+
+
+ );
+
+ return (
+ onSelect(selection as string)}
+ onOpenChange={() => setIsOpen(false)}
+ toggle={toggle}
+ >
+
+ {selectOptions.map((option, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx
new file mode 100644
index 00000000000..089e456ac31
--- /dev/null
+++ b/packages/react-core/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx
@@ -0,0 +1,222 @@
+import React from 'react';
+import {
+ Select,
+ SelectOption,
+ SelectList,
+ SelectOptionProps,
+ MenuToggle,
+ MenuToggleElement,
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ ChipGroup,
+ Chip,
+ Button
+} from '@patternfly/react-core';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+
+let initialSelectOptions: SelectOptionProps[] = [
+ { itemId: 'Alabama', children: 'Alabama' },
+ { itemId: 'Florida', children: 'Florida' },
+ { itemId: 'New Jersey', children: 'New Jersey' },
+ { itemId: 'New Mexico', children: 'New Mexico' },
+ { itemId: 'New York', children: 'New York' },
+ { itemId: 'North Carolina', children: 'North Carolina' }
+];
+
+export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [inputValue, setInputValue] = React.useState('');
+ const [selected, setSelected] = React.useState([]);
+ const [selectOptions, setSelectOptions] = React.useState(initialSelectOptions);
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState(null);
+ const [activeItem, setActiveItem] = React.useState(null);
+ const [onCreation, setOnCreation] = React.useState(false); // Boolean to refresh filter state after new option is created
+ const textInputRef = React.useRef();
+
+ React.useEffect(() => {
+ let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
+
+ // Filter menu items based on the text input value when one exists
+ if (inputValue) {
+ newSelectOptions = initialSelectOptions.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase())
+ );
+
+ // When no options are found after filtering, display creation option
+ if (!newSelectOptions.length) {
+ newSelectOptions = [{ isDisabled: false, children: `Create new option "${inputValue}"`, itemId: 'create' }];
+ }
+
+ // Open the menu when the input value changes and the new value is not empty
+ if (!isOpen) {
+ setIsOpen(true);
+ }
+ }
+
+ setSelectOptions(newSelectOptions);
+ setFocusedItemIndex(null);
+ setActiveItem(null);
+ }, [inputValue, onCreation]);
+
+ const handleMenuArrowKeys = (key: string) => {
+ let indexToFocus;
+
+ if (isOpen) {
+ if (key === 'ArrowUp') {
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
+ indexToFocus = selectOptions.length - 1;
+ } else {
+ indexToFocus = focusedItemIndex - 1;
+ }
+ }
+
+ if (key === 'ArrowDown') {
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = focusedItemIndex + 1;
+ }
+ }
+
+ setFocusedItemIndex(indexToFocus);
+ const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
+ setActiveItem(`select-multi-create-typeahead-${focusedItem.itemId.replace(' ', '-')}`);
+ }
+ };
+
+ const onInputKeyDown = (event: React.KeyboardEvent) => {
+ const enabledMenuItems = selectOptions.filter((menuItem) => !menuItem.isDisabled);
+ const [firstMenuItem] = enabledMenuItems;
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
+
+ switch (event.key) {
+ // Select the first available option
+ case 'Enter':
+ if (!isOpen) {
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ } else if (isOpen && focusedItem.itemId !== 'no results') {
+ onSelect(focusedItem.itemId as string);
+ }
+ break;
+ case 'Tab':
+ case 'Escape':
+ setIsOpen(false);
+ setActiveItem(null);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ event.preventDefault();
+ handleMenuArrowKeys(event.key);
+ break;
+ }
+ };
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onTextInputChange = (_event: React.FormEvent, value: string) => {
+ setInputValue(value);
+ };
+
+ const onSelect = (itemId: string) => {
+ if (itemId) {
+ if (itemId === 'create') {
+ if (!initialSelectOptions.some((item) => item.itemId === inputValue)) {
+ initialSelectOptions = [...initialSelectOptions, { itemId: inputValue, children: inputValue }];
+ }
+ setSelected(
+ selected.includes(inputValue)
+ ? selected.filter((selection) => selection !== inputValue)
+ : [...selected, inputValue]
+ );
+ setOnCreation(!onCreation);
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('selected', itemId);
+ setSelected(
+ selected.includes(itemId) ? selected.filter((selection) => selection !== itemId) : [...selected, itemId]
+ );
+ }
+ }
+
+ textInputRef.current?.focus();
+ };
+
+ const toggle = (toggleRef: React.Ref) => (
+
+
+
+
+ {selected.map((selection, index) => (
+ {
+ ev.stopPropagation();
+ onSelect(selection);
+ }}
+ >
+ {selection}
+
+ ))}
+
+
+
+ {selected.length > 0 && (
+ {
+ setInputValue('');
+ setSelected([]);
+ textInputRef?.current?.focus();
+ }}
+ aria-label="Clear input value"
+ >
+
+
+ )}
+
+
+
+ );
+
+ return (
+ onSelect(selection as string)}
+ onOpenChange={() => setIsOpen(false)}
+ toggle={toggle}
+ >
+
+ {selectOptions.map((option, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/packages/react-core/src/components/Select/examples/SelectOptionVariations.tsx b/packages/react-core/src/components/Select/examples/SelectOptionVariations.tsx
new file mode 100644
index 00000000000..8bc3d1d35de
--- /dev/null
+++ b/packages/react-core/src/components/Select/examples/SelectOptionVariations.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { Select, SelectOption, SelectList, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
+import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';
+
+export const SelectOptionVariations: React.FunctionComponent = () => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState('Select a value');
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
+ // eslint-disable-next-line no-console
+ console.log('selected', itemId);
+
+ setSelected(itemId as string);
+ setIsOpen(false);
+ };
+
+ const toggle = (toggleRef: React.Ref) => (
+
+ {selected}
+
+ );
+
+ return (
+ setIsOpen(isOpen)}
+ toggle={toggle}
+ shouldFocusToggleOnSelect
+ >
+
+ Basic option
+
+ Option with description
+
+ event.preventDefault()}
+ itemId="Option with link"
+ isExternalLink
+ >
+ Option with link
+
+ }>
+ Option with icon
+
+
+ Disabled option
+
+ See Menu for additional variations!
+
+
+ );
+};
diff --git a/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx b/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx
index b360398d10c..71446f45ddc 100644
--- a/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx
+++ b/packages/react-core/src/components/Select/examples/SelectTypeahead.tsx
@@ -26,28 +26,25 @@ export const SelectBasic: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
const [selected, setSelected] = React.useState('');
const [inputValue, setInputValue] = React.useState('');
+ const [filterValue, setFilterValue] = React.useState('');
const [selectOptions, setSelectOptions] = React.useState(initialSelectOptions);
const [focusedItemIndex, setFocusedItemIndex] = React.useState(null);
const [activeItem, setActiveItem] = React.useState(null);
-
- const menuRef = React.useRef(null);
const textInputRef = React.useRef();
React.useEffect(() => {
let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
// Filter menu items based on the text input value when one exists
- if (inputValue) {
- newSelectOptions = initialSelectOptions.filter(menuItem =>
- String(menuItem.children)
- .toLowerCase()
- .includes(inputValue.toLowerCase())
+ if (filterValue) {
+ newSelectOptions = initialSelectOptions.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase())
);
// When no options are found after filtering, display 'No results found'
if (!newSelectOptions.length) {
newSelectOptions = [
- { isDisabled: false, children: `No results found for "${inputValue}"`, itemId: 'no results' }
+ { isDisabled: false, children: `No results found for "${filterValue}"`, itemId: 'no results' }
];
}
@@ -60,7 +57,7 @@ export const SelectBasic: React.FunctionComponent = () => {
setSelectOptions(newSelectOptions);
setActiveItem(null);
setFocusedItemIndex(null);
- }, [inputValue]);
+ }, [filterValue]);
const onToggleClick = () => {
setIsOpen(!isOpen);
@@ -72,6 +69,7 @@ export const SelectBasic: React.FunctionComponent = () => {
if (itemId && itemId !== 'no results') {
setInputValue(itemId as string);
+ setFilterValue('');
setSelected(itemId as string);
}
setIsOpen(false);
@@ -81,6 +79,7 @@ export const SelectBasic: React.FunctionComponent = () => {
const onTextInputChange = (_event: React.FormEvent, value: string) => {
setInputValue(value);
+ setFilterValue(value);
};
const handleMenuArrowKeys = (key: string) => {
@@ -106,13 +105,13 @@ export const SelectBasic: React.FunctionComponent = () => {
}
setFocusedItemIndex(indexToFocus);
- const focusedItem = selectOptions.filter(option => !option.isDisabled)[indexToFocus];
+ const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
setActiveItem(`select-typeahead-${focusedItem.itemId.replace(' ', '-')}`);
}
};
const onInputKeyDown = (event: React.KeyboardEvent) => {
- const enabledMenuItems = selectOptions.filter(option => !option.isDisabled);
+ const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled);
const [firstMenuItem] = enabledMenuItems;
const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
@@ -121,10 +120,11 @@ export const SelectBasic: React.FunctionComponent = () => {
case 'Enter':
if (isOpen && focusedItem.itemId !== 'no results') {
setInputValue(String(focusedItem.children));
+ setFilterValue('');
setSelected(String(focusedItem.children));
}
- setIsOpen(prevIsOpen => !prevIsOpen);
+ setIsOpen((prevIsOpen) => !prevIsOpen);
setFocusedItemIndex(null);
setActiveItem(null);
@@ -167,6 +167,7 @@ export const SelectBasic: React.FunctionComponent = () => {
onClick={() => {
setSelected('');
setInputValue('');
+ setFilterValue('');
textInputRef?.current?.focus();
}}
aria-label="Clear input value"
@@ -182,7 +183,6 @@ export const SelectBasic: React.FunctionComponent = () => {
return (
{
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState('');
+ const [inputValue, setInputValue] = React.useState('');
+ const [filterValue, setFilterValue] = React.useState('');
+ const [selectOptions, setSelectOptions] = React.useState(initialSelectOptions);
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState(null);
+ const [activeItem, setActiveItem] = React.useState(null);
+ const [onCreation, setOnCreation] = React.useState(false); // Boolean to refresh filter state after new option is created
+ const textInputRef = React.useRef();
+
+ React.useEffect(() => {
+ let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
+
+ // Filter menu items based on the text input value when one exists
+ if (filterValue) {
+ newSelectOptions = initialSelectOptions.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase())
+ );
+
+ // When no options are found after filtering, display creation option
+ if (!newSelectOptions.length) {
+ newSelectOptions = [{ isDisabled: false, children: `Create new option "${filterValue}"`, itemId: 'create' }];
+ }
+
+ // Open the menu when the input value changes and the new value is not empty
+ if (!isOpen) {
+ setIsOpen(true);
+ }
+ }
+
+ setSelectOptions(newSelectOptions);
+ setActiveItem(null);
+ setFocusedItemIndex(null);
+ }, [filterValue, onCreation]);
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
+ // eslint-disable-next-line no-console
+
+ if (itemId) {
+ if (itemId === 'create') {
+ if (!initialSelectOptions.some((item) => item.itemId === filterValue)) {
+ initialSelectOptions = [...initialSelectOptions, { itemId: filterValue, children: filterValue }];
+ }
+ setSelected(filterValue);
+ setOnCreation(!onCreation);
+ setFilterValue('');
+ } else {
+ // eslint-disable-next-line no-console
+ console.log('selected', itemId);
+ setInputValue(itemId as string);
+ setFilterValue('');
+ setSelected(itemId as string);
+ }
+ }
+
+ setIsOpen(false);
+ setFocusedItemIndex(null);
+ setActiveItem(null);
+ };
+
+ const onTextInputChange = (_event: React.FormEvent, value: string) => {
+ setInputValue(value);
+ setFilterValue(value);
+ };
+
+ const handleMenuArrowKeys = (key: string) => {
+ let indexToFocus;
+
+ if (isOpen) {
+ if (key === 'ArrowUp') {
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
+ indexToFocus = selectOptions.length - 1;
+ } else {
+ indexToFocus = focusedItemIndex - 1;
+ }
+ }
+
+ if (key === 'ArrowDown') {
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = focusedItemIndex + 1;
+ }
+ }
+
+ setFocusedItemIndex(indexToFocus);
+ const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
+ setActiveItem(`select-create-typeahead-${focusedItem.itemId.replace(' ', '-')}`);
+ }
+ };
+
+ const onInputKeyDown = (event: React.KeyboardEvent) => {
+ const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled);
+ const [firstMenuItem] = enabledMenuItems;
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
+
+ switch (event.key) {
+ // Select the first available option
+ case 'Enter':
+ if (isOpen) {
+ onSelect(undefined, focusedItem.itemId as string);
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ setFocusedItemIndex(null);
+ setActiveItem(null);
+ }
+
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ setFocusedItemIndex(null);
+ setActiveItem(null);
+
+ break;
+ case 'Tab':
+ case 'Escape':
+ setIsOpen(false);
+ setActiveItem(null);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ event.preventDefault();
+ handleMenuArrowKeys(event.key);
+ break;
+ }
+ };
+
+ const toggle = (toggleRef: React.Ref) => (
+
+
+
+
+
+ {!!inputValue && (
+ {
+ setSelected('');
+ setInputValue('');
+ setFilterValue('');
+ textInputRef?.current?.focus();
+ }}
+ aria-label="Clear input value"
+ >
+
+
+ )}
+
+
+
+ );
+
+ return (
+ {
+ setIsOpen(false);
+ }}
+ toggle={toggle}
+ >
+
+ {selectOptions.map((option, index) => (
+ setSelected(option.itemId)}
+ id={`select-typeahead-${option.itemId.replace(' ', '-')}`}
+ {...option}
+ ref={null}
+ />
+ ))}
+
+
+ );
+};
diff --git a/packages/react-core/src/components/Select/examples/SelectViewMore.tsx b/packages/react-core/src/components/Select/examples/SelectViewMore.tsx
new file mode 100644
index 00000000000..740a64b1c45
--- /dev/null
+++ b/packages/react-core/src/components/Select/examples/SelectViewMore.tsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import { Select, SelectOption, SelectList, MenuToggle, Spinner } from '@patternfly/react-core';
+
+export const SelectViewMore: React.FunctionComponent = () => {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState('Select a value');
+ const [activeItem, setActiveItem] = React.useState(0);
+ const [isLoading, setIsLoading] = React.useState(false);
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [selectOptions, setSelectOptions] = React.useState([
+
+ Option 1
+ ,
+
+ Option 2
+ ,
+
+ Option 3
+ ,
+
+ Option 4
+ ,
+
+ Option 5
+ ,
+
+ Option 6
+ ,
+
+ Option 7
+ ,
+
+ Option 8
+ ,
+
+ Option 9
+ ,
+
+ Final Option 10
+
+ ]);
+ const [numOptions, setNumOptions] = React.useState(3);
+ const [visibleOptions, setVisibleOptions] = React.useState(selectOptions.slice(0, numOptions));
+ const activeItemRef = React.useRef(null);
+ const viewMoreRef = React.useRef(null);
+ const toggleRef = React.useRef(null);
+
+ React.useEffect(() => {
+ activeItemRef.current?.focus();
+ }, [visibleOptions]);
+
+ const simulateNetworkCall = (networkCallback: () => void) => {
+ setTimeout(networkCallback, 2000);
+ };
+
+ const getNextValidItem = (startingIndex: number, maxLength: number) => {
+ let validItem;
+ for (let i = startingIndex; i < maxLength; i++) {
+ if (selectOptions[i].props.isDisabled) {
+ continue;
+ } else {
+ validItem = selectOptions[i];
+ break;
+ }
+ }
+ return validItem;
+ };
+
+ const loadMoreOptions = () => {
+ const newLength = numOptions + 3 <= selectOptions.length ? numOptions + 3 : selectOptions.length;
+ const prevPosition = numOptions;
+ const nextValidItem = getNextValidItem(prevPosition, newLength);
+
+ setNumOptions(newLength);
+ setIsLoading(false);
+ setActiveItem(nextValidItem.props.itemId);
+ setVisibleOptions(selectOptions.slice(0, newLength));
+ };
+
+ const onViewMoreClick = () => {
+ setIsLoading(true);
+ simulateNetworkCall(() => {
+ loadMoreOptions();
+ });
+ };
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (_event: React.MouseEvent | undefined, itemId: string | number | undefined) => {
+ // eslint-disable-next-line no-console
+ console.log('selected', itemId);
+
+ if (itemId !== 'loader') {
+ setSelected(itemId as string);
+ setIsOpen(false);
+ toggleRef?.current?.focus(); // Only focus the toggle when a non-loader option is selected
+ }
+ };
+
+ const toggle = (
+
+ {selected}
+
+ );
+
+ return (
+ setIsOpen(isOpen)}
+ toggle={{ toggleNode: toggle, toggleRef }}
+ >
+
+ {visibleOptions.map((option) => {
+ const props = option.props;
+
+ return ;
+ })}
+ {numOptions !== selectOptions.length && (
+
+ {isLoading ? : 'View more'}
+
+ )}
+
+
+ );
+};
diff --git a/packages/react-core/src/demos/ComposableMenu/ComposableMenu.md b/packages/react-core/src/demos/ComposableMenu/ComposableMenu.md
index c06bda7770b..b221de57461 100644
--- a/packages/react-core/src/demos/ComposableMenu/ComposableMenu.md
+++ b/packages/react-core/src/demos/ComposableMenu/ComposableMenu.md
@@ -32,36 +32,43 @@ Custom menus can be constructed using a composable approach by combining the [Me
### Composable simple dropdown
```ts file="./examples/ComposableSimpleDropdown.tsx"
+
```
### Composable actions menu
```ts file="./examples/ComposableActionsMenu.tsx"
+
```
### Composable simple select
```ts file="./examples/ComposableSimpleSelect.tsx"
+
```
### Composable simple checkbox select
```ts file="./examples/ComposableSimpleCheckboxSelect.tsx"
+
```
### Composable typeahead select
```ts file="./examples/ComposableTypeaheadSelect.tsx"
+
```
### Composable multiple typeahead select
```ts file="./examples/ComposableMultipleTypeaheadSelect.tsx"
+
```
### Composable drilldown menu
```ts isBeta file="./examples/ComposableDrilldownMenu.tsx"
+
```
### Composable tree view menu
@@ -69,6 +76,7 @@ Custom menus can be constructed using a composable approach by combining the [Me
When rendering a menu-like element that does not contain MenuItem components, [Panel](/components/panel) allows more flexible control and customization.
```ts file="./examples/ComposableTreeViewMenu.tsx"
+
```
### Composable flyout
@@ -76,29 +84,35 @@ When rendering a menu-like element that does not contain MenuItem components, [P
The flyout will automatically position to the left or top if it would otherwise go outside the window. The menu must be placed in a container outside the main content like Popper, [Popover](/components/popover) or [Tooltip](/components/tooltip) since it may go over the side nav.
```ts isBeta file="./examples/ComposableFlyout.tsx"
+
```
### Composable application launcher
```ts file="./examples/ComposableApplicationLauncher.tsx"
+
```
### Composable context selector
```ts file="./examples/ComposableContextSelector.tsx"
+
```
### Composable options menu variants
```ts file="./examples/ComposableOptionsMenuVariants.tsx"
+
```
### Composable dropdown variants
```ts file="./examples/ComposableDropdwnVariants.tsx"
+
```
### Composable date select
```ts file="./examples/ComposableDateSelect.tsx"
+
```
diff --git a/packages/react-core/src/helpers/Popper/Popper.tsx b/packages/react-core/src/helpers/Popper/Popper.tsx
index b6ad58ada3e..75cfcec7687 100644
--- a/packages/react-core/src/helpers/Popper/Popper.tsx
+++ b/packages/react-core/src/helpers/Popper/Popper.tsx
@@ -68,7 +68,7 @@ export interface PopperProps {
position?: 'right' | 'left' | 'center';
/** Instead of direction and position can set the placement of the popper */
placement?: Placement;
- /** Custsom width of the popper. If the value is "trigger", it will set the width to the trigger element's width */
+ /** Custom width of the popper. If the value is "trigger", it will set the width to the trigger element's width */
width?: string | 'trigger';
/** Minimum width of the popper. If the value is "trigger", it will set the min width to the trigger element's width */
minWidth?: string | 'trigger';