From b56e22a3fd8ca711396a81421551de9f92d7a10a Mon Sep 17 00:00:00 2001
From: Jenny <32821331+jenny-s51@users.noreply.github.com>
Date: Fri, 24 Mar 2023 16:05:20 -0400
Subject: [PATCH 1/2] feat(Select): Deprecated old select and promoted select
next
---
.../CalendarMonth/CalendarMonth.tsx | 40 +-
.../__snapshots__/DatePicker.test.tsx.snap | 53 +-
.../examples/LoginPageLanguageSelect.tsx | 47 +-
.../src/components/MenuToggle/MenuToggle.tsx | 3 +-
.../__snapshots__/MenuToggle.test.tsx.snap | 28 +-
.../src/components/Select/Select.tsx | 1588 +--------
.../src/components/Select/SelectGroup.tsx | 38 +-
.../components/Select/SelectList.tsx | 2 +-
.../src/components/Select/SelectOption.tsx | 474 +--
.../src/components/Select/examples/Select.md | 2908 +---------------
.../Select/examples/SelectBasic.tsx | 3 +-
.../Select/examples/SelectCheckbox.tsx | 3 +-
.../Select/examples/SelectGrouped.tsx | 3 +-
.../Select/examples/SelectMultiTypeahead.tsx | 5 +-
.../Select/examples/SelectTypeahead.tsx | 5 +-
.../react-core/src/components/Select/index.ts | 2 +-
.../Toolbar/__tests__/Toolbar.test.tsx | 24 +-
.../__snapshots__/Toolbar.test.tsx.snap | 136 +-
.../components/Toolbar/examples/Toolbar.md | 1 -
.../ToolbarComponentManagedToggleGroups.tsx | 122 +-
.../ToolbarConsumerManagedToggleGroups.tsx | 120 +-
.../ToolbarCustomChipGroupContent.tsx | 130 +-
.../Toolbar/examples/ToolbarGroups.tsx | 155 +-
.../Toolbar/examples/ToolbarStacked.tsx | 95 +-
.../Toolbar/examples/ToolbarWithFilters.tsx | 143 +-
.../Tooltip/examples/TooltipOptions.tsx | 76 +-
packages/react-core/src/demos/Card/Card.md | 62 +-
packages/react-core/src/demos/CardDemos.md | 145 +-
packages/react-core/src/demos/Toolbar.md | 72 +-
.../src/demos/examples/Tabs/TabsAndTable.tsx | 27 +-
.../deprecated/components/Select/Select.tsx | 1495 +++++++++
.../components/Select/SelectGroup.tsx | 36 +
.../components/Select/SelectMenu.tsx | 4 +-
.../components/Select/SelectOption.tsx | 461 +++
.../components/Select/SelectToggle.tsx | 6 +-
.../Select/__tests__/Select.test.tsx | 2 +-
.../Select/__tests__/SelectGroup.test.tsx | 0
.../Select/__tests__/SelectOption.test.tsx | 0
.../Select/__tests__/SelectToggle.test.tsx | 0
.../__snapshots__/Select.test.tsx.snap | 0
.../__snapshots__/SelectGroup.test.tsx.snap | 0
.../__snapshots__/SelectOption.test.tsx.snap | 0
.../__snapshots__/SelectToggle.test.tsx.snap | 0
.../components/Select/examples/Select.md | 2936 +++++++++++++++++
.../components/Select/index.ts | 2 +-
.../components/Select/selectConstants.tsx | 0
.../src/deprecated/components/index.ts | 1 +
.../src/next/components/Select/Select.tsx | 133 -
.../next/components/Select/SelectGroup.tsx | 24 -
.../next/components/Select/SelectOption.tsx | 31 -
.../next/components/Select/examples/Select.md | 38 -
.../react-core/src/next/components/index.ts | 1 -
.../demo-app-ts/src/Demos.ts | 22 +-
.../components/demos/FormDemo/FormDemo.tsx | 10 +-
.../FilteringSelectDemo.tsx | 3 +-
.../FilteringSelectLiveUpdateDemo.tsx | 2 +-
.../SelectDemo.tsx | 16 +-
.../SelectFavoritesDemo.tsx | 10 +-
.../SelectFooterFilteringDemo.tsx | 10 +-
.../SelectInModal.tsx | 8 +-
.../SelectTypeaheadFooterDemo.tsx | 3 +-
.../SelectValidatedDemo.tsx | 3 +-
.../SelectViewMoreDemo.tsx | 3 +-
.../SelectViewMoreGroupedDemo.tsx | 10 +-
.../SelectViewMoreTypeaheadGroupedDemo.tsx | 3 +-
.../demos/TableDemo/TableEditableDemo.tsx | 3 +-
.../demos/ToolbarDemo/ToolbarDemo.tsx | 6 +-
.../demos/TopologyDemo/useTopologyOptions.tsx | 8 +-
.../demo-app-ts/src/components/demos/index.ts | 22 +-
.../Table/EditableSelectInputCell.tsx | 5 +-
.../src/components/Table/base/types.tsx | 5 +-
.../Table/examples/TableSortableCustom.tsx | 97 +-
.../examples/LegacyTableSortableCustom.tsx | 112 +-
.../components/Table/examples/Table.md | 26 +-
packages/react-table/src/docs/demos/Table.md | 186 +-
.../demos/table-demos/ColumnManagement.jsx | 34 +-
.../src/docs/demos/table-demos/Compact.jsx | 36 +-
.../demos/table-demos/CompoundExpansion.jsx | 30 +-
.../demos/table-demos/SortableResponsive.jsx | 166 +-
.../table-demos/StaticBottomPagination.jsx | 32 +-
80 files changed, 6456 insertions(+), 6095 deletions(-)
rename packages/react-core/src/{next => }/components/Select/SelectList.tsx (92%)
rename packages/react-core/src/{next => }/components/Select/examples/SelectBasic.tsx (89%)
rename packages/react-core/src/{next => }/components/Select/examples/SelectCheckbox.tsx (92%)
rename packages/react-core/src/{next => }/components/Select/examples/SelectGrouped.tsx (91%)
rename packages/react-core/src/{next => }/components/Select/examples/SelectMultiTypeahead.tsx (98%)
rename packages/react-core/src/{next => }/components/Select/examples/SelectTypeahead.tsx (98%)
create mode 100644 packages/react-core/src/deprecated/components/Select/Select.tsx
create mode 100644 packages/react-core/src/deprecated/components/Select/SelectGroup.tsx
rename packages/react-core/src/{ => deprecated}/components/Select/SelectMenu.tsx (98%)
create mode 100644 packages/react-core/src/deprecated/components/Select/SelectOption.tsx
rename packages/react-core/src/{ => deprecated}/components/Select/SelectToggle.tsx (98%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/Select.test.tsx (99%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/SelectGroup.test.tsx (100%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/SelectOption.test.tsx (100%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/SelectToggle.test.tsx (100%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/__snapshots__/Select.test.tsx.snap (100%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/__snapshots__/SelectGroup.test.tsx.snap (100%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/__snapshots__/SelectOption.test.tsx.snap (100%)
rename packages/react-core/src/{ => deprecated}/components/Select/__tests__/__snapshots__/SelectToggle.test.tsx.snap (100%)
create mode 100644 packages/react-core/src/deprecated/components/Select/examples/Select.md
rename packages/react-core/src/{next => deprecated}/components/Select/index.ts (71%)
rename packages/react-core/src/{ => deprecated}/components/Select/selectConstants.tsx (100%)
delete mode 100644 packages/react-core/src/next/components/Select/Select.tsx
delete mode 100644 packages/react-core/src/next/components/Select/SelectGroup.tsx
delete mode 100644 packages/react-core/src/next/components/Select/SelectOption.tsx
delete mode 100644 packages/react-core/src/next/components/Select/examples/Select.md
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/FilteringSelectDemo.tsx (96%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/FilteringSelectLiveUpdateDemo.tsx (98%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectDemo.tsx (99%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectFavoritesDemo.tsx (99%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectFooterFilteringDemo.tsx (98%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectInModal.tsx (97%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectTypeaheadFooterDemo.tsx (93%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectValidatedDemo.tsx (94%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectViewMoreDemo.tsx (98%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectViewMoreGroupedDemo.tsx (98%)
rename packages/react-integration/demo-app-ts/src/components/demos/{SelectDemo => SelectDeprecatedDemo}/SelectViewMoreTypeaheadGroupedDemo.tsx (95%)
diff --git a/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx b/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx
index e0093cb58f7..38e70fad421 100644
--- a/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx
+++ b/packages/react-core/src/components/CalendarMonth/CalendarMonth.tsx
@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
-import { TextInput } from '../TextInput/TextInput';
-import { Button } from '../Button/Button';
-import { Select, SelectOption } from '../Select';
+import { TextInput } from '../TextInput';
+import { Button } from '../Button';
+import { Select, SelectList, SelectOption } from '../Select';
+import { MenuToggle, MenuToggleElement } from '../MenuToggle';
import { InputGroup } from '../InputGroup';
import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
@@ -263,13 +264,21 @@ export const CalendarMonth = ({
Month
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 0a190bf7690..a7876dbb096 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
@@ -103,45 +103,36 @@ exports[`With popover opened 1`] = `
>
Month
-
+
diff --git a/packages/react-core/src/components/LoginPage/examples/LoginPageLanguageSelect.tsx b/packages/react-core/src/components/LoginPage/examples/LoginPageLanguageSelect.tsx
index 849b6880ef3..6d24a04e6f6 100644
--- a/packages/react-core/src/components/LoginPage/examples/LoginPageLanguageSelect.tsx
+++ b/packages/react-core/src/components/LoginPage/examples/LoginPageLanguageSelect.tsx
@@ -8,9 +8,11 @@ import {
LoginPage,
ListItem,
ListVariant,
+ MenuToggle,
+ MenuToggleElement,
Select,
- SelectOption,
- SelectOptionObject
+ SelectList,
+ SelectOption
} from '@patternfly/react-core';
import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
@@ -22,7 +24,7 @@ export const LoginPageLanguageSelect: React.FunctionComponent = () => {
const [isValidPassword, setIsValidPassword] = React.useState(true);
const [isRememberMeChecked, setIsRememberMeChecked] = React.useState(false);
const [isHeaderUtilsOpen, setIsHeaderUtilsOpen] = React.useState(false);
- const [selectedHeaderUtils, setSelectedHeaderUtils] = React.useState
('English');
+ const [selectedHeaderUtils, setSelectedHeaderUtils] = React.useState('English');
/** i18n object is used to simulate i18n integration of native language translation */
const i18n = {
@@ -35,23 +37,21 @@ export const LoginPageLanguageSelect: React.FunctionComponent = () => {
Bengali: 'বাংলা'
};
- const headerUtilsOptions = [
- ,
- ,
- ,
- ,
- ,
- ,
-
- ];
-
- const onHeaderUtilsToggle = (_event: any, isExpanded: boolean) => {
- setIsHeaderUtilsOpen(isExpanded);
- };
+ const headerUtilsOptions = (
+
+ {i18n.English}
+ {i18n.Mandarin}
+ {i18n.Hindi}
+ {i18n.Spanish}
+ {i18n.Portuguese}
+ {i18n.Arabic}
+ {i18n.Bengali}
+
+ );
const onHeaderUtilsSelect = (
_event: React.MouseEvent | React.ChangeEvent,
- value: string | SelectOptionObject
+ value: string
) => {
setSelectedHeaderUtils(value);
setIsHeaderUtilsOpen(false);
@@ -60,9 +60,18 @@ export const LoginPageLanguageSelect: React.FunctionComponent = () => {
const headerUtils = (
@@ -267,7 +155,7 @@ exports[`Toolbar should render with custom chip content 1`] = `
class="pf-c-toolbar__item pf-m-chip-group"
>
Status
@@ -301,7 +189,7 @@ exports[`Toolbar should render with custom chip content 1`] = `
>
New
@@ -312,12 +200,12 @@ exports[`Toolbar should render with custom chip content 1`] = `
diff --git a/packages/react-core/src/demos/Card/Card.md b/packages/react-core/src/demos/Card/Card.md
index 65ccac01b12..5f387ba4225 100644
--- a/packages/react-core/src/demos/Card/Card.md
+++ b/packages/react-core/src/demos/Card/Card.md
@@ -39,6 +39,7 @@ This demonstrates how you can assemble a full page view that contains a grid of
```js isFullscreen
import React from 'react';
import {
+ Badge,
Bullseye,
Button,
Card,
@@ -63,16 +64,16 @@ import {
PageSection,
PageSectionVariants,
Pagination,
- Select,
- SelectOption,
- SelectVariant,
TextContent,
Text,
Title,
Toolbar,
ToolbarItem,
ToolbarFilter,
- ToolbarContent
+ ToolbarContent,
+ Select,
+ SelectList,
+ SelectOption
} from '@patternfly/react-core';
import {
Dropdown as DropdownDeprecated,
@@ -131,9 +132,9 @@ class CardViewBasic extends React.Component {
return selected === total;
};
- this.onToolbarDropdownToggle = (_event, isLowerToolbarDropdownOpen) => {
+ this.onToolbarDropdownToggle = () => {
this.setState((prevState) => ({
- isLowerToolbarDropdownOpen
+ isLowerToolbarDropdownOpen: !prevState.isLowerToolbarDropdownOpen
}));
};
@@ -462,29 +463,44 @@ class CardViewBasic extends React.Component {
buildFilterDropdown() {
const { isLowerToolbarDropdownOpen, filters } = this.state;
- const filterDropdownItems = [
- ,
- ,
- ,
- ,
- ,
- ,
- ,
- ,
- ,
-
- ];
+ const filterDropdownItems = (
+
+ PatternFly
+ ActiveMQ
+ Apache Spark
+ Avro
+ Azure Services
+ Crypto
+ DropBox
+ JBoss Data Grid
+ REST
+ SWAGGER
+
+ );
return (
diff --git a/packages/react-core/src/demos/CardDemos.md b/packages/react-core/src/demos/CardDemos.md
index f9e0b30c034..87a96b6de55 100644
--- a/packages/react-core/src/demos/CardDemos.md
+++ b/packages/react-core/src/demos/CardDemos.md
@@ -1254,7 +1254,10 @@ import {
StackItem,
Divider,
Select,
- SelectOption
+ SelectList,
+ SelectOption,
+ MenuToggle,
+ MenuToggleElement
} from '@patternfly/react-core';
import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { Chart, ChartStack, ChartBar, ChartTooltip } from '@patternfly/react-charts';
@@ -1266,21 +1269,32 @@ import chart_color_red_100 from '@patternfly/react-tokens/dist/esm/chart_color_r
const UtilizationCard3: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
- const selectItems = [
- ,
- ,
- ,
-
- ];
+ const selectItems = (
+
+ Last hour
+ Last 6 hours
+ Last 24 hours
+ Last 7 days
+
+ );
+
+ const toggle = (toggleRef: React.Ref) => (
+ setIsOpen(!isOpen)}
+ isExpanded={isOpen}
+ variant="plainText"
+ >
+ Filter
+
+ );
const headerActions = (
@@ -1454,7 +1468,6 @@ import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-ch
;
```
-
### Nested cards
```js
@@ -2523,28 +2536,42 @@ import {
FlexItem,
Divider,
Select,
- SelectOption
+ SelectList,
+ SelectOption,
+ MenuToggle,
+ MenuToggleElement
} from '@patternfly/react-core';
import { ChartArea, ChartContainer, ChartGroup, ChartLabel, ChartVoronoiContainer } from '@patternfly/react-charts';
const TrendCard1: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
- const selectItems = [
- ,
- ,
- ,
-
- ];
+ const selectItems = (
+
+ Last hour
+ Last 6 hours
+ Last 24 hours
+ Last 7 days
+
+ );
+
+ const toggle = (toggleRef: React.Ref) => (
+ setIsOpen(!isOpen)}
+ isExpanded={isOpen}
+ variant="plainText"
+ >
+ Filter
+
+ );
const headerActions = (
@@ -2701,8 +2728,10 @@ import {
DescriptionListTerm,
DescriptionListDescription,
Select,
+ SelectList,
SelectOption,
- Divider
+ Divider,
+ MenuToggle
} from '@patternfly/react-core';
CardLogViewDemo = () => {
@@ -2716,21 +2745,32 @@ CardLogViewDemo = () => {
setIsOpen(isOpen);
};
- const selectItems = [
- ,
- ,
- ,
-
- ];
+ const selectItems = (
+
+ Last hour
+ Last 6 hours
+ Last 24 hours
+ Last 7 days
+
+ );
+
+ const toggle = (toggleRef) => (
+ setIsOpen(!isOpen)}
+ isExpanded={isOpen}
+ variant="plainText"
+ >
+ Filter
+
+ );
const headerActions = (
@@ -2819,8 +2859,10 @@ import {
DescriptionListDescription,
Spinner,
Select,
+ SelectList,
SelectOption,
- Divider
+ Divider,
+ MenuToggle
} from '@patternfly/react-core';
import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon';
@@ -2836,20 +2878,31 @@ CardEventViewDemo = () => {
setIsOpen(isOpen);
};
- const selectItems = [
- ,
- ,
-
- ];
+ const selectItems = (
+
+ Success
+ Error
+ Error
+
+ );
+
+ const toggle = (toggleRef) => (
+ setIsOpen(!isOpen)}
+ isExpanded={isOpen}
+ variant="plainText"
+ >
+ Status
+
+ );
const headerActions = (
diff --git a/packages/react-core/src/demos/Toolbar.md b/packages/react-core/src/demos/Toolbar.md
index 60598e6203f..11c48b3f685 100644
--- a/packages/react-core/src/demos/Toolbar.md
+++ b/packages/react-core/src/demos/Toolbar.md
@@ -30,16 +30,22 @@ This is an example of toolbar usage in log viewer.
```js isFullscreen
import React from 'react';
-import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, ToolbarToggleGroup } from '@patternfly/react-core';
import {
Badge,
Button,
Checkbox,
+ MenuToggle,
SearchInput,
Select,
+ SelectList,
SelectOption,
PageSection,
PageSectionVariants,
+ Toolbar,
+ ToolbarContent,
+ ToolbarGroup,
+ ToolbarItem,
+ ToolbarToggleGroup,
Tooltip
} from '@patternfly/react-core';
import {
@@ -90,15 +96,19 @@ class ConsoleLogViewerToolbar extends React.Component {
mobileView: window.innerWidth >= 1450 ? false : true
};
- this.onContainerToggle = (_event, isExpanded) => {
- this.setState({
- containerExpanded: isExpanded
+ this.onContainerToggle = () => {
+ this.setState(prevState => {
+ return {
+ containerExpanded: !prevState.containerExpanded
+ }
});
};
- this.onContainerToggleMobile = (_event, isExpanded) => {
- this.setState({
- containerExpandedMobile: isExpanded
+ this.onContainerToggleMobile = () => {
+ this.setState(prevState => {
+ return {
+ containerExpandedMobile: !prevState.containerExpandedMobile
+ }
});
};
@@ -332,19 +342,19 @@ class ConsoleLogViewerToolbar extends React.Component {
];
const selectDropdownContent = (
-
+
{Object.entries(this.firstOptions).map(([value, { type }]) => (
{type}
{` ${value}`}
))}
-
+
);
const selectToggleContent = ({ showText }) => {
@@ -379,13 +389,23 @@ class ConsoleLogViewerToolbar extends React.Component {
Select container}>
diff --git a/packages/react-core/src/demos/examples/Tabs/TabsAndTable.tsx b/packages/react-core/src/demos/examples/Tabs/TabsAndTable.tsx
index 1b9791fdb37..c21b5f24f5a 100644
--- a/packages/react-core/src/demos/examples/Tabs/TabsAndTable.tsx
+++ b/packages/react-core/src/demos/examples/Tabs/TabsAndTable.tsx
@@ -27,8 +27,6 @@ import {
PageSectionVariants,
Progress,
ProgressSize,
- Select,
- SelectVariant,
Tabs,
Tab,
TabContent,
@@ -58,6 +56,7 @@ import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
import CodeBranchIcon from '@patternfly/react-icons/dist/esm/icons/code-branch-icon';
import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
+import SortAmountDownIcon from '@patternfly/react-icons/dist/esm/icons/sort-amount-down-icon';
interface Repository {
name: string;
@@ -152,28 +151,12 @@ export const TablesAndTabs = () => {
} breakpoint="xl">
-
- {/* TODO: replace with select after #8073 goes in
- }
- aria-label="Sort"
- hideCaret
- />
- }
- />
- */}
+
+
+
diff --git a/packages/react-core/src/deprecated/components/Select/Select.tsx b/packages/react-core/src/deprecated/components/Select/Select.tsx
new file mode 100644
index 00000000000..74f9ea49158
--- /dev/null
+++ b/packages/react-core/src/deprecated/components/Select/Select.tsx
@@ -0,0 +1,1495 @@
+import * as React from 'react';
+import styles from '@patternfly/react-styles/css/components/Select/select';
+import badgeStyles from '@patternfly/react-styles/css/components/Badge/badge';
+import formStyles from '@patternfly/react-styles/css/components/FormControl/form-control';
+import buttonStyles from '@patternfly/react-styles/css/components/Button/button';
+import { css } from '@patternfly/react-styles';
+import TimesCircleIcon from '@patternfly/react-icons/dist/esm/icons/times-circle-icon';
+import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon';
+import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
+import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
+import { SelectMenu } from './SelectMenu';
+import { SelectOption, SelectOptionObject } from './SelectOption';
+import { SelectGroup, SelectGroupProps } from './SelectGroup';
+import { SelectToggle } from './SelectToggle';
+import {
+ SelectContext,
+ SelectVariant,
+ SelectPosition,
+ SelectDirection,
+ SelectFooterTabbableItems
+} from './selectConstants';
+import { ChipGroup, ChipGroupProps } from '../../../components/ChipGroup';
+import { Chip } from '../../../components/Chip';
+import { Spinner } from '../../../components/Spinner';
+import {
+ keyHandler,
+ getNextIndex,
+ getOUIAProps,
+ OUIAProps,
+ getDefaultOUIAId,
+ PickOptional,
+ GenerateId
+} from '../../../helpers';
+import { KeyTypes } from '../../../helpers/constants';
+import { Divider } from '../../../components/Divider';
+import { Popper } from '../../../helpers/Popper/Popper';
+import { createRenderableFavorites, extendItemsWithFavorite } from '../../../helpers/favorites';
+import { ValidatedOptions } from '../../../helpers/constants';
+import { findTabbableElements } from '../../../helpers/util';
+
+// seed for the aria-labelledby ID
+let currentId = 0;
+
+export interface SelectViewMoreObject {
+ /** View more text */
+ text: string;
+ /** Callback for when the view more button is clicked */
+ onClick: (event: React.MouseEvent | React.ChangeEvent) => void;
+}
+export interface SelectProps
+ extends Omit, 'onSelect' | 'ref' | 'checked' | 'selected'>,
+ OUIAProps {
+ /** Content rendered inside the Select. Must be React.ReactElement[] */
+ children?: React.ReactElement[];
+ /** Classes applied to the root of the Select */
+ className?: string;
+ /** Indicates where menu will be aligned horizontally */
+ position?: SelectPosition | 'right' | 'left';
+ /** Flag specifying which direction the Select menu expands */
+ direction?: 'up' | 'down';
+ /** Flag to indicate if select is open */
+ isOpen?: boolean;
+ /** Flag to indicate if select options are grouped */
+ isGrouped?: boolean;
+ /** Display the toggle with no border or background */
+ isPlain?: boolean;
+ /** Flag to indicate if select is disabled */
+ isDisabled?: boolean;
+ /** Flag to indicate if the typeahead select allows new items */
+ isCreatable?: boolean;
+ /** Flag to indicate if create option should be at top of typeahead */
+ isCreateOptionOnTop?: boolean;
+ /** Flag indicating if placeholder styles should be applied */
+ hasPlaceholderStyle?: boolean;
+ /** Flag indicating if the creatable option should set its value as a SelectOptionObject */
+ isCreateSelectOptionObject?: boolean;
+ /** Value to indicate if the select is modified to show that validation state.
+ * If set to success, select will be modified to indicate valid state.
+ * If set to error, select will be modified to indicate error state.
+ * If set to warning, select will be modified to indicate warning state.
+ */
+ validated?: 'success' | 'warning' | 'error' | 'default';
+ /** Loading variant to display either the spinner or the view more text button */
+ loadingVariant?: 'spinner' | SelectViewMoreObject;
+ /** Text displayed in typeahead select to prompt the user to create an item */
+ createText?: string;
+ /** Title text of Select */
+ placeholderText?: string | React.ReactNode;
+ /** Text to display in typeahead select when no results are found */
+ noResultsFoundText?: string;
+ /** Array of selected items for multi select variants. */
+ selections?: string | SelectOptionObject | (string | SelectOptionObject)[];
+ /** Flag indicating if selection badge should be hidden for checkbox variant,default false */
+ isCheckboxSelectionBadgeHidden?: boolean;
+ /** Id for select toggle element */
+ toggleId?: string;
+ /** Ref for the select toggle element */
+ toggleRef?: React.Ref | React.Ref;
+ /** Adds accessible text to Select */
+ 'aria-label'?: string;
+ /** Id of label for the Select aria-labelledby */
+ 'aria-labelledby'?: string;
+ /** Id of div for the select aria-labelledby */
+ 'aria-describedby'?: string;
+ /** Flag indicating if the select is an invalid state */
+ 'aria-invalid'?: boolean;
+ /** Label for input field of type ahead select variants */
+ typeAheadAriaLabel?: string;
+ /** Id of div for input field of type ahead select variants */
+ typeAheadAriaDescribedby?: string;
+ /** Label for clear selection button of type ahead select variants */
+ clearSelectionsAriaLabel?: string;
+ /** Label for toggle of type ahead select variants */
+ toggleAriaLabel?: string;
+ /** Label for remove chip button of multiple type ahead select variant */
+ removeSelectionAriaLabel?: string;
+ /** ID list of favorited select items */
+ favorites?: string[];
+ /** Label for the favorites group */
+ favoritesLabel?: string;
+ /** Enables favorites. Callback called when a select options's favorite button is clicked */
+ onFavorite?: (itemId: string, isFavorite: boolean) => void;
+ /** Callback for selection behavior */
+ onSelect?: (
+ event: React.MouseEvent | React.ChangeEvent,
+ value: string | SelectOptionObject,
+ isPlaceholder?: boolean
+ ) => void;
+ /** Callback for toggle button behavior */
+ onToggle: (event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent | Event, isExpanded: boolean) => void;
+ /** Callback for toggle blur */
+ onBlur?: (event?: any) => void;
+ /** Callback for typeahead clear button */
+ onClear?: (event: React.MouseEvent) => void;
+ /** Optional callback for custom filtering */
+ onFilter?: (e: React.ChangeEvent | null, value: string) => React.ReactElement[] | undefined;
+ /** Optional callback for newly created options */
+ onCreateOption?: (newOptionValue: string) => void;
+ /** Optional event handler called each time the value in the typeahead input changes. */
+ onTypeaheadInputChanged?: (value: string) => void;
+ /** Variant of rendered Select */
+ variant?: 'single' | 'checkbox' | 'typeahead' | 'typeaheadmulti';
+ /** Width of the select container as a number of px or string percentage */
+ width?: string | number;
+ /** Max height of the select container as a number of px or string percentage */
+ maxHeight?: string | number;
+ /** Icon element to render inside the select toggle */
+ toggleIcon?: React.ReactElement;
+ /** Custom icon for the dropdown replacing the CaretDownIcon */
+ toggleIndicator?: React.ReactElement;
+ /** Custom content to render in the select menu. If this prop is defined, the variant prop will be ignored and the select will render with a single select toggle */
+ customContent?: React.ReactNode;
+ /** Flag indicating if select should have an inline text input for filtering */
+ hasInlineFilter?: boolean;
+ /** Placeholder text for inline filter */
+ inlineFilterPlaceholderText?: string;
+ /** Custom text for select badge */
+ customBadgeText?: string | number;
+ /** Prefix for the id of the input in the checkbox select variant*/
+ inputIdPrefix?: string;
+ /** Value for the typeahead and inline filtering input autocomplete attribute. When targeting Chrome this property should be a random string. */
+ inputAutoComplete?: string;
+ /** Optional props to pass to the chip group in the typeaheadmulti variant */
+ chipGroupProps?: Omit;
+ /** Optional props to render custom chip group in the typeaheadmulti variant */
+ chipGroupComponent?: React.ReactNode;
+ /** Flag for retaining keyboard-entered value in typeahead text field when focus leaves input away */
+ isInputValuePersisted?: boolean;
+ /** Flag for retaining filter results on blur from keyboard-entered typeahead text */
+ isInputFilterPersisted?: boolean;
+ /** Flag indicating the typeahead input value should reset upon selection */
+ shouldResetOnSelect?: boolean;
+ /** Content rendered in the footer of the select menu */
+ footer?: React.ReactNode;
+ /** The container to append the menu to. Defaults to 'inline'.
+ * If your menu is being cut off you can append it to an element higher up the DOM tree.
+ * Some examples:
+ * menuAppendTo="parent"
+ * menuAppendTo={() => document.body}
+ * menuAppendTo={document.getElementById('target')}
+ */
+ menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
+ /** Flag for indicating that the select menu should automatically flip vertically when
+ * it reaches the boundary. This prop can only be used when the select component is not
+ * appended inline, e.g. `menuAppendTo="parent"`
+ */
+ isFlipEnabled?: boolean;
+ /** z-index of the select menu when menuAppendTo is not inline. */
+ zIndex?: number;
+ /** Value to overwrite the randomly generated data-ouia-component-id.*/
+ ouiaId?: number | string;
+ /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */
+ ouiaSafe?: boolean;
+}
+
+export interface SelectState {
+ focusFirstOption: boolean;
+ typeaheadInputValue: string | null;
+ typeaheadFilteredChildren: React.ReactNode[];
+ favoritesGroup: React.ReactNode[];
+ typeaheadCurrIndex: number;
+ creatableValue: string;
+ tabbedIntoFavoritesMenu: boolean;
+ typeaheadStoredIndex: number;
+ ouiaStateId: string;
+ viewMoreNextIndex: number;
+}
+
+export class Select extends React.Component {
+ static displayName = 'Select';
+ private parentRef = React.createRef();
+ private menuComponentRef = React.createRef();
+ private filterRef = React.createRef();
+ private clearRef = React.createRef();
+ private inputRef = React.createRef();
+ private refCollection: HTMLElement[][] = [[]];
+ private optionContainerRefCollection: HTMLElement[] = [];
+ private footerRef = React.createRef();
+
+ static defaultProps: PickOptional = {
+ children: [] as React.ReactElement[],
+ className: '',
+ position: SelectPosition.left,
+ direction: SelectDirection.down,
+ toggleId: null as string,
+ isOpen: false,
+ isGrouped: false,
+ isPlain: false,
+ isDisabled: false,
+ hasPlaceholderStyle: false,
+ isCreatable: false,
+ isCreateOptionOnTop: false,
+ validated: 'default',
+ 'aria-label': '',
+ 'aria-labelledby': '',
+ 'aria-describedby': '',
+ 'aria-invalid': false,
+ typeAheadAriaLabel: '',
+ typeAheadAriaDescribedby: '',
+ clearSelectionsAriaLabel: 'Clear all',
+ toggleAriaLabel: 'Options menu',
+ removeSelectionAriaLabel: 'Remove',
+ selections: [],
+ createText: 'Create',
+ placeholderText: '',
+ noResultsFoundText: 'No results found',
+ variant: SelectVariant.single,
+ width: '',
+ onClear: () => undefined as void,
+ onCreateOption: () => undefined as void,
+ toggleIcon: null as React.ReactElement,
+ toggleIndicator: null as React.ReactElement,
+ onFilter: null,
+ onTypeaheadInputChanged: null,
+ customContent: null,
+ hasInlineFilter: false,
+ inlineFilterPlaceholderText: null,
+ customBadgeText: null,
+ inputIdPrefix: '',
+ inputAutoComplete: 'off',
+ menuAppendTo: 'inline',
+ favorites: [] as string[],
+ favoritesLabel: 'Favorites',
+ ouiaSafe: true,
+ chipGroupComponent: null,
+ isInputValuePersisted: false,
+ isInputFilterPersisted: false,
+ isCreateSelectOptionObject: false,
+ shouldResetOnSelect: true,
+ isFlipEnabled: true,
+ zIndex: 9999
+ };
+
+ state: SelectState = {
+ focusFirstOption: false,
+ typeaheadInputValue: null,
+ typeaheadFilteredChildren: React.Children.toArray(this.props.children),
+ favoritesGroup: [] as React.ReactNode[],
+ typeaheadCurrIndex: -1,
+ typeaheadStoredIndex: -1,
+ creatableValue: '',
+ tabbedIntoFavoritesMenu: false,
+ ouiaStateId: getDefaultOUIAId(Select.displayName, this.props.variant),
+ viewMoreNextIndex: -1
+ };
+
+ getTypeaheadActiveChild = (typeaheadCurrIndex: number) =>
+ this.refCollection[typeaheadCurrIndex] ? this.refCollection[typeaheadCurrIndex][0] : null;
+
+ componentDidUpdate = (prevProps: SelectProps, prevState: SelectState) => {
+ if (this.props.hasInlineFilter) {
+ this.refCollection[0][0] = this.filterRef.current;
+ }
+
+ // Move focus to top of the menu if state.focusFirstOption was updated to true and the menu does not have custom content
+ if (!prevState.focusFirstOption && this.state.focusFirstOption && !this.props.customContent) {
+ const firstRef = this.refCollection.find(
+ ref =>
+ // If a select option is disabled then ref[0] will be undefined, so we want to return
+ // the first ref that both a) is not null and b) is not disabled.
+ ref !== null && ref[0]
+ );
+
+ if (firstRef && firstRef[0]) {
+ firstRef[0].focus();
+ }
+ } else if (
+ // if viewMoreNextIndex is not -1, view more was clicked, set focus on first newly loaded item
+ this.state.viewMoreNextIndex !== -1 &&
+ this.refCollection.length > this.state.viewMoreNextIndex &&
+ this.props.loadingVariant !== 'spinner' &&
+ this.refCollection[this.state.viewMoreNextIndex][0] &&
+ this.props.variant !== 'typeahead' && // do not hard focus newly added items for typeahead variants
+ this.props.variant !== 'typeaheadmulti'
+ ) {
+ this.refCollection[this.state.viewMoreNextIndex][0].focus();
+ this.setState({ viewMoreNextIndex: -1 });
+ }
+
+ const checkUpdatedChildren = (prevChildren: React.ReactElement[], currChildren: React.ReactElement[]) =>
+ Array.from(prevChildren).some((prevChild: React.ReactElement, index: number) => {
+ const prevChildProps = prevChild.props;
+ const currChild = currChildren[index];
+ const { props: currChildProps } = currChild;
+
+ if (prevChildProps && currChildProps) {
+ return (
+ prevChildProps.value !== currChildProps.value ||
+ prevChildProps.label !== currChildProps.label ||
+ prevChildProps.isDisabled !== currChildProps.isDisabled ||
+ prevChildProps.isPlaceholder !== currChildProps.isPlaceholder
+ );
+ } else {
+ return prevChild !== currChild;
+ }
+ });
+
+ const hasUpdatedChildren =
+ prevProps.children.length !== this.props.children.length ||
+ checkUpdatedChildren(prevProps.children, this.props.children) ||
+ (this.props.isGrouped &&
+ Array.from(prevProps.children).some(
+ (prevChild: React.ReactElement, index: number) =>
+ prevChild.type === SelectGroup &&
+ prevChild.props.children &&
+ this.props.children[index].props.children &&
+ (prevChild.props.children.length !== this.props.children[index].props.children.length ||
+ checkUpdatedChildren(prevChild.props.children, this.props.children[index].props.children))
+ ));
+
+ if (hasUpdatedChildren) {
+ this.updateTypeAheadFilteredChildren(prevState.typeaheadInputValue || '', null);
+ }
+
+ // for menus with favorites,
+ // if the number of favorites or typeahead filtered children has changed, the generated
+ // list of favorites needs to be updated
+ if (
+ this.props.onFavorite &&
+ (this.props.favorites.length !== prevProps.favorites.length ||
+ this.state.typeaheadFilteredChildren !== prevState.typeaheadFilteredChildren)
+ ) {
+ const tempRenderableChildren =
+ this.props.variant === 'typeahead' || this.props.variant === 'typeaheadmulti'
+ ? this.state.typeaheadFilteredChildren
+ : this.props.children;
+ const renderableFavorites = createRenderableFavorites(
+ tempRenderableChildren,
+ this.props.isGrouped,
+ this.props.favorites
+ );
+ const favoritesGroup = renderableFavorites.length
+ ? [
+
+ {renderableFavorites}
+ ,
+
+ ]
+ : [];
+ this.setState({ favoritesGroup });
+ }
+ };
+
+ onEnter = () => {
+ this.setState({ focusFirstOption: true });
+ };
+
+ onToggle = (e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent | Event, isExpanded: boolean) => {
+ const { isInputValuePersisted, onSelect, onToggle, hasInlineFilter } = this.props;
+ if (!isExpanded && isInputValuePersisted && onSelect) {
+ onSelect(undefined, this.inputRef.current ? this.inputRef.current.value : '');
+ }
+ if (isExpanded && hasInlineFilter) {
+ this.setState({
+ focusFirstOption: true
+ });
+ }
+ onToggle(e, isExpanded);
+ };
+
+ onClose = () => {
+ const { isInputFilterPersisted } = this.props;
+
+ this.setState({
+ focusFirstOption: false,
+ typeaheadInputValue: null,
+ ...(!isInputFilterPersisted && {
+ typeaheadFilteredChildren: React.Children.toArray(this.props.children)
+ }),
+ typeaheadCurrIndex: -1,
+ tabbedIntoFavoritesMenu: false,
+ viewMoreNextIndex: -1
+ });
+ };
+
+ onChange = (e: React.ChangeEvent) => {
+ if (e.target.value.toString() !== '' && !this.props.isOpen) {
+ this.onToggle(e, true);
+ }
+
+ if (this.props.onTypeaheadInputChanged) {
+ this.props.onTypeaheadInputChanged(e.target.value.toString());
+ }
+
+ this.setState({
+ typeaheadCurrIndex: -1,
+ typeaheadInputValue: e.target.value,
+ creatableValue: e.target.value
+ });
+ this.updateTypeAheadFilteredChildren(e.target.value.toString(), e);
+ this.refCollection = [[]];
+ };
+
+ updateTypeAheadFilteredChildren = (typeaheadInputValue: string, e: React.ChangeEvent | null) => {
+ let typeaheadFilteredChildren: any;
+
+ const {
+ onFilter,
+ isCreatable,
+ isCreateOptionOnTop,
+ onCreateOption,
+ createText,
+ noResultsFoundText,
+ children,
+ isGrouped,
+ isCreateSelectOptionObject,
+ loadingVariant
+ } = this.props;
+
+ if (onFilter) {
+ /* The updateTypeAheadFilteredChildren callback is not only called on input changes but also when the children change.
+ * In this case the e is null but we can get the typeaheadInputValue from the state.
+ */
+ typeaheadFilteredChildren = onFilter(e, e ? e.target.value : typeaheadInputValue) || children;
+ } else {
+ let input: RegExp;
+ try {
+ input = new RegExp(typeaheadInputValue.toString(), 'i');
+ } catch (err) {
+ input = new RegExp(typeaheadInputValue.toString().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
+ }
+ const childrenArray = React.Children.toArray(children) as React.ReactElement[];
+ if (isGrouped) {
+ const childFilter = (child: React.ReactElement) =>
+ child.props.value &&
+ child.props.value.toString &&
+ this.getDisplay(child.props.value.toString(), 'text').search(input) === 0;
+ typeaheadFilteredChildren =
+ typeaheadInputValue.toString() !== ''
+ ? React.Children.map(children, group => {
+ if (
+ React.isValidElement>(group) &&
+ group.type === SelectGroup
+ ) {
+ const filteredGroupChildren = (React.Children.toArray(group.props.children) as React.ReactElement<
+ SelectGroupProps
+ >[]).filter(childFilter);
+ if (filteredGroupChildren.length > 0) {
+ return React.cloneElement(group, {
+ titleId: group.props.label && group.props.label.replace(/\W/g, '-'),
+ children: filteredGroupChildren as any
+ });
+ }
+ } else {
+ return (React.Children.toArray(group) as React.ReactElement[]).filter(childFilter);
+ }
+ })
+ : childrenArray;
+ } else {
+ typeaheadFilteredChildren =
+ typeaheadInputValue.toString() !== ''
+ ? childrenArray.filter(child => {
+ const valueToCheck = child.props.value;
+ // Dividers don't have value and should not be filtered
+ if (!valueToCheck) {
+ return true;
+ }
+
+ const isSelectOptionObject =
+ typeof valueToCheck !== 'string' &&
+ (valueToCheck as SelectOptionObject).toString &&
+ (valueToCheck as SelectOptionObject).compareTo;
+
+ // View more option should be returned as not a match
+ if (loadingVariant !== 'spinner' && loadingVariant?.text === valueToCheck) {
+ return true;
+ }
+
+ // spinner should be returned as not a match
+ if (loadingVariant === 'spinner' && valueToCheck === 'loading') {
+ return true;
+ }
+
+ if (isSelectOptionObject) {
+ return (valueToCheck as SelectOptionObject).compareTo(typeaheadInputValue);
+ } else {
+ return this.getDisplay(child.props.value.toString(), 'text').search(input) === 0;
+ }
+ })
+ : childrenArray;
+ }
+ }
+ if (!typeaheadFilteredChildren) {
+ typeaheadFilteredChildren = [];
+ }
+ if (typeaheadFilteredChildren.length === 0) {
+ !isCreatable &&
+ typeaheadFilteredChildren.push(
+
+ );
+ }
+ if (isCreatable && typeaheadInputValue !== '') {
+ const newValue = typeaheadInputValue;
+ if (
+ !typeaheadFilteredChildren.find(
+ (i: React.ReactElement) =>
+ i.props.value && i.props.value.toString().toLowerCase() === newValue.toString().toLowerCase()
+ )
+ ) {
+ const newOptionValue = isCreateSelectOptionObject
+ ? ({
+ toString: () => newValue,
+ compareTo: value =>
+ this.toString()
+ .toLowerCase()
+ .includes(value.toString().toLowerCase())
+ } as SelectOptionObject)
+ : newValue;
+
+ const createSelectOption = (
+ onCreateOption && onCreateOption(newValue)}
+ >
+ {createText} "{newValue}"
+
+ );
+
+ if (isCreateOptionOnTop) {
+ typeaheadFilteredChildren.unshift(createSelectOption);
+ } else {
+ typeaheadFilteredChildren.push(createSelectOption);
+ }
+ }
+ }
+
+ this.setState({
+ typeaheadFilteredChildren
+ });
+ };
+
+ onClick = (e: React.MouseEvent) => {
+ if (!this.props.isOpen) {
+ this.onToggle(e, true);
+ }
+ };
+
+ clearSelection = (_e: React.MouseEvent) => {
+ this.setState({
+ typeaheadInputValue: null,
+ typeaheadFilteredChildren: React.Children.toArray(this.props.children),
+ typeaheadCurrIndex: -1
+ });
+ };
+
+ extendTypeaheadChildren(typeaheadCurrIndex: number, favoritesGroup?: React.ReactNode[]) {
+ const { isGrouped, onFavorite, createText } = this.props;
+ const typeaheadChildren = favoritesGroup
+ ? favoritesGroup.concat(this.state.typeaheadFilteredChildren)
+ : this.state.typeaheadFilteredChildren;
+ const activeElement = this.optionContainerRefCollection[typeaheadCurrIndex];
+
+ let typeaheadActiveChild = this.getTypeaheadActiveChild(typeaheadCurrIndex);
+ if (typeaheadActiveChild && typeaheadActiveChild.classList.contains('pf-m-description')) {
+ typeaheadActiveChild = typeaheadActiveChild.firstElementChild as HTMLElement;
+ }
+
+ this.refCollection = [[]];
+ this.optionContainerRefCollection = [];
+ if (isGrouped) {
+ return React.Children.map(typeaheadChildren as React.ReactElement[], (group: React.ReactElement) => {
+ if (group.type === Divider) {
+ return group;
+ } else if (group.type === SelectGroup && onFavorite) {
+ return React.cloneElement(group, {
+ titleId: group.props.label && group.props.label.replace(/\W/g, '-'),
+ children: React.Children.map(group.props.children, (child: React.ReactElement) =>
+ child.type === Divider
+ ? child
+ : React.cloneElement(child as React.ReactElement, {
+ isFocused:
+ activeElement &&
+ (activeElement.id === (child as React.ReactElement).props.id ||
+ (this.props.isCreatable &&
+ typeaheadActiveChild.textContent ===
+ `${createText} "${(group as React.ReactElement).props.value}"`))
+ })
+ )
+ });
+ } else if (group.type === SelectGroup) {
+ return React.cloneElement(group, {
+ titleId: group.props.label && group.props.label.replace(/\W/g, '-'),
+ children: React.Children.map(group.props.children, (child: React.ReactElement) =>
+ child.type === Divider
+ ? child
+ : React.cloneElement(child as React.ReactElement, {
+ isFocused:
+ typeaheadActiveChild &&
+ (typeaheadActiveChild.textContent === (child as React.ReactElement).props.value.toString() ||
+ (this.props.isCreatable &&
+ typeaheadActiveChild.textContent ===
+ `${createText} "${(child as React.ReactElement).props.value}"`))
+ })
+ )
+ });
+ } else {
+ // group has been filtered down to SelectOption
+ return React.cloneElement(group as React.ReactElement, {
+ isFocused:
+ typeaheadActiveChild &&
+ (typeaheadActiveChild.textContent === group.props.value.toString() ||
+ (this.props.isCreatable && typeaheadActiveChild.textContent === `${createText} "${group.props.value}"`))
+ });
+ }
+ });
+ }
+ return typeaheadChildren.map((child: React.ReactNode, index) => {
+ const childElement = child as any;
+ return childElement.type.displayName === 'Divider'
+ ? child
+ : React.cloneElement(child as React.ReactElement, {
+ isFocused: typeaheadActiveChild
+ ? typeaheadActiveChild.textContent === (child as React.ReactElement).props.value.toString() ||
+ (this.props.isCreatable &&
+ typeaheadActiveChild.textContent === `${createText} "${(child as React.ReactElement).props.value}"`)
+ : index === typeaheadCurrIndex // fallback for view more + typeahead use cases, when the new expanded list is loaded and refCollection hasn't be updated yet
+ });
+ });
+ }
+
+ sendRef = (
+ optionRef: React.ReactNode,
+ favoriteRef: React.ReactNode,
+ optionContainerRef: React.ReactNode,
+ index: number
+ ) => {
+ this.refCollection[index] = [(optionRef as unknown) as HTMLElement, (favoriteRef as unknown) as HTMLElement];
+ this.optionContainerRefCollection[index] = (optionContainerRef as unknown) as HTMLElement;
+ };
+
+ handleMenuKeys = (index: number, innerIndex: number, position: string) => {
+ keyHandler(index, innerIndex, position, this.refCollection, this.refCollection);
+ if (this.props.variant === SelectVariant.typeahead || this.props.variant === SelectVariant.typeaheadMulti) {
+ if (position !== 'tab') {
+ this.handleTypeaheadKeys(position);
+ }
+ }
+ };
+
+ moveFocus = (nextIndex: number, updateCurrentIndex: boolean = true) => {
+ const { isCreatable, createText } = this.props;
+
+ const hasDescriptionElm = Boolean(
+ this.refCollection[nextIndex][0] && this.refCollection[nextIndex][0].classList.contains('pf-m-description')
+ );
+ const isLoad = Boolean(
+ this.refCollection[nextIndex][0] && this.refCollection[nextIndex][0].classList.contains('pf-m-load')
+ );
+ const optionTextElm = hasDescriptionElm
+ ? (this.refCollection[nextIndex][0].firstElementChild as HTMLElement)
+ : this.refCollection[nextIndex][0];
+
+ let typeaheadInputValue = '';
+ if (isCreatable && optionTextElm.textContent.includes(createText)) {
+ typeaheadInputValue = this.state.creatableValue;
+ } else if (optionTextElm && !isLoad) {
+ // !isLoad prevents the view more button text from appearing the typeahead input
+ typeaheadInputValue = optionTextElm.textContent;
+ }
+ this.setState(prevState => ({
+ typeaheadCurrIndex: updateCurrentIndex ? nextIndex : prevState.typeaheadCurrIndex,
+ typeaheadStoredIndex: nextIndex,
+ typeaheadInputValue
+ }));
+ };
+
+ switchFocusToFavoriteMenu = () => {
+ const { typeaheadCurrIndex, typeaheadStoredIndex } = this.state;
+ let indexForFocus = 0;
+
+ if (typeaheadCurrIndex !== -1) {
+ indexForFocus = typeaheadCurrIndex;
+ } else if (typeaheadStoredIndex !== -1) {
+ indexForFocus = typeaheadStoredIndex;
+ }
+
+ if (this.refCollection[indexForFocus] !== null && this.refCollection[indexForFocus][0] !== null) {
+ this.refCollection[indexForFocus][0].focus();
+ } else {
+ this.clearRef.current.focus();
+ }
+
+ this.setState({
+ tabbedIntoFavoritesMenu: true,
+ typeaheadCurrIndex: -1
+ });
+ };
+
+ moveFocusToLastMenuItem = () => {
+ const refCollectionLen = this.refCollection.length;
+ if (
+ refCollectionLen > 0 &&
+ this.refCollection[refCollectionLen - 1] !== null &&
+ this.refCollection[refCollectionLen - 1][0] !== null
+ ) {
+ this.refCollection[refCollectionLen - 1][0].focus();
+ }
+ };
+
+ handleTypeaheadKeys = (position: string, shiftKey: boolean = false) => {
+ const { isOpen, onFavorite, isCreatable } = this.props;
+ const { typeaheadCurrIndex, tabbedIntoFavoritesMenu } = this.state;
+ const typeaheadActiveChild = this.getTypeaheadActiveChild(typeaheadCurrIndex);
+ if (isOpen) {
+ if (position === 'enter') {
+ if (
+ (typeaheadCurrIndex !== -1 || (isCreatable && this.refCollection.length === 1)) && // do not allow selection without moving to an initial option unless it is a single create option
+ (typeaheadActiveChild || (this.refCollection[0] && this.refCollection[0][0]))
+ ) {
+ if (typeaheadActiveChild) {
+ if (!typeaheadActiveChild.classList.contains('pf-m-load')) {
+ const hasDescriptionElm = typeaheadActiveChild.childElementCount > 1;
+ const typeaheadActiveChildText = hasDescriptionElm
+ ? (typeaheadActiveChild.firstChild as HTMLElement).textContent
+ : typeaheadActiveChild.textContent;
+ this.setState({
+ typeaheadInputValue: typeaheadActiveChildText
+ });
+ }
+ } else if (this.refCollection[0] && this.refCollection[0][0]) {
+ this.setState({
+ typeaheadInputValue: this.refCollection[0][0].textContent
+ });
+ }
+ if (typeaheadActiveChild) {
+ typeaheadActiveChild.click();
+ } else {
+ this.refCollection[0][0].click();
+ }
+ }
+ } else if (position === 'tab') {
+ if (onFavorite) {
+ // if the input has focus, tab to the first item or the last item that was previously focused.
+ if (this.inputRef.current === document.activeElement) {
+ // If shift is also clicked and there is a footer, tab to the last item in tabbable footer
+ if (this.props.footer && shiftKey) {
+ const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
+ if (tabbableItems.length > 0) {
+ if (tabbableItems[tabbableItems.length - 1]) {
+ tabbableItems[tabbableItems.length - 1].focus();
+ }
+ }
+ } else {
+ this.switchFocusToFavoriteMenu();
+ }
+ } else {
+ // focus is on menu or footer
+ if (this.props.footer) {
+ let tabbedIntoMenu = false;
+ const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
+ if (tabbableItems.length > 0) {
+ // if current element is not in footer, tab to first tabbable element in footer,
+ // if shift was clicked, tab to input since focus is on menu
+ const currentElementIndex = tabbableItems.findIndex((item: any) => item === document.activeElement);
+ if (currentElementIndex === -1) {
+ if (shiftKey) {
+ // currently in menu, shift back to input
+ this.inputRef.current.focus();
+ } else {
+ // currently in menu, tab to first tabbable item in footer
+ tabbableItems[0].focus();
+ }
+ } else {
+ // already in footer
+ if (shiftKey) {
+ // shift to previous item
+ if (currentElementIndex === 0) {
+ // on first footer item, shift back to menu
+ this.switchFocusToFavoriteMenu();
+ tabbedIntoMenu = true;
+ } else {
+ // shift to previous footer item
+ tabbableItems[currentElementIndex - 1].focus();
+ }
+ } else {
+ // tab to next tabbable item in footer or to input.
+ if (tabbableItems[currentElementIndex + 1]) {
+ tabbableItems[currentElementIndex + 1].focus();
+ } else {
+ this.inputRef.current.focus();
+ }
+ }
+ }
+ } else {
+ // no tabbable items in footer, tab to input
+ this.inputRef.current.focus();
+ tabbedIntoMenu = false;
+ }
+ this.setState({ tabbedIntoFavoritesMenu: tabbedIntoMenu });
+ } else {
+ this.inputRef.current.focus();
+ this.setState({ tabbedIntoFavoritesMenu: false });
+ }
+ }
+ } else {
+ // Close if there is no footer
+ if (!this.props.footer) {
+ this.onToggle(null, false);
+ this.onClose();
+ } else {
+ // has footer
+ const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
+ const currentElementIndex = tabbableItems.findIndex((item: any) => item === document.activeElement);
+ if (this.inputRef.current === document.activeElement) {
+ if (shiftKey) {
+ // close toggle if shift key and tab on input
+ this.onToggle(null, false);
+ this.onClose();
+ } else {
+ // tab to first tabbable item in footer
+ if (tabbableItems[0]) {
+ tabbableItems[0].focus();
+ } else {
+ this.onToggle(null, false);
+ this.onClose();
+ }
+ }
+ } else {
+ // focus is in footer
+ if (shiftKey) {
+ if (currentElementIndex === 0) {
+ // shift tab back to input
+ this.inputRef.current.focus();
+ } else {
+ // shift to previous footer item
+ tabbableItems[currentElementIndex - 1].focus();
+ }
+ } else {
+ // tab to next footer item or close tab if last item
+ if (tabbableItems[currentElementIndex + 1]) {
+ tabbableItems[currentElementIndex + 1].focus();
+ } else {
+ // no next item, close toggle
+ this.onToggle(null, false);
+ this.inputRef.current.focus();
+ this.onClose();
+ }
+ }
+ }
+ }
+ }
+ } else if (!tabbedIntoFavoritesMenu) {
+ if (this.refCollection[0][0] === null) {
+ return;
+ }
+ let nextIndex;
+ if (typeaheadCurrIndex === -1 && position === 'down') {
+ nextIndex = 0;
+ } else if (typeaheadCurrIndex === -1 && position === 'up') {
+ nextIndex = this.refCollection.length - 1;
+ } else if (position !== 'left' && position !== 'right') {
+ nextIndex = getNextIndex(typeaheadCurrIndex, position, this.refCollection);
+ } else {
+ nextIndex = typeaheadCurrIndex;
+ }
+ if (this.refCollection[nextIndex] === null) {
+ return;
+ }
+ this.moveFocus(nextIndex);
+ } else {
+ const nextIndex = this.refCollection.findIndex(
+ ref => ref !== undefined && (ref[0] === document.activeElement || ref[1] === document.activeElement)
+ );
+ this.moveFocus(nextIndex);
+ }
+ }
+ };
+
+ onClickTypeaheadToggleButton = () => {
+ if (this.inputRef && this.inputRef.current) {
+ this.inputRef.current.focus();
+ }
+ };
+
+ getDisplay = (value: string | SelectOptionObject, type: 'node' | 'text' = 'node') => {
+ if (!value) {
+ return;
+ }
+ const item = this.props.isGrouped
+ ? (React.Children.toArray(this.props.children) as React.ReactElement[])
+ .reduce((acc, curr) => [...acc, ...React.Children.toArray(curr.props.children)], [])
+ .find(child => child.props.value.toString() === value.toString())
+ : React.Children.toArray(this.props.children).find(
+ child =>
+ (child as React.ReactElement).props.value &&
+ (child as React.ReactElement).props.value.toString() === value.toString()
+ );
+ if (item) {
+ if (item && item.props.children) {
+ if (type === 'node') {
+ return item.props.children;
+ }
+ return this.findText(item);
+ }
+ return item.props.value.toString();
+ }
+ return value.toString();
+ };
+
+ findText = (item: React.ReactNode) => {
+ if (typeof item === 'string') {
+ return item;
+ } else if (!React.isValidElement(item)) {
+ return '';
+ } else {
+ const multi: string[] = [];
+ React.Children.toArray(item.props.children).forEach(child => multi.push(this.findText(child)));
+ return multi.join('');
+ }
+ };
+
+ generateSelectedBadge = () => {
+ const { customBadgeText, selections } = this.props;
+ if (customBadgeText !== null) {
+ return customBadgeText;
+ }
+ if (Array.isArray(selections) && selections.length > 0) {
+ return selections.length;
+ }
+ return null;
+ };
+
+ setVieMoreNextIndex = () => {
+ this.setState({ viewMoreNextIndex: this.refCollection.length - 1 });
+ };
+
+ isLastOptionBeforeFooter = (index: any) =>
+ this.props.footer && index === this.refCollection.length - 1 ? true : false;
+
+ render() {
+ const {
+ children,
+ chipGroupProps,
+ chipGroupComponent,
+ className,
+ customContent,
+ variant,
+ direction,
+ onSelect,
+ onClear,
+ onBlur,
+ toggleId,
+ toggleRef,
+ isOpen,
+ isGrouped,
+ isPlain,
+ isDisabled,
+ hasPlaceholderStyle,
+ validated,
+ selections: selectionsProp,
+ typeAheadAriaLabel,
+ typeAheadAriaDescribedby,
+ clearSelectionsAriaLabel,
+ toggleAriaLabel,
+ removeSelectionAriaLabel,
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledBy,
+ 'aria-describedby': ariaDescribedby,
+ 'aria-invalid': ariaInvalid,
+ placeholderText,
+ width,
+ maxHeight,
+ toggleIcon,
+ toggleIndicator,
+ ouiaId,
+ ouiaSafe,
+ hasInlineFilter,
+ isCheckboxSelectionBadgeHidden,
+ inlineFilterPlaceholderText,
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ onFilter,
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ onTypeaheadInputChanged,
+ onCreateOption,
+ isCreatable,
+ onToggle,
+ createText,
+ noResultsFoundText,
+ customBadgeText,
+ inputIdPrefix,
+ inputAutoComplete,
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ isInputValuePersisted,
+ isInputFilterPersisted,
+ /* eslint-enable @typescript-eslint/no-unused-vars */
+ menuAppendTo,
+ favorites,
+ onFavorite,
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ favoritesLabel,
+ footer,
+ loadingVariant,
+ isCreateSelectOptionObject,
+ isCreateOptionOnTop,
+ shouldResetOnSelect,
+ isFlipEnabled,
+ zIndex,
+ ...props
+ } = this.props;
+ const {
+ focusFirstOption: openedOnEnter,
+ typeaheadCurrIndex,
+ typeaheadInputValue,
+ typeaheadFilteredChildren,
+ favoritesGroup
+ } = this.state;
+ const selectToggleId = toggleId || `pf-select-toggle-id-${currentId++}`;
+ const selections = Array.isArray(selectionsProp) ? selectionsProp : [selectionsProp];
+ // Find out if the selected option is a placeholder
+ const selectedOption = React.Children.toArray(children).find(
+ (option: any) => option.props.value === selections[0]
+ ) as any;
+ const isSelectedPlaceholder = selectedOption && selectedOption.props.isPlaceholder;
+ const hasAnySelections = Boolean(selections[0] && selections[0] !== '');
+ const typeaheadActiveChild = this.getTypeaheadActiveChild(typeaheadCurrIndex);
+ let childPlaceholderText = null as string;
+
+ // If onFavorites is set, add isFavorite prop to children and add a Favorites group to the SelectMenu
+ let renderableItems: React.ReactNode[] = [];
+ if (onFavorite) {
+ // if variant is type-ahead call the extendTypeaheadChildren before adding favorites
+ let tempExtendedChildren: (React.ReactElement | React.ReactNode | {})[] = children;
+ if (variant === 'typeahead' || variant === 'typeaheadmulti') {
+ tempExtendedChildren = this.extendTypeaheadChildren(typeaheadCurrIndex, favoritesGroup);
+ } else if (onFavorite) {
+ tempExtendedChildren = favoritesGroup.concat(children);
+ }
+ // mark items that are favorited with isFavorite
+ renderableItems = extendItemsWithFavorite(tempExtendedChildren, isGrouped, favorites);
+ } else {
+ renderableItems = children;
+ }
+
+ if (!customContent) {
+ if (!hasAnySelections && !placeholderText) {
+ const childPlaceholder = React.Children.toArray(children).filter(
+ (child: React.ReactNode) => (child as React.ReactElement).props.isPlaceholder === true
+ );
+ childPlaceholderText =
+ (childPlaceholder[0] && this.getDisplay((childPlaceholder[0] as React.ReactElement).props.value, 'node')) ||
+ (children[0] && this.getDisplay(children[0].props.value, 'node'));
+ }
+ }
+
+ if (isOpen) {
+ if (renderableItems.find(item => (item as any)?.key === 'loading') === undefined) {
+ if (loadingVariant === 'spinner') {
+ renderableItems.push(
+
+
+
+ );
+ } else if (loadingVariant?.text) {
+ renderableItems.push(
+
+ );
+ }
+ }
+ }
+
+ const hasOnClear = onClear !== Select.defaultProps.onClear;
+ const clearBtn = (
+
+ );
+
+ let selectedChips = null as any;
+ if (variant === SelectVariant.typeaheadMulti) {
+ selectedChips = chipGroupComponent ? (
+ chipGroupComponent
+ ) : (
+
+ {selections &&
+ (selections as string[]).map(item => (
+ onSelect(e, item)}
+ closeBtnAriaLabel={removeSelectionAriaLabel}
+ >
+ {this.getDisplay(item, 'node')}
+
+ ))}
+
+ );
+ }
+
+ if (hasInlineFilter) {
+ const filterBox = (
+
+
+ {
+ if (event.key === KeyTypes.ArrowUp) {
+ this.handleMenuKeys(0, 0, 'up');
+ event.preventDefault();
+ } else if (event.key === KeyTypes.ArrowDown) {
+ this.handleMenuKeys(0, 0, 'down');
+ event.preventDefault();
+ } else if (event.key === KeyTypes.ArrowLeft) {
+ this.handleMenuKeys(0, 0, 'left');
+ event.preventDefault();
+ } else if (event.key === KeyTypes.ArrowRight) {
+ this.handleMenuKeys(0, 0, 'right');
+ event.preventDefault();
+ } else if (event.key === KeyTypes.Tab && variant !== SelectVariant.checkbox && this.props.footer) {
+ // tab to footer or close menu if shift key
+ if (event.shiftKey) {
+ this.onToggle(event, false);
+ } else {
+ const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
+ if (tabbableItems.length > 0) {
+ tabbableItems[0].focus();
+ event.stopPropagation();
+ event.preventDefault();
+ } else {
+ this.onToggle(event, false);
+ }
+ }
+ } else if (event.key === KeyTypes.Tab && variant === SelectVariant.checkbox) {
+ // More modal-like experience for checkboxes
+ // Let SelectOption handle this
+ if (event.shiftKey) {
+ this.handleMenuKeys(0, 0, 'up');
+ } else {
+ this.handleMenuKeys(0, 0, 'down');
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }}
+ ref={this.filterRef}
+ autoComplete={inputAutoComplete}
+ />
+
+
+
+ );
+ renderableItems = [filterBox, ...(typeaheadFilteredChildren as React.ReactElement[])].map((option, index) =>
+ React.cloneElement(option, { key: index })
+ );
+ }
+
+ let variantProps: any;
+ let variantChildren: any;
+ if (customContent) {
+ variantProps = {
+ selected: selections,
+ openedOnEnter,
+ isCustomContent: true
+ };
+ variantChildren = customContent;
+ } else {
+ switch (variant) {
+ case 'single':
+ variantProps = {
+ selected: selections[0],
+ hasInlineFilter,
+ openedOnEnter
+ };
+ variantChildren = renderableItems;
+ break;
+ case 'checkbox':
+ variantProps = {
+ checked: selections,
+ isGrouped,
+ hasInlineFilter,
+ openedOnEnter
+ };
+ variantChildren = renderableItems;
+ break;
+ case 'typeahead':
+ variantProps = {
+ selected: selections[0],
+ openedOnEnter
+ };
+ variantChildren = onFavorite ? renderableItems : this.extendTypeaheadChildren(typeaheadCurrIndex);
+ if (variantChildren.length === 0) {
+ variantChildren.push();
+ }
+ break;
+ case 'typeaheadmulti':
+ variantProps = {
+ selected: selections,
+ openedOnEnter
+ };
+ variantChildren = onFavorite ? renderableItems : this.extendTypeaheadChildren(typeaheadCurrIndex);
+ if (variantChildren.length === 0) {
+ variantChildren.push();
+ }
+ break;
+ }
+ }
+
+ const isStatic = isFlipEnabled && menuAppendTo !== 'inline';
+ const innerMenu = (
+
+ {variantChildren}
+
+ );
+
+ const menuContainer = footer ? {innerMenu}
: innerMenu;
+
+ const popperRef = React.createRef();
+ const popperContainer = (
+
+ {isOpen && menuContainer}
+
+ );
+
+ const mainContainer = (
+
+
+ {customContent && (
+
+ {toggleIcon && {toggleIcon}}
+ {placeholderText}
+
+ )}
+ {variant === SelectVariant.single && !customContent && (
+
+
+ {toggleIcon && {toggleIcon}}
+
+ {this.getDisplay(selections[0] as string, 'node') || placeholderText || childPlaceholderText}
+
+
+ {hasOnClear && hasAnySelections && clearBtn}
+
+ )}
+ {variant === SelectVariant.checkbox && !customContent && (
+
+
+ {toggleIcon &&
{toggleIcon}}
+
{placeholderText}
+ {!isCheckboxSelectionBadgeHidden && hasAnySelections && (
+
+
+ {this.generateSelectedBadge()}
+
+
+ )}
+
+ {hasOnClear && hasAnySelections && clearBtn}
+
+ )}
+ {variant === SelectVariant.typeahead && !customContent && (
+
+
+ {toggleIcon && {toggleIcon}}
+
+
+ {hasOnClear && (selections[0] || typeaheadInputValue) && clearBtn}
+
+ )}
+ {variant === SelectVariant.typeaheadMulti && !customContent && (
+
+
+ {toggleIcon && {toggleIcon}}
+ {selections && Array.isArray(selections) && selections.length > 0 && selectedChips}
+
+
+ {hasOnClear && ((selections && selections.length > 0) || typeaheadInputValue) && clearBtn}
+
+ )}
+ {validated === ValidatedOptions.success && (
+
+
+
+ )}
+ {validated === ValidatedOptions.error && (
+
+
+
+ )}
+ {validated === ValidatedOptions.warning && (
+
+
+
+ )}
+
+ {isOpen && menuAppendTo === 'inline' && menuContainer}
+
+ );
+
+ const getParentElement = () => {
+ if (this.parentRef && this.parentRef.current) {
+ return this.parentRef.current.parentElement;
+ }
+ return null;
+ };
+
+ return (
+
+ {randomId => (
+
+ {menuAppendTo === 'inline' ? (
+ mainContainer
+ ) : (
+
+ )}
+
+ )}
+
+ );
+ }
+}
diff --git a/packages/react-core/src/deprecated/components/Select/SelectGroup.tsx b/packages/react-core/src/deprecated/components/Select/SelectGroup.tsx
new file mode 100644
index 00000000000..82556acb4bf
--- /dev/null
+++ b/packages/react-core/src/deprecated/components/Select/SelectGroup.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import styles from '@patternfly/react-styles/css/components/Select/select';
+import { css } from '@patternfly/react-styles';
+
+import { SelectConsumer, SelectVariant } from './selectConstants';
+
+export interface SelectGroupProps extends React.HTMLProps {
+ /** Checkboxes within group. Must be React.ReactElement[] */
+ children?: React.ReactNode;
+ /** Additional classes added to the CheckboxSelectGroup control */
+ className?: string;
+ /** Group label */
+ label?: string;
+ /** ID for title label */
+ titleId?: string;
+}
+
+export const SelectGroup: React.FunctionComponent = ({
+ children = [],
+ className = '',
+ label = '',
+ titleId = '',
+ ...props
+}: SelectGroupProps) => (
+
+ {({ variant }) => (
+
+
+ {label}
+
+ {variant === SelectVariant.checkbox ? children :
}
+
+ )}
+
+);
+SelectGroup.displayName = 'SelectGroup';
diff --git a/packages/react-core/src/components/Select/SelectMenu.tsx b/packages/react-core/src/deprecated/components/Select/SelectMenu.tsx
similarity index 98%
rename from packages/react-core/src/components/Select/SelectMenu.tsx
rename to packages/react-core/src/deprecated/components/Select/SelectMenu.tsx
index bbdd822f8ae..c773ab55053 100644
--- a/packages/react-core/src/components/Select/SelectMenu.tsx
+++ b/packages/react-core/src/deprecated/components/Select/SelectMenu.tsx
@@ -4,10 +4,10 @@ import formStyles from '@patternfly/react-styles/css/components/Form/form';
import { css } from '@patternfly/react-styles';
import { SelectOptionObject, SelectOption } from './SelectOption';
import { SelectConsumer, SelectPosition, SelectVariant, SelectContextInterface } from './selectConstants';
-import { PickOptional } from '../../helpers/typeUtils';
+import { PickOptional } from '../../../helpers/typeUtils';
import { SelectGroup } from './SelectGroup';
-import { Divider } from '../Divider/Divider';
+import { Divider } from '../../../components/Divider/Divider';
export interface SelectMenuProps extends Omit, 'checked' | 'selected' | 'ref'> {
/** Content rendered inside the SelectMenu */
diff --git a/packages/react-core/src/deprecated/components/Select/SelectOption.tsx b/packages/react-core/src/deprecated/components/Select/SelectOption.tsx
new file mode 100644
index 00000000000..0f80f35494c
--- /dev/null
+++ b/packages/react-core/src/deprecated/components/Select/SelectOption.tsx
@@ -0,0 +1,461 @@
+import * as React from 'react';
+import styles from '@patternfly/react-styles/css/components/Select/select';
+import checkStyles from '@patternfly/react-styles/css/components/Check/check';
+import { css } from '@patternfly/react-styles';
+import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
+import { SelectConsumer, SelectVariant } from './selectConstants';
+import StarIcon from '@patternfly/react-icons/dist/esm/icons/star-icon';
+import { getUniqueId } from '../../../helpers/util';
+import { KeyTypes } from '../../../helpers/constants';
+
+export interface SelectOptionObject {
+ /** Function returns a string to represent the select option object */
+ toString(): string;
+ /** Function returns a true if the passed in select option is equal to this select option object, false otherwise */
+ compareTo?(selectOption: any): boolean;
+}
+export interface SelectOptionProps extends Omit, 'type' | 'ref' | 'value'> {
+ /** Optional alternate display for the option */
+ children?: React.ReactNode;
+ /** Additional classes added to the Select Option */
+ className?: string;
+ /** Description of the item for single and both typeahead select variants */
+ description?: React.ReactNode;
+ /** Number of items matching the option */
+ itemCount?: number;
+ /** @hide Internal index of the option */
+ index?: number;
+ /** Indicates which component will be used as select item */
+ component?: React.ReactNode;
+ /** The value for the option, can be a string or select option object */
+ value: string | SelectOptionObject;
+ /** Flag indicating if the option is disabled */
+ isDisabled?: boolean;
+ /** Flag indicating if the option acts as a placeholder */
+ isPlaceholder?: boolean;
+ /** Flag indicating if the option acts as a "no results" indicator */
+ isNoResultsOption?: boolean;
+ /** @hide Internal flag indicating if the option is selected */
+ isSelected?: boolean;
+ /** @hide Internal flag indicating if the option is checked */
+ isChecked?: boolean;
+ /** Flag forcing the focused state */
+ isFocused?: boolean;
+ /** @hide Internal callback for ref tracking */
+ sendRef?: (
+ ref: React.ReactNode,
+ favoriteRef: React.ReactNode,
+ optionContainerRef: React.ReactNode,
+ index: number
+ ) => void;
+ /** @hide Internal callback for keyboard navigation */
+ keyHandler?: (index: number, innerIndex: number, position: string) => void;
+ /** Optional callback for click event */
+ onClick?: (event: React.MouseEvent | React.ChangeEvent) => void;
+ /** Id of the checkbox input */
+ inputId?: string;
+ /** @hide Internal Flag indicating if the option is favorited */
+ isFavorite?: boolean;
+ /** Aria label text for favoritable button when favorited */
+ ariaIsFavoriteLabel?: string;
+ /** Aria label text for favoritable button when not favorited */
+ ariaIsNotFavoriteLabel?: string;
+ /** ID of the item. Required for tracking favorites */
+ id?: string;
+ /** @hide Internal flag to apply the load styling to view more option */
+ isLoad?: boolean;
+ /** @hide Internal flag to apply the loading styling to spinner */
+ isLoading?: boolean;
+ /** @hide Internal callback for the setting the index of the next item to focus after view more is clicked */
+ setViewMoreNextIndex?: () => void;
+ /** @hide Flag indicating this is the last option when there is a footer */
+ isLastOptionBeforeFooter?: (index: number) => boolean;
+ /** @hide Flag indicating that the the option loading variant is in a grouped select */
+ isGrouped?: boolean;
+}
+
+export class SelectOption extends React.Component {
+ static displayName = 'SelectOption';
+ private ref = React.createRef();
+ private liRef = React.createRef();
+ private favoriteRef = React.createRef();
+ static defaultProps: SelectOptionProps = {
+ className: '',
+ value: '',
+ index: 0,
+ isDisabled: false,
+ isPlaceholder: false,
+ isSelected: false,
+ isChecked: false,
+ isNoResultsOption: false,
+ component: 'button',
+ onClick: () => {},
+ sendRef: () => {},
+ keyHandler: () => {},
+ inputId: '',
+ isFavorite: null,
+ isLoad: false,
+ isLoading: false,
+ setViewMoreNextIndex: () => {},
+ isLastOptionBeforeFooter: () => false
+ };
+
+ componentDidMount() {
+ this.props.sendRef(
+ this.props.isDisabled ? null : this.ref.current,
+ this.props.isDisabled ? null : this.favoriteRef.current,
+ this.props.isDisabled ? null : this.liRef.current,
+ this.props.index
+ );
+ }
+
+ componentDidUpdate() {
+ this.props.sendRef(
+ this.props.isDisabled ? null : this.ref.current,
+ this.props.isDisabled ? null : this.favoriteRef.current,
+ this.props.isDisabled ? null : this.liRef.current,
+ this.props.index
+ );
+ }
+
+ onKeyDown = (event: React.KeyboardEvent, innerIndex: number, onEnter?: any, isCheckbox?: boolean) => {
+ const { index, keyHandler, isLastOptionBeforeFooter } = this.props;
+ let isLastItemBeforeFooter = false;
+ if (isLastOptionBeforeFooter !== undefined) {
+ isLastItemBeforeFooter = isLastOptionBeforeFooter(index);
+ }
+
+ if (event.key === KeyTypes.Tab) {
+ // More modal-like experience for checkboxes
+ if (isCheckbox && !isLastItemBeforeFooter) {
+ if (event.shiftKey) {
+ keyHandler(index, innerIndex, 'up');
+ } else {
+ keyHandler(index, innerIndex, 'down');
+ }
+ event.stopPropagation();
+ } else {
+ if (event.shiftKey) {
+ keyHandler(index, innerIndex, 'up');
+ } else {
+ keyHandler(index, innerIndex, 'tab');
+ }
+ }
+ }
+ event.preventDefault();
+ if (event.key === KeyTypes.ArrowUp) {
+ keyHandler(index, innerIndex, 'up');
+ } else if (event.key === KeyTypes.ArrowDown) {
+ keyHandler(index, innerIndex, 'down');
+ } else if (event.key === KeyTypes.ArrowLeft) {
+ keyHandler(index, innerIndex, 'left');
+ } else if (event.key === KeyTypes.ArrowRight) {
+ keyHandler(index, innerIndex, 'right');
+ } else if (event.key === KeyTypes.Enter) {
+ if (onEnter !== undefined) {
+ onEnter();
+ } else {
+ this.ref.current.click();
+ }
+ }
+ };
+
+ render() {
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ const {
+ children,
+ className,
+ id,
+ description,
+ itemCount,
+ value,
+ onClick,
+ isDisabled,
+ isPlaceholder,
+ isNoResultsOption,
+ isSelected,
+ isChecked,
+ isFocused,
+ sendRef,
+ keyHandler,
+ index,
+ component,
+ inputId,
+ isFavorite,
+ ariaIsFavoriteLabel = 'starred',
+ ariaIsNotFavoriteLabel = 'not starred',
+ isLoad,
+ isLoading,
+ setViewMoreNextIndex,
+ // eslint-disable-next-line no-console
+ isLastOptionBeforeFooter,
+ isGrouped = false,
+ ...props
+ } = this.props;
+ /* eslint-enable @typescript-eslint/no-unused-vars */
+ const Component = component as any;
+
+ if (!id && isFavorite !== null) {
+ // eslint-disable-next-line no-console
+ console.error('Please provide an id to use the favorites feature.');
+ }
+
+ const generatedId = id || getUniqueId('select-option');
+ const favoriteButton = (onFavorite: any) => (
+
+ );
+
+ const itemDisplay = itemCount ? (
+
+
+ {children || (value && value.toString && value.toString())}
+
+ {itemCount}
+
+ ) : (
+ children || value.toString()
+ );
+
+ const onViewMoreClick = (event: any) => {
+ // Set the index for the next item to focus after view more clicked, then call view more callback
+ setViewMoreNextIndex();
+ onClick(event);
+ };
+
+ const renderOption = (
+ onSelect: (
+ event: React.MouseEvent | React.ChangeEvent,
+ value: string | SelectOptionObject,
+ isPlaceholder?: boolean
+ ) => void,
+ onClose: () => void,
+ variant: string,
+ inputIdPrefix: string,
+ onFavorite: (itemId: string, isFavorite: boolean) => void,
+ shouldResetOnSelect: boolean
+ ) => {
+ if (variant !== SelectVariant.checkbox && isLoading && isGrouped) {
+ return (
+
+ {children}
+
+ );
+ } else if (variant !== SelectVariant.checkbox && isLoad && isGrouped) {
+ return (
+
+
+
+ );
+ } else if (variant !== SelectVariant.checkbox) {
+ return (
+
+ {isLoading && children}
+ {isLoad && !isGrouped && (
+
+ )}
+ {!isLoading && !isLoad && (
+ <>
+ {
+ if (!isDisabled) {
+ onClick(event);
+ onSelect(event, value, isPlaceholder);
+ shouldResetOnSelect && onClose();
+ }
+ }}
+ role="option"
+ aria-selected={isSelected || null}
+ ref={this.ref}
+ onKeyDown={(event: React.KeyboardEvent) => {
+ this.onKeyDown(event, 0);
+ }}
+ type="button"
+ >
+ {description && (
+
+
+ {itemDisplay}
+ {isSelected && (
+
+
+
+ )}
+
+ {description}
+
+ )}
+ {!description && (
+
+ {itemDisplay}
+ {isSelected && (
+
+
+
+ )}
+
+ )}
+
+ {isFavorite !== null && id && favoriteButton(onFavorite)}
+ >
+ )}
+
+ );
+ } else if (variant === SelectVariant.checkbox && isLoad) {
+ return (
+
+ );
+ } else if (variant === SelectVariant.checkbox && isLoading) {
+ return (
+ {children}
+ );
+ } else if (variant === SelectVariant.checkbox && !isNoResultsOption && !isLoading && !isLoad) {
+ return (
+
+ );
+ } else if (variant === SelectVariant.checkbox && isNoResultsOption && !isLoading && !isLoad) {
+ return (
+
+ {
+ this.onKeyDown(event, 0, undefined, true);
+ }}
+ type="button"
+ >
+ {itemDisplay}
+
+
+ );
+ }
+ };
+
+ return (
+
+ {({ onSelect, onClose, variant, inputIdPrefix, onFavorite, shouldResetOnSelect }) => (
+
+ {renderOption(onSelect, onClose, variant, inputIdPrefix, onFavorite, shouldResetOnSelect)}
+
+ )}
+
+ );
+ }
+}
diff --git a/packages/react-core/src/components/Select/SelectToggle.tsx b/packages/react-core/src/deprecated/components/Select/SelectToggle.tsx
similarity index 98%
rename from packages/react-core/src/components/Select/SelectToggle.tsx
rename to packages/react-core/src/deprecated/components/Select/SelectToggle.tsx
index c0b04f98265..2fc07511e15 100644
--- a/packages/react-core/src/components/Select/SelectToggle.tsx
+++ b/packages/react-core/src/deprecated/components/Select/SelectToggle.tsx
@@ -4,9 +4,9 @@ import buttonStyles from '@patternfly/react-styles/css/components/Button/button'
import { css } from '@patternfly/react-styles';
import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
import { SelectVariant, SelectFooterTabbableItems } from './selectConstants';
-import { PickOptional } from '../../helpers/typeUtils';
-import { findTabbableElements } from '../../helpers/util';
-import { KeyTypes } from '../../helpers/constants';
+import { PickOptional } from '../../../helpers/typeUtils';
+import { findTabbableElements } from '../../../helpers/util';
+import { KeyTypes } from '../../../helpers/constants';
export interface SelectToggleProps extends Omit, 'ref'> {
/** HTML ID of dropdown toggle */
diff --git a/packages/react-core/src/components/Select/__tests__/Select.test.tsx b/packages/react-core/src/deprecated/components/Select/__tests__/Select.test.tsx
similarity index 99%
rename from packages/react-core/src/components/Select/__tests__/Select.test.tsx
rename to packages/react-core/src/deprecated/components/Select/__tests__/Select.test.tsx
index b8d1d991176..d7cfbf7278b 100644
--- a/packages/react-core/src/components/Select/__tests__/Select.test.tsx
+++ b/packages/react-core/src/deprecated/components/Select/__tests__/Select.test.tsx
@@ -7,7 +7,7 @@ import { Select } from '../Select';
import { SelectOption, SelectOptionObject } from '../SelectOption';
import { SelectGroup } from '../SelectGroup';
import { SelectVariant, SelectDirection } from '../selectConstants';
-import { KeyTypes } from '../../../helpers';
+import { KeyTypes } from '../../../../helpers';
class User implements SelectOptionObject {
private firstName: string;
diff --git a/packages/react-core/src/components/Select/__tests__/SelectGroup.test.tsx b/packages/react-core/src/deprecated/components/Select/__tests__/SelectGroup.test.tsx
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/SelectGroup.test.tsx
rename to packages/react-core/src/deprecated/components/Select/__tests__/SelectGroup.test.tsx
diff --git a/packages/react-core/src/components/Select/__tests__/SelectOption.test.tsx b/packages/react-core/src/deprecated/components/Select/__tests__/SelectOption.test.tsx
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/SelectOption.test.tsx
rename to packages/react-core/src/deprecated/components/Select/__tests__/SelectOption.test.tsx
diff --git a/packages/react-core/src/components/Select/__tests__/SelectToggle.test.tsx b/packages/react-core/src/deprecated/components/Select/__tests__/SelectToggle.test.tsx
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/SelectToggle.test.tsx
rename to packages/react-core/src/deprecated/components/Select/__tests__/SelectToggle.test.tsx
diff --git a/packages/react-core/src/components/Select/__tests__/__snapshots__/Select.test.tsx.snap b/packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/Select.test.tsx.snap
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/__snapshots__/Select.test.tsx.snap
rename to packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/Select.test.tsx.snap
diff --git a/packages/react-core/src/components/Select/__tests__/__snapshots__/SelectGroup.test.tsx.snap b/packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/SelectGroup.test.tsx.snap
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/__snapshots__/SelectGroup.test.tsx.snap
rename to packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/SelectGroup.test.tsx.snap
diff --git a/packages/react-core/src/components/Select/__tests__/__snapshots__/SelectOption.test.tsx.snap b/packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/SelectOption.test.tsx.snap
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/__snapshots__/SelectOption.test.tsx.snap
rename to packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/SelectOption.test.tsx.snap
diff --git a/packages/react-core/src/components/Select/__tests__/__snapshots__/SelectToggle.test.tsx.snap b/packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/SelectToggle.test.tsx.snap
similarity index 100%
rename from packages/react-core/src/components/Select/__tests__/__snapshots__/SelectToggle.test.tsx.snap
rename to packages/react-core/src/deprecated/components/Select/__tests__/__snapshots__/SelectToggle.test.tsx.snap
diff --git a/packages/react-core/src/deprecated/components/Select/examples/Select.md b/packages/react-core/src/deprecated/components/Select/examples/Select.md
new file mode 100644
index 00000000000..eac9f1d65d3
--- /dev/null
+++ b/packages/react-core/src/deprecated/components/Select/examples/Select.md
@@ -0,0 +1,2936 @@
+---
+id: Select
+section: components
+subsection: menus
+cssPrefix: pf-c-select
+propComponents: ['Select', 'SelectOption', 'SelectGroup', 'SelectOptionObject', 'SelectViewMoreObject']
+ouia: true
+---
+
+import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
+
+## Examples
+
+### Single select
+
+To let users select a single item from a list, use a single select list.
+
+A select list may use other properties for additional customization. Select each checkbox in the example below to visualize the following behavior:
+
+- To prevent a toggle click from opening a select list, use the `isDisabled` property.
+- To adjust the direction a select menu opens, use the `direction` property. The menu in the following example expands upwards. By default, select lists open upwards.
+- To add an icon to a select toggle, use the `toggleIcon` property.
+
+```js
+import React from 'react';
+import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
+import { Checkbox, Divider } from '@patternfly/react-core';
+import { Select, SelectOption, SelectVariant, SelectDirection } from '@patternfly/react-core/deprecated';
+
+class SingleSelectInput extends React.Component {
+ constructor(props) {
+ super(props);
+ this.options = [
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+
+ ];
+
+ this.toggleRef = React.createRef();
+
+ this.state = {
+ isToggleIcon: false,
+ isOpen: false,
+ selected: null,
+ isDisabled: false,
+ direction: SelectDirection.down
+ };
+
+ this.onToggle = (_event, isOpen) => {
+ this.setState({
+ isOpen
+ });
+ };
+
+ this.onSelect = (event, selection, isPlaceholder) => {
+ if (isPlaceholder) this.clearSelection();
+ else {
+ this.setState({
+ selected: selection,
+ isOpen: false
+ });
+ console.log('selected:', selection);
+ this.toggleRef.current.focus();
+ }
+ };
+
+ this.clearSelection = () => {
+ this.setState({
+ selected: null,
+ isOpen: false
+ });
+ };
+
+ this.toggleDisabled = checked => {
+ this.setState({
+ isDisabled: checked
+ });
+ };
+
+ this.setIcon = checked => {
+ this.setState({
+ isToggleIcon: checked
+ });
+ };
+
+ this.toggleDirection = () => {
+ if (this.state.direction === SelectDirection.up) {
+ this.setState({
+ direction: SelectDirection.down
+ });
+ } else {
+ this.setState({
+ direction: SelectDirection.up
+ });
+ }
+ };
+ }
+
+ render() {
+ const { isOpen, selected, isDisabled, direction, isToggleIcon } = this.state;
+ const titleId = 'title-id-1';
+ return (
+
+
+ Title
+
+ }
+ variant={SelectVariant.single}
+ aria-label="Select Input"
+ onToggle={this.onToggle}
+ onSelect={this.onSelect}
+ selections={selected}
+ isOpen={isOpen}
+ aria-labelledby={titleId}
+ isDisabled={isDisabled}
+ direction={direction}
+ >
+ {this.options}
+
+ this.toggleDisabled(checked)}
+ aria-label="disabled checkbox"
+ id="toggle-disabled"
+ name="toggle-disabled"
+ />
+
+ this.setIcon(checked)}
+ aria-label="show icon checkbox"
+ id="toggle-icon"
+ name="toggle-icon"
+ />
+
+ );
+ }
+}
+```
+
+### With item descriptions
+
+To give more context to a `` in a list, use the `description` property.
+
+```js
+import React from 'react';
+import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
+
+class SingleSelectDescription extends React.Component {
+ constructor(props) {
+ super(props);
+ this.options = [
+ { value: 'Mr', disabled: false },
+ { value: 'Miss', disabled: false },
+ { value: 'Mrs', disabled: false },
+ { value: 'Ms', disabled: false },
+ { value: 'Dr', disabled: false },
+ { value: 'Other', disabled: false }
+ ];
+
+ this.toggleRef = React.createRef();
+
+ this.state = {
+ isOpen: false,
+ selected: null,
+ isDisabled: false
+ };
+
+ this.onToggle = (_event, isOpen) => {
+ this.setState({
+ isOpen
+ });
+ };
+
+ this.onSelect = (event, selection, isPlaceholder) => {
+ if (isPlaceholder) this.clearSelection();
+ else {
+ this.setState({
+ selected: selection,
+ isOpen: false
+ });
+ console.log('selected:', selection);
+ this.toggleRef.current.focus();
+ }
+ };
+
+ this.clearSelection = () => {
+ this.setState({
+ selected: null,
+ isOpen: false
+ });
+ };
+ }
+
+ render() {
+ const { isOpen, selected, isDisabled, direction, isToggleIcon } = this.state;
+ const titleId = 'select-descriptions-title';
+ return (
+
+
+ Title
+
+
+
+ );
+ }
+}
+```
+
+### With grouped items
+
+To group related select options together, use 1 or more `` components and title each group using the `label` property.
+
+```js
+import React from 'react';
+import { Divider } from '@patternfly/react-core';
+import { Select, SelectOption, SelectVariant, SelectGroup } from '@patternfly/react-core/deprecated';
+
+class GroupedSingleSelectInput extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpen: false,
+ selected: null
+ };
+
+ this.toggleRef = React.createRef();
+
+ this.onToggle = (_event, isOpen) => {
+ this.setState({
+ isOpen
+ });
+ };
+
+ this.onSelect = (event, selection) => {
+ this.setState({
+ selected: selection,
+ isOpen: false
+ });
+ this.toggleRef.current.focus();
+ };
+
+ this.clearSelection = () => {
+ this.setState({
+ selected: null
+ });
+ };
+
+ this.options = [
+
+
+
+
+
+
+ ,
+ ,
+
+
+
+
+
+ ];
+ }
+
+ render() {
+ const { isOpen, selected } = this.state;
+ const titleId = 'grouped-single-select-id';
+ return (
+
+
+ Grouped Checkbox Title
+
+
+
+ );
+ }
+}
+```
+
+### Favoriting items
+
+To allow users to favorite items in a select list, use the `onFavorite` callback. When users click the favorite button, the item is duplicated and placed in a separated group at the top of the menu. To change the name of the group use the `favoritesLabel` property.
+
+```js
+import React from 'react';
+import { Select, SelectOption, SelectVariant, SelectGroup } from '@patternfly/react-core/deprecated';
+
+class FavoritesSelect extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isOpen: false,
+ selected: null,
+ favorites: []
+ };
+
+ this.onToggle = (_event, isOpen) => {
+ this.setState({
+ isOpen
+ });
+ };
+
+ this.onSelect = (event, selection, isPlaceholder) => {
+ if (isPlaceholder) this.clearSelection();
+ else {
+ this.setState({
+ selected: selection,
+ isOpen: false
+ });
+ console.log('selected:', selection);
+ }
+ };
+
+ this.clearSelection = () => {
+ this.setState({
+ selected: null,
+ isOpen: false
+ });
+ };
+
+ this.onFavorite = (itemId, isFavorite) => {
+ if (isFavorite) {
+ this.setState({
+ favorites: this.state.favorites.filter(id => id !== itemId)
+ });
+ } else
+ this.setState({
+ favorites: [...this.state.favorites, itemId]
+ });
+ };
+
+ this.options = [
+
+
+
+
+
+
+ ,
+
+
+
+
+
+ ];
+ }
+
+ render() {
+ const { isOpen, selected, favorites } = this.state;
+ const titleId = 'grouped-single-select-id';
+ return (
+
+ );
+ }
+}
+```
+
+### Validated selections
+
+To validate selections that users make, pass a validation state to the `validated` property. Validating selections can let users know if the selections they make would cause issues or errors.
+
+The example below passes an "error" state when you choose “select a title”, a "warning" state when you choose "other", and a "success" state for any other item selected from the menu.
+
+```js
+import React from 'react';
+import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
+
+class ValidatedSelect extends React.Component {
+ constructor(props) {
+ super(props);
+ this.options = [
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+
+ ];
+
+ this.toggleRef = React.createRef();
+
+ this.state = {
+ isOpen: false,
+ selected: null,
+ isDisabled: false,
+ validated: 'default'
+ };
+
+ this.onToggle = (_event, isOpen) => {
+ this.setState({
+ isOpen
+ });
+ };
+
+ this.onSelect = (event, selection, isPlaceholder) => {
+ let validatedState = 'success';
+ if (isPlaceholder) {
+ this.clearSelection();
+ validatedState = 'error';
+ } else {
+ if (selection === 'Other') {
+ validatedState = 'warning';
+ } else {
+ validatedState = 'success';
+ }
+ this.setState({
+ selected: selection,
+ isOpen: false
+ });
+ console.log('selected:', selection);
+ }
+ this.setState({
+ validated: validatedState
+ });
+ this.toggleRef.current.focus();
+ };
+
+ this.clearSelection = () => {
+ this.setState({
+ selected: null,
+ isOpen: false
+ });
+ };
+ }
+
+ render() {
+ const { isOpen, selected, isDisabled, direction, isToggleIcon, validated } = this.state;
+ const titleId = 'select-validated-title';
+ return (
+
+
+ Title
+
+
+
+ {validated}
+
+
+ );
+ }
+}
+```
+
+### Styled placeholder text
+
+To add a toggle label to a select, use the `placeholderText` property. The following example displays "Filter by status" in the toggle before a selection is made.
+
+To fade the color of `placeholderText` to gray, use the `hasPlaceholderStyle` property.
+
+```js
+import React from 'react';
+import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
+
+function SelectWithPlaceholderStyle() {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState([]);
+
+ const options = [
+ ,
+ ,
+
+ ];
+
+ const onToggle = (_event, isOpen) => setIsOpen(isOpen);
+
+ const onSelect = (event, selection, isPlaceholder) => {
+ setSelected(selection);
+ setIsOpen(false);
+ };
+
+ const clearSelection = () => {
+ setSelected(null);
+ setIsOpen(false);
+ };
+
+ const titleId = 'placeholder-style-select-id';
+
+ return (
+
+
+ Placeholder styles
+
+
+
+ );
+}
+```
+
+### Placeholder select options
+
+To set a `` as a placeholder, use the `isPlaceholder` property. The following example sets the "Filter by status" as a placeholder so that it is pre-selected.
+
+```js
+
+import React from 'react';
+import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
+
+function SelectWithPlaceholderStyle() {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState([]);
+
+ const options = [
+ ,
+ ,
+ ,
+
+ ];
+
+ const onToggle = (_event, isOpen) => setIsOpen(isOpen);
+
+ const onSelect = (event, selection, isPlaceholder) => {
+ setSelected(selection);
+ setIsOpen(false);
+ };
+
+ const clearSelection = () => {
+ setSelected(null);
+ setIsOpen(false);
+ };
+
+ const titleId = 'placeholder-style-select-option-id';
+
+ return (
+
+
+ Placeholder styles - select option
+
+
+
+ );
+}
+```
+
+### With a footer
+
+You can add a `footer` to a `
+
+
+
Ascending
-
- setActiveSortDirection('desc')}
+
+
Descending
-
-
- ]}
- isOpen={isSortDropdownOpen}
- toggle={
- setIsSortDropdownOpen(!isSortDropdownOpen)}
- toggleTemplate={}
- />
- }
- isPlain
- isGrouped
- />
- */}
+
+
+
+
+
diff --git a/packages/react-table/src/deprecated/components/Table/examples/LegacyTableSortableCustom.tsx b/packages/react-table/src/deprecated/components/Table/examples/LegacyTableSortableCustom.tsx
index 85dcb7fb80d..dcfe444f450 100644
--- a/packages/react-table/src/deprecated/components/Table/examples/LegacyTableSortableCustom.tsx
+++ b/packages/react-table/src/deprecated/components/Table/examples/LegacyTableSortableCustom.tsx
@@ -4,7 +4,18 @@ import { Table, TableHeader, TableBody, TableProps } from '@patternfly/react-tab
import {
Toolbar,
ToolbarContent,
+ ToolbarItem,
+ MenuToggle,
+ MenuToggleElement
} from '@patternfly/react-core';
+import {
+ Select as NewSelect,
+ SelectGroup as NewSelectGroup,
+ SelectList as NewSelectList,
+ SelectOption as NewSelectOption
+} from '@patternfly/react-core/dist/esm/components/Select';
+
+import SortAmountDownIcon from '@patternfly/react-icons/dist/esm/icons/sort-amount-down-icon';
interface Repository {
name: string;
@@ -22,13 +33,15 @@ export const LegacyTableSortableCustom: React.FunctionComponent = () => {
{ name: 'p', branches: 'two', prs: 'b', workspaces: 'four', lastCommit: 'five' }
];
- // const columnNames = {
- // name: 'Repositories',
- // branches: 'Branches',
- // prs: 'Pull requests',
- // workspaces: 'Workspaces',
- // lastCommit: 'Last commit'
- // };
+ const columnNames = {
+ name: 'Repositories',
+ branches: 'Branches',
+ prs: 'Pull requests',
+ workspaces: 'Workspaces',
+ lastCommit: 'Last commit'
+ };
+
+ const [isSortDropdownOpen, setIsSortDropdownOpen] = React.useState(false);
// Index of the currently sorted column
// Note: if you intend to make columns reorderable, you may instead want to use a non-numeric key
@@ -110,56 +123,63 @@ export const LegacyTableSortableCustom: React.FunctionComponent = () => {
- {/* TODO: replace with select after #8073
- */}
+
+
+
+
+
(
-
))}
- onToggle={isOpen => {
+ onToggle={(event, isOpen) => {
this.onToggle(isOpen, rowIndex, cellIndex);
}}
selections={props.selected}
@@ -486,7 +494,7 @@ class EditableRowsTable extends React.Component {
isOpen={props.isSelectOpen}
options={props.options.map((option, index) => {
return (
-
);
})}
- onToggle={isOpen => {
+ onToggle={(event, isOpen) => {
this.onToggle(isOpen, rowIndex, cellIndex);
}}
selections={props.selected}
@@ -606,14 +614,14 @@ class EditableRowsTable extends React.Component {
clearSelection={this.clearSelection}
isOpen={props.isSelectOpen}
options={props.options.map((option, index) => (
-
))}
- onToggle={isOpen => {
+ onToggle={(event, isOpen) => {
this.onToggle(isOpen, rowIndex, cellIndex);
}}
selections={props.selected}
@@ -684,6 +692,7 @@ class EditableRowsTable extends React.Component {
}
let newSelected = Array.from(newCellProps.selected);
+ let newSelectOpen = false;
switch (newCellProps.editableSelectProps.variant) {
case 'typeaheadmulti':
@@ -693,6 +702,7 @@ class EditableRowsTable extends React.Component {
} else {
newSelected = newSelected.filter(el => el !== newValue);
}
+ newSelectOpen = true;
break;
}
default: {
@@ -702,6 +712,7 @@ class EditableRowsTable extends React.Component {
newCellProps.editableValue = newSelected;
newCellProps.selected = newSelected;
+ newCellProps.isSelectOpen = newSelectOpen;
}
this.setState({
@@ -720,6 +731,7 @@ class EditableRowsTable extends React.Component {
};
this.onToggle = (isOpen, rowIndex, cellIndex) => {
+ console.log("isOpen", isOpen);
let newRows = Array.from(this.state.rows);
newRows[rowIndex].cells[cellIndex].props.isSelectOpen = isOpen;
this.setState({
diff --git a/packages/react-table/src/docs/demos/Table.md b/packages/react-table/src/docs/demos/Table.md
index f82cd7b20e3..b345fa8ce20 100644
--- a/packages/react-table/src/docs/demos/Table.md
+++ b/packages/react-table/src/docs/demos/Table.md
@@ -362,8 +362,7 @@ import {
PaginationVariant,
Text,
TextContent,
- Select,
- SelectVariant
+ MenuToggle
} from '@patternfly/react-core';
import { Table as TableDeprecated, TableHeader, TableBody } from '@patternfly/react-table/deprecated';
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
@@ -1003,33 +1002,11 @@ class ColumnManagementAction extends React.Component {
- {}}
- aria-label="Select Input"
- aria-labelledby="page-layout-table-draggable-column-management-action-toolbar-top-select-checkbox-label page-layout-table-column-management-action-toolbar-top-select-checkbox-toggle"
- placeholderText={
- <>
- Name
- >
- }
- />
+ Name
+
+
+
- {/* TODO: replace with select after #8073
- }
- aria-label="Sort by"
- hideCaret
- />
- }
- />
- */}
@@ -1116,7 +1093,6 @@ import {
Title,
Select,
SelectOption,
- SelectVariant,
SearchInput,
EmptyState,
EmptyStateActions,
@@ -1184,22 +1160,22 @@ class FilterTableDemo extends React.Component {
}
};
- this.onCategoryToggle = (isOpen) => {
+ this.onCategoryToggle = () => {
this.setState({
- isCategoryDropdownOpen: isOpen
+ isCategoryDropdownOpen: !this.state.isCategoryDropdownOpen
});
};
this.onCategorySelect = (event) => {
this.setState({
currentCategory: event.target.innerText,
- isCategoryDropdownOpen: !this.state.isCategoryDropdownOpen
+ isCategoryDropdownOpen: false
});
};
- this.onFilterToggle = (isOpen) => {
+ this.onFilterToggle = () => {
this.setState({
- isFilterDropdownOpen: isOpen
+ isFilterDropdownOpen: !this.state.isFilterDropdownOpen
});
};
@@ -1237,7 +1213,8 @@ class FilterTableDemo extends React.Component {
filters: {
...prevState.filters,
status: checked ? [...prevSelections, selection] : prevSelections.filter((value) => value !== selection)
- }
+ },
+ isFilterDropdownOpen: false
};
});
};
@@ -1266,7 +1243,8 @@ class FilterTableDemo extends React.Component {
filters: {
...prevState.filters,
['location']: [selection]
- }
+ },
+ isFilterDropdownOpen: false
};
});
this.onFilterSelect();
@@ -1277,21 +1255,33 @@ class FilterTableDemo extends React.Component {
const { isCategoryDropdownOpen, currentCategory } = this.state;
const categoryMenuItems = [
- ,
- ,
-
+ Location,
+ Name,
+ Status
];
return (
) => (
+ }
+ style={
+ {
+ width: '100%',
+ verticalAlign: 'text-bottom'
+ } as React.CSSProperties
+ }
+ >
+ {currentCategory}
+
+ )}
isOpen={isCategoryDropdownOpen}
- toggleIcon={}
- style={{ width: '100%' }}
>
{categoryMenuItems}
@@ -1303,19 +1293,54 @@ class FilterTableDemo extends React.Component {
const { currentCategory, isFilterDropdownOpen, inputValue, filters } = this.state;
const locationMenuItems = [
- ,
- ,
- ,
- ,
-
+ Raleigh,
+ Westford,
+ Boston,
+ Brno,
+ Bangalore
];
const statusMenuItems = [
- ,
- ,
- ,
- ,
-
+
+ Running
+ ,
+
+ Stopped
+ ,
+
+ Down
+ ,
+
+ Degraded
+ ,
+
+ Needs Maintainence
+
];
return (
@@ -1328,11 +1353,25 @@ class FilterTableDemo extends React.Component {
>
) => (
+
+ {filters.location[0] || `Any`}
+
+ )}
>
{locationMenuItems}
@@ -1361,13 +1400,28 @@ class FilterTableDemo extends React.Component {
showToolbarItem={currentCategory === 'Status'}
>
) => (
+
+ Filter by status
+ {filters.status.length > 0 && {filters.status.length}}
+
+ )}
>
{statusMenuItems}
@@ -1382,7 +1436,13 @@ class FilterTableDemo extends React.Component {
} breakpoint="xl">
-
+
{this.buildCategoryDropdown()}
{this.buildFilterDropdown()}
diff --git a/packages/react-table/src/docs/demos/table-demos/ColumnManagement.jsx b/packages/react-table/src/docs/demos/table-demos/ColumnManagement.jsx
index fbf676c0903..8f7821b215b 100644
--- a/packages/react-table/src/docs/demos/table-demos/ColumnManagement.jsx
+++ b/packages/react-table/src/docs/demos/table-demos/ColumnManagement.jsx
@@ -11,6 +11,7 @@ import {
Toolbar,
ToolbarContent,
ToolbarItem,
+ MenuToggle,
Modal,
OverflowMenu,
OverflowMenuGroup,
@@ -20,9 +21,7 @@ import {
Pagination,
PaginationVariant,
Text,
- TextContent,
- Select,
- SelectVariant
+ TextContent
} from '@patternfly/react-core';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon';
@@ -390,32 +389,11 @@ export const ColumnManagementAction = () => {
-
- Name
- >
- }
- />
+ Name
+
+
+
- {/* TODO: replace with select after #8073
- }
- aria-label="Sort by"
- hideCaret
- />
- }
- />
- */}
diff --git a/packages/react-table/src/docs/demos/table-demos/Compact.jsx b/packages/react-table/src/docs/demos/table-demos/Compact.jsx
index 5e9217ff5b9..f86f97ef685 100644
--- a/packages/react-table/src/docs/demos/table-demos/Compact.jsx
+++ b/packages/react-table/src/docs/demos/table-demos/Compact.jsx
@@ -2,15 +2,16 @@ import React from 'react';
import {
Button,
Card,
+ MenuToggle,
+ MenuToggleElement,
+ Pagination,
+ PageSection,
+ Select,
+ SelectOptionm,
Toolbar,
ToolbarContent,
ToolbarGroup,
- ToolbarItem,
- Pagination,
- Select,
- SelectVariant,
- SelectOption,
- PageSection
+ ToolbarItem
} from '@patternfly/react-core';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
@@ -54,22 +55,25 @@ export const CompactTable = () => {
+ toggle={(toggleRef: React.Ref) => (
+ setIsSelectOpen(!isSelectOpen)}
+ isExpanded={isSelectOpen}
+ >
Status
- >
- }
+
+ )}
isOpen={isSelectOpen}
- onToggle={() => setIsSelectOpen(!isSelectOpen)}
+ onOpenChange={isOpen => setIsSelectOpen(isOpen)}
onSelect={() => setIsSelectOpen(!isSelectOpen)}
>
{[
- ,
- ,
- ,
-
+ Debug,
+ Info,
+ Warn,
+ Error
]}
diff --git a/packages/react-table/src/docs/demos/table-demos/CompoundExpansion.jsx b/packages/react-table/src/docs/demos/table-demos/CompoundExpansion.jsx
index f6b06b8b794..1c7352506fc 100644
--- a/packages/react-table/src/docs/demos/table-demos/CompoundExpansion.jsx
+++ b/packages/react-table/src/docs/demos/table-demos/CompoundExpansion.jsx
@@ -14,15 +14,16 @@ import {
Card,
Flex,
FlexItem,
+ MenuToggle,
+ MenuToggleElement,
Toolbar,
ToolbarContent,
ToolbarGroup,
ToolbarItem,
Pagination,
+ PageSection,
Select,
- SelectVariant,
- SelectOption,
- PageSection
+ SelectOption
} from '@patternfly/react-core';
import CodeBranchIcon from '@patternfly/react-icons/dist/esm/icons/code-branch-icon';
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
@@ -95,22 +96,25 @@ export const CompoundExpandable = () => {
+ toggle={(toggleRef: React.Ref) => (
+ setIsSelectOpen(!isSelectOpen)}
+ isExpanded={isSelectOpen}
+ >
Status
- >
- }
+
+ )}
isOpen={isSelectOpen}
- onToggle={() => setIsSelectOpen(!isSelectOpen)}
+ onOpenChange={isOpen => setIsSelectOpen(isOpen)}
onSelect={() => setIsSelectOpen(!isSelectOpen)}
>
{[
- ,
- ,
- ,
-
+ Debug,
+ Info,
+ Warn,
+ Error
]}
diff --git a/packages/react-table/src/docs/demos/table-demos/SortableResponsive.jsx b/packages/react-table/src/docs/demos/table-demos/SortableResponsive.jsx
index f6a4c5c368c..463b577ec45 100644
--- a/packages/react-table/src/docs/demos/table-demos/SortableResponsive.jsx
+++ b/packages/react-table/src/docs/demos/table-demos/SortableResponsive.jsx
@@ -7,20 +7,18 @@ import {
Flex,
FlexItem,
MenuToggle,
- Toolbar,
- ToolbarContent,
- ToolbarGroup,
- ToolbarItem,
+ MenuToggleElement,
+ PageSection,
Pagination,
+ SelectOption,
+ SelectList,
+ SelectGroup,
Text,
TextContent,
- OverflowMenu,
- OverflowMenuContent,
- OverflowMenuControl,
- OverflowMenuDropdownItem,
- OverflowMenuGroup,
- OverflowMenuItem,
- PageSection
+ Toolbar,
+ ToolbarContent,
+ ToolbarGroup,
+ ToolbarItem
} from '@patternfly/react-core';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { rows, columns } from '../../examples/Data.jsx';
@@ -29,6 +27,7 @@ import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon';
import SyncIcon from '@patternfly/react-icons/dist/esm/icons/sync-icon';
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
import CodeBranchIcon from '@patternfly/react-icons/dist/esm/icons/code-branch-icon';
+import SortAmountDownIcon from '@patternfly/react-icons/dist/esm/icons/sort-amount-down-icon';
import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon';
import DashboardWrapper from '@patternfly/react-core/src/demos/examples/DashboardWrapper';
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
@@ -125,89 +124,96 @@ export const ComposableTableSortable = () => {
const tableToolbar = (
- {/* TODO: replace with select after #8073
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setIsKebabDropdownOpen(!isKebabDropdownOpen)}
+ onOpenChange={(isKebabDropdownOpen) => setIsKebabDropdownOpen(isKebabDropdownOpen)}
+ toggle={(toggleRef) => (
+ setIsKebabDropdownOpen(!isKebabDropdownOpen)}
+ isExpanded={false}
+ >
+
+
+ )}
+ isOpen={isKebabDropdownOpen}
+ >
+ {kebabDropdownItems}
+
+
+
+
} />
diff --git a/packages/react-table/src/docs/demos/table-demos/StaticBottomPagination.jsx b/packages/react-table/src/docs/demos/table-demos/StaticBottomPagination.jsx
index 3dfa6d2fbfe..c6233429b9c 100644
--- a/packages/react-table/src/docs/demos/table-demos/StaticBottomPagination.jsx
+++ b/packages/react-table/src/docs/demos/table-demos/StaticBottomPagination.jsx
@@ -7,11 +7,12 @@ import {
ToolbarGroup,
ToolbarItem,
Pagination,
- Select,
- SelectVariant,
- SelectOption,
PageSection,
- Label
+ MenuToggle,
+ MenuToggleElement,
+ Label,
+ Select,
+ SelectOption
} from '@patternfly/react-core';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { rows, columns } from '../../examples/Data.jsx';
@@ -85,22 +86,25 @@ export const StaticBottomPagination = () => {
+ toggle={(toggleRef: React.Ref) => (
+ setIsSelectOpen(!isSelectOpen)}
+ isExpanded={isSelectOpen}
+ >
Status
- >
- }
+
+ )}
isOpen={isSelectOpen}
- onToggle={() => setIsSelectOpen(!isSelectOpen)}
+ onOpenChange={isOpen => setIsSelectOpen(isOpen)}
onSelect={() => setIsSelectOpen(!isSelectOpen)}
>
{[
- ,
- ,
- ,
-
+ Debug,
+ Info,
+ Warn,
+ Error
]}
From 9e62e40457703a7785e2bdaf5be21e42c7141a4a Mon Sep 17 00:00:00 2001
From: nicolethoen
Date: Tue, 11 Apr 2023 15:57:06 -0400
Subject: [PATCH 2/2] update ouiaId after rebase
---
.../src/deprecated/components/Select/examples/Select.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/react-core/src/deprecated/components/Select/examples/Select.md b/packages/react-core/src/deprecated/components/Select/examples/Select.md
index eac9f1d65d3..dfd1215b557 100644
--- a/packages/react-core/src/deprecated/components/Select/examples/Select.md
+++ b/packages/react-core/src/deprecated/components/Select/examples/Select.md
@@ -121,6 +121,7 @@ class SingleSelectInput extends React.Component {
aria-labelledby={titleId}
isDisabled={isDisabled}
direction={direction}
+ ouiaId="SingleSelect"
>
{this.options}