Skip to content

Commit

Permalink
Merge pull request #961 from dlabrecq/select
Browse files Browse the repository at this point in the history
Replace deprecated PatternFly select component
  • Loading branch information
dlabrecq authored Jan 22, 2024
2 parents 829aae0 + 6cc226e commit 9d8cc3c
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 113 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@redhat-cloud-services/tsc-transform-imports": "^1.0.5",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
Expand Down
2 changes: 1 addition & 1 deletion src/routes/components/data-toolbar/DataToolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Workaround for https://github.com/patternfly/patternfly-react/issues/4477
// and https://github.com/patternfly/patternfly-react/issues/6371
.selectOverride {
&.pf-v5-c-select {
.pf-v5-c-menu-toggle {
min-width: 250px;
}
}
Expand Down
81 changes: 36 additions & 45 deletions src/routes/components/perspective/Perspective.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { MessageDescriptor } from '@formatjs/intl/src/types';
import { Title } from '@patternfly/react-core';
import type { SelectOptionObject } from '@patternfly/react-core/deprecated';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
import React, { useState } from 'react';
import React from 'react';
import { useIntl } from 'react-intl';
import type { SelectWrapperOption } from 'routes/components/selectWrapper';
import { SelectWrapper } from 'routes/components/selectWrapper';

import { styles } from './Perspective.styles';

Expand All @@ -14,20 +14,13 @@ export interface PerspectiveOption {
value: string;
}

interface PerspectiveOptionExt extends SelectOptionObject {
description?: string;
isDisabled?: boolean;
toString(): string; // label
value?: string;
}

interface PerspectiveOwnProps {
currentItem?: string;
id?: string;
isDisabled?: boolean;
label?: string;
minWidth?: number | string;
onSelected(value: string);
onSelect(event, selection: SelectWrapperOption);
options?: PerspectiveOption[];
}

Expand All @@ -39,14 +32,13 @@ const Perspective: React.FC<PerspectiveProps> = ({
isDisabled,
label,
minWidth,
onSelected,
onSelect,
options,
}) => {
const [isSelectOpen, setIsSelectOpen] = useState(false);
const intl = useIntl();

const getSelectOptions = (): PerspectiveOptionExt[] => {
const selectOptions: PerspectiveOptionExt[] = [];
const getSelectOptions = (): SelectWrapperOption[] => {
const selectOptions: SelectWrapperOption[] = [];

options.map(option => {
selectOptions.push({
Expand All @@ -61,39 +53,38 @@ const Perspective: React.FC<PerspectiveProps> = ({

const getSelect = () => {
const selectOptions = getSelectOptions();
const selection = selectOptions.find((option: PerspectiveOptionExt) => option.value === currentItem);
const selection = selectOptions.find(option => option.value === currentItem);

return (
<Select
id={id}
isDisabled={isDisabled}
isOpen={isSelectOpen}
onSelect={(_evt, value) => handleOnSelect(value)}
onToggle={(_evt, isExpanded) => handleOnToggle(isExpanded)}
selections={selection}
variant={SelectVariant.single}
>
{selectOptions.map((option, index) => (
<SelectOption
key={`${id}-${index}-${option.value}`}
description={option.description}
value={option}
isDisabled={option.isDisabled}
/>
))}
</Select>
<>
TEST
<SelectWrapper
id={id}
isDisabled={isDisabled}
onSelect={onSelect}
selection={selection}
selectOptions={selectOptions}
/>
</>
);
};

const handleOnSelect = (selection: SelectOptionObject) => {
if (onSelected) {
onSelected((selection as PerspectiveOption).value);
}
setIsSelectOpen(false);
};

const handleOnToggle = isOpen => {
setIsSelectOpen(isOpen);
// <Select
// id={id}
// isDisabled={isDisabled}
// isOpen={isSelectOpen}
// onSelect={(_evt, value) => handleOnSelect(value)}
// onToggle={(_evt, isExpanded) => handleOnToggle(isExpanded)}
// selections={selection}
// variant={SelectVariant.single}
// >
// {selectOptions.map((option, index) => (
// <SelectOption
// key={`${id}-${index}-${option.value}`}
// description={option.description}
// value={option}
// isDisabled={option.isDisabled}
// />
// ))}
// </Select>
};

return (
Expand Down
1 change: 1 addition & 0 deletions src/routes/components/selectWrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as SelectWrapper, SelectWrapperOption } from './selectWrapper';
8 changes: 8 additions & 0 deletions src/routes/components/selectWrapper/select.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import global_spacer_sm from '@patternfly/react-tokens/dist/js/global_spacer_sm';
import type React from 'react';

export const styles = {
badge: {
marginLeft: global_spacer_sm.var,
},
} as { [className: string]: React.CSSProperties };
11 changes: 11 additions & 0 deletions src/routes/components/selectWrapper/selectWrapper.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import url("~@patternfly/patternfly/base/patternfly-variables.css");

// Workaround for missing "position" property
.selectWrapper {
.pf-v5-c-menu-toggle {
max-width: unset;
}
.pf-v5-c-menu-toggle__text {
width: max-content;
}
}
31 changes: 31 additions & 0 deletions src/routes/components/selectWrapper/selectWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { SelectWrapper } from './index';

test('primary selector', async () => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const handleOnSelect = jest.fn();
const selectOptions = [
{ toString: () => 'CPU', value: 'cpu' },
{ toString: () => 'Memory', value: 'memory' },
{ toString: () => 'Storage', value: 'storage' },
];
render(
<SelectWrapper
onSelect={(_evt, select) => handleOnSelect(select.value)}
placeholder={'Resources'}
selection={selectOptions[0]}
selectOptions={selectOptions}
/>
);
expect(screen.queryAllByText('CPU').length).toBe(1);
expect(screen.queryAllByText('Memory').length).toBe(0);
const button = screen.getByRole('button');
await user.click(button);
const options = screen.getAllByRole('option');
expect(options.length).toBe(3);
await user.click(options[1]);
expect(handleOnSelect.mock.calls).toEqual([['memory']]);
});
108 changes: 108 additions & 0 deletions src/routes/components/selectWrapper/selectWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import './selectWrapper.scss';

import { Icon, MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core';
import React, { useState } from 'react';

export interface SelectWrapperOption {
description?: string; // Option description
compareTo?: (option: SelectWrapperOption) => boolean;
isDisabled?: boolean;
toString?: () => string; // Option label
value?: string; // Option value
}

interface SelectWrapperOwnProps {
ariaLabel?: string;
className?: string;
id?: string;
isDisabled?: boolean;
onSelect?: (event, value: SelectWrapperOption) => void;
placeholder?: string;
position?: 'right' | 'left' | 'center' | 'start' | 'end';
selection?: string | SelectWrapperOption;
selectOptions?: SelectWrapperOption[];
toggleIcon?: React.ReactNode;
}

type SelectWrapperProps = SelectWrapperOwnProps;

const SelectWrapper: React.FC<SelectWrapperProps> = ({
ariaLabel,
className,
id,
isDisabled,
onSelect = () => {},
placeholder = null,
position,
selection,
selectOptions,
toggleIcon,
}) => {
const [isOpen, setIsOpen] = useState(false);

const getSelectOption = (option, index) => {
const isSelected = option.value === (typeof selection === 'string' ? selection : selection?.value);

return (
<SelectOption
description={option.description}
isDisabled={option.isDisabled}
isSelected={isSelected}
key={`${option.value}-${index}`}
value={option}
>
{option.toString()}
</SelectOption>
);
};

const getPlaceholder = () => {
const label = typeof selection === 'string' ? selection : selection?.toString();
return label ? label : placeholder;
};

const handleOnSelect = (evt, value) => {
onSelect(evt, value);
setIsOpen(false);
};

const handleOnToggle = () => {
setIsOpen(!isOpen);
};

const toggle = toggleRef => (
<MenuToggle
icon={toggleIcon && <Icon>{toggleIcon}</Icon>}
isDisabled={isDisabled}
isExpanded={isOpen}
onClick={handleOnToggle}
ref={toggleRef}
>
{getPlaceholder()}
</MenuToggle>
);

return (
<div className={className ? `selectWrapper ${className}` : 'selectWrapper'}>
<Select
id={id}
onOpenChange={isExpanded => setIsOpen(isExpanded)}
onSelect={handleOnSelect}
isOpen={isOpen}
popperProps={
position && {
position,
}
}
selected={selection}
toggle={toggle}
>
<SelectList aria-label={ariaLabel}>
{selectOptions.map((option, index) => getSelectOption(option, index))}
</SelectList>
</Select>
</div>
);
};

export default SelectWrapper;
Loading

0 comments on commit 9d8cc3c

Please sign in to comment.