Skip to content

Commit

Permalink
feat(Select): add examples, add popper interface, fix focus
Browse files Browse the repository at this point in the history
  • Loading branch information
kmcfaul committed May 9, 2023
1 parent b0075af commit 4ef7084
Show file tree
Hide file tree
Showing 14 changed files with 1,116 additions and 32 deletions.
35 changes: 30 additions & 5 deletions packages/react-core/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ import { Menu, MenuContent, MenuProps } from '../Menu';
import { Popper, PopperProps } from '../../helpers/Popper/Popper';
import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers';

export interface SelectPopperProps extends PopperProps {
/** popper direction */
direction?: 'up' | 'down';
/** popper position */
position?: 'right' | 'left' | 'center';
/** Custom width of the popper. If the value is "trigger", it will set the width to the select toggle's width */
width?: string | 'trigger';
/** Minimum width of the popper. If the value is "trigger", it will set the min width to the select toggle's width */
minWidth?: string | 'trigger';
/** Maximum width of the popper. If the value is "trigger", it will set the max width to the select toggle's width */
maxWidth?: string | 'trigger';
/** Enable to flip the popper when it reaches the boundary */
enableFlip?: boolean;
}

/**
* See the Menu documentation for additional props that may be passed.
*/

export interface SelectProps extends MenuProps, OUIAProps {
/** Anything which can be rendered in a select */
children?: React.ReactNode;
Expand All @@ -16,7 +35,11 @@ export interface SelectProps extends MenuProps, OUIAProps {
/** Renderer for a custom select toggle. Forwards a ref to the toggle. */
toggle: (toggleRef: React.RefObject<any>) => React.ReactNode;
/** Function callback when user selects an option. */
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, itemId?: string | number) => void;
onSelect?: (
event?: React.MouseEvent<Element, MouseEvent>,
itemId?: string | number,
toggleRef?: React.RefObject<any>
) => void;
/** Callback to allow the select component to change the open state of the menu.
* Triggered by clicking outside of the menu, or by pressing either tab or escape. */
onOpenChange?: (isOpen: boolean) => void;
Expand All @@ -29,7 +52,7 @@ export interface SelectProps extends MenuProps, OUIAProps {
/** @beta Determines the accessible role of the select. For a checkbox select pass in "menu". */
role?: string;
/** Additional properties to pass to the popper */
popperProps?: Partial<PopperProps>;
popperProps?: SelectPopperProps;
}

const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
Expand All @@ -56,10 +79,12 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape if onOpenChange is provided
if (
(isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
toggleRef.current?.contains(event.target as Node)
isOpen &&
onOpenChange &&
(menuRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node))
) {
if (event.key === 'Escape' || event.key === 'Tab') {
event.preventDefault();
onOpenChange(false);
toggleRef.current?.focus();
}
Expand Down Expand Up @@ -97,7 +122,7 @@ const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({
role={role}
className={css(className)}
ref={menuRef}
onSelect={(event, itemId) => onSelect && onSelect(event, itemId)}
onSelect={(event, itemId) => onSelect && onSelect(event, itemId, toggleRef)}
isPlain={isPlain}
selected={selected}
{...getOUIAProps(
Expand Down
3 changes: 3 additions & 0 deletions packages/react-core/src/components/Select/SelectGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import React from 'react';
import { css } from '@patternfly/react-styles';
import { MenuGroupProps, MenuGroup } from '../Menu';

/**
* See the MenuGroup section of the Menu documentation for additional props that may be passed.
*/
export interface SelectGroupProps extends Omit<MenuGroupProps, 'ref'> {
/** Anything which can be rendered in a select group */
children: React.ReactNode;
Expand Down
24 changes: 22 additions & 2 deletions packages/react-core/src/components/Select/SelectOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import React from 'react';
import { css } from '@patternfly/react-styles';
import { MenuItemProps, MenuItem } from '../Menu';

/**
* See the MenuItem section of the Menu documentation for additional props that may be passed.
*/

export interface SelectOptionProps extends Omit<MenuItemProps, 'ref'> {
/** Anything which can be rendered in a select option */
children?: React.ReactNode;
/** Classes applied to root element of select option */
className?: string;
/** @hide Forwarded ref */
innerRef?: React.Ref<HTMLAnchorElement | HTMLButtonElement>;
/** Identifies the component in the Select onSelect callback */
itemId?: any;
/** Indicates the option has a checkbox */
Expand All @@ -17,15 +23,29 @@ export interface SelectOptionProps extends Omit<MenuItemProps, 'ref'> {
isSelected?: boolean;
/** Indicates the option is focused */
isFocused?: boolean;
/** Render an external link icon on focus or hover, and set the link's
* "target" attribute to a value of "_blank".
*/
isExternalLink?: boolean;
/** Render option with icon */
icon?: React.ReactNode;
/** Description of the option */
description?: React.ReactNode;
}

export const SelectOption: React.FunctionComponent<MenuItemProps> = ({
const SelectOptionBase: React.FunctionComponent<MenuItemProps> = ({
children,
className,
innerRef,
...props
}: SelectOptionProps) => (
<MenuItem className={css(className)} {...props}>
<MenuItem ref={innerRef} className={css(className)} {...props}>
{children}
</MenuItem>
);

export const SelectOption = React.forwardRef((props: SelectOptionProps, ref: React.Ref<any>) => (
<SelectOptionBase {...props} innerRef={ref} />
));

SelectOption.displayName = 'SelectOption';
40 changes: 38 additions & 2 deletions packages/react-core/src/components/Select/examples/Select.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,71 @@ id: Select
section: components
subsection: menus
cssPrefix: pf-c-select
propComponents: ['Select', 'SelectOption', 'SelectGroup', 'SelectList', 'MenuToggle']
propComponents: ['Select', 'SelectOption', 'SelectGroup', 'SelectList', 'MenuToggle', 'SelectPopperProps']
ouia: true
---

import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon';

## Examples

### Single

```ts file="./SelectBasic.tsx"

```

### Option variations

```ts file="./SelectOptionVariations.tsx"

```

### Grouped single

```ts file="./SelectGrouped.tsx"

```

### Checkbox

```ts file="./SelectCheckbox.tsx"

```

### Typeahead

```ts file="./SelectTypeahead.tsx"

```

### Typeahead with create option

```ts file="./SelectTypeaheadCreatable.tsx"

```

### Multiple Typeahead
### Multiple typeahead with chips

```ts file="./SelectMultiTypeahead.tsx"

```

### Multiple typeahead with create option

```ts file="./SelectMultiTypeaheadCreatable.tsx"

```

### Multiple typeahead with checkboxes

```ts file="./SelectMultiTypeaheadCheckbox.tsx"

```

### View more

```ts file="./SelectViewMore.tsx"

```
50 changes: 33 additions & 17 deletions packages/react-core/src/components/Select/examples/SelectBasic.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import React from 'react';
import { Select, SelectOption, SelectList, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
import { Select, SelectOption, SelectList, MenuToggle, MenuToggleElement, Checkbox } from '@patternfly/react-core';

export const SelectBasic: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
const [selected, setSelected] = React.useState<string>('Select a value');
const [isDisabled, setIsDisabled] = React.useState<boolean>(false);
const menuRef = React.useRef<HTMLDivElement>(null);

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

const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => {
const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
itemId: string | number | undefined,
toggleRef: React.RefObject<any>
) => {
// eslint-disable-next-line no-console
console.log('selected', itemId);

setSelected(itemId as string);
setIsOpen(false);
toggleRef.current?.focus();
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
isDisabled={isDisabled}
style={
{
width: '200px'
Expand All @@ -34,20 +41,29 @@ export const SelectBasic: React.FunctionComponent = () => {
);

return (
<Select
id="single-select"
ref={menuRef}
isOpen={isOpen}
selected={selected}
onSelect={onSelect}
onOpenChange={isOpen => setIsOpen(isOpen)}
toggle={toggle}
>
<SelectList>
<SelectOption itemId="Option 1">Option 1</SelectOption>
<SelectOption itemId="Option 2">Option 2</SelectOption>
<SelectOption itemId="Option 3">Option 3</SelectOption>
</SelectList>
</Select>
<React.Fragment>
<Checkbox
id="toggle-disabled"
label="isDisabled"
isChecked={isDisabled}
onChange={(_event, checked) => setIsDisabled(checked)}
style={{ marginBottom: 20 }}
/>
<Select
id="single-select"
ref={menuRef}
isOpen={isOpen}
selected={selected}
onSelect={onSelect}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
toggle={toggle}
>
<SelectList>
<SelectOption itemId="Option 1">Option 1</SelectOption>
<SelectOption itemId="Option 2">Option 2</SelectOption>
<SelectOption itemId="Option 3">Option 3</SelectOption>
</SelectList>
</Select>
</React.Fragment>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react';
import { Select, SelectOption, SelectList, SelectGroup, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectList,
SelectGroup,
MenuToggle,
MenuToggleElement,
Divider
} from '@patternfly/react-core';

export const SelectBasic: React.FunctionComponent = () => {
export const SelectGrouped: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
const [selected, setSelected] = React.useState<string>('Select a value');
const menuRef = React.useRef<HTMLDivElement>(null);
Expand All @@ -10,12 +18,17 @@ export const SelectBasic: React.FunctionComponent = () => {
setIsOpen(!isOpen);
};

const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => {
const onSelect = (
_event: React.MouseEvent<Element, MouseEvent> | undefined,
itemId: string | number | undefined,
toggleRef: React.RefObject<any>
) => {
// eslint-disable-next-line no-console
console.log('selected', itemId);

setSelected(itemId as string);
setIsOpen(false);
toggleRef?.current.focus();
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
Expand All @@ -35,12 +48,12 @@ export const SelectBasic: React.FunctionComponent = () => {

return (
<Select
id="single-select"
id="single-grouped-select"
ref={menuRef}
isOpen={isOpen}
selected={selected}
onSelect={onSelect}
onOpenChange={isOpen => setIsOpen(isOpen)}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
toggle={toggle}
>
<SelectGroup label="Group 1">
Expand All @@ -50,6 +63,7 @@ export const SelectBasic: React.FunctionComponent = () => {
<SelectOption itemId="Option 3">Option 3</SelectOption>
</SelectList>
</SelectGroup>
<Divider />
<SelectGroup label="Group 2">
<SelectList>
<SelectOption itemId="Option 4">Option 4</SelectOption>
Expand Down
Loading

0 comments on commit 4ef7084

Please sign in to comment.