-
Notifications
You must be signed in to change notification settings - Fork 357
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Select): add next version using menu components (#8115)
* next(Select): add select using menu components * add id * fix duplicate id * pr feedback * add ouia props * update ta data, update ta behavior to better match select * remove example, update structure on grouped
- Loading branch information
Showing
12 changed files
with
818 additions
and
1 deletion.
There are no files selected for viewing
129 changes: 129 additions & 0 deletions
129
packages/react-core/src/next/components/Select/Select.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import React from 'react'; | ||
import { css } from '@patternfly/react-styles'; | ||
import { Menu, MenuContent, MenuProps } from '../../../components/Menu'; | ||
import { Popper } from '../../../helpers/Popper/Popper'; | ||
import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../../helpers'; | ||
|
||
export interface SelectProps extends MenuProps, OUIAProps { | ||
/** Anything which can be rendered in a select */ | ||
children?: React.ReactNode; | ||
/** Classes applied to root element of select */ | ||
className?: string; | ||
/** Flag to indicate if select is open */ | ||
isOpen?: boolean; | ||
/** Single itemId for single select menus, or array of itemIds for multi select. You can also specify isSelected on the SelectOption. */ | ||
selected?: any | any[]; | ||
/** Renderer for a custom select toggle. Forwards a ref to the toggle. */ | ||
toggle: (toggleRef: React.RefObject<any>) => React.ReactNode; | ||
/** Function callback when user selects an option. */ | ||
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, itemId?: string | number) => void; | ||
/** Callback to allow the select component to change the open state of the menu. | ||
* Triggered by clicking outside of the menu, or by pressing either tab or escape. */ | ||
onOpenChange?: (isOpen: boolean) => void; | ||
/** Indicates if the select should be without the outer box-shadow */ | ||
isPlain?: boolean; | ||
/** Minimum width of the select menu */ | ||
minWidth?: string; | ||
/** @hide Forwarded ref */ | ||
innerRef?: React.Ref<HTMLDivElement>; | ||
} | ||
|
||
const SelectBase: React.FunctionComponent<SelectProps & OUIAProps> = ({ | ||
children, | ||
className, | ||
onSelect, | ||
isOpen, | ||
selected, | ||
toggle, | ||
onOpenChange, | ||
isPlain, | ||
minWidth, | ||
innerRef, | ||
...props | ||
}: SelectProps & OUIAProps) => { | ||
const localMenuRef = React.useRef<HTMLDivElement>(); | ||
const toggleRef = React.useRef<HTMLButtonElement>(); | ||
const containerRef = React.useRef<HTMLDivElement>(); | ||
|
||
const menuRef = (innerRef as React.RefObject<HTMLDivElement>) || localMenuRef; | ||
React.useEffect(() => { | ||
const handleMenuKeys = (event: KeyboardEvent) => { | ||
if (!isOpen && toggleRef.current?.contains(event.target as Node)) { | ||
// toggle was clicked open, focus on first menu item | ||
if (event.key === 'Enter' || event.key === 'Space') { | ||
setTimeout(() => { | ||
const firstElement = menuRef?.current?.querySelector('li button:not(:disabled),li input:not(:disabled)'); | ||
firstElement && (firstElement as HTMLElement).focus(); | ||
}, 0); | ||
} | ||
} | ||
// 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) | ||
) { | ||
if (event.key === 'Escape' || event.key === 'Tab') { | ||
onOpenChange(!isOpen); | ||
toggleRef.current?.focus(); | ||
} | ||
} | ||
}; | ||
|
||
const handleClickOutside = (event: MouseEvent) => { | ||
// If the event is not on the toggle and onOpenChange callback is provided, close the menu | ||
if (isOpen && onOpenChange && !toggleRef?.current?.contains(event.target as Node)) { | ||
if (isOpen && !menuRef.current?.contains(event.target as Node)) { | ||
onOpenChange(false); | ||
} | ||
} | ||
}; | ||
|
||
window.addEventListener('keydown', handleMenuKeys); | ||
window.addEventListener('click', handleClickOutside); | ||
|
||
return () => { | ||
window.removeEventListener('keydown', handleMenuKeys); | ||
window.removeEventListener('click', handleClickOutside); | ||
}; | ||
}, [isOpen, menuRef, onOpenChange]); | ||
|
||
const menu = ( | ||
<Menu | ||
className={css(className)} | ||
ref={menuRef} | ||
onSelect={(event, itemId) => onSelect(event, itemId)} | ||
isPlain={isPlain} | ||
selected={selected} | ||
{...(minWidth && { | ||
style: { | ||
'--pf-c-menu--MinWidth': minWidth | ||
} as React.CSSProperties | ||
})} | ||
{...getOUIAProps( | ||
Select.displayName, | ||
props.ouiaId !== undefined ? props.ouiaId : getDefaultOUIAId(Select.displayName), | ||
props.ouiaSafe !== undefined ? props.ouiaSafe : true | ||
)} | ||
{...props} | ||
> | ||
<MenuContent>{children}</MenuContent> | ||
</Menu> | ||
); | ||
return ( | ||
<div ref={containerRef}> | ||
<Popper | ||
trigger={toggle(toggleRef)} | ||
removeFindDomNode | ||
popper={menu} | ||
appendTo={containerRef.current || undefined} | ||
isVisible={isOpen} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export const Select = React.forwardRef((props: SelectProps, ref: React.Ref<any>) => ( | ||
<SelectBase innerRef={ref} {...props} /> | ||
)); | ||
|
||
Select.displayName = 'Select'; |
24 changes: 24 additions & 0 deletions
24
packages/react-core/src/next/components/Select/SelectGroup.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import React from 'react'; | ||
import { css } from '@patternfly/react-styles'; | ||
import { MenuGroupProps, MenuGroup } from '../../../components/Menu'; | ||
|
||
export interface SelectGroupProps extends Omit<MenuGroupProps, 'ref'> { | ||
/** Anything which can be rendered in a select group */ | ||
children: React.ReactNode; | ||
/** Classes applied to root element of select group */ | ||
className?: string; | ||
/** Label of the select group */ | ||
label?: string; | ||
} | ||
|
||
export const SelectGroup: React.FunctionComponent<SelectGroupProps> = ({ | ||
children, | ||
className, | ||
label, | ||
...props | ||
}: SelectGroupProps) => ( | ||
<MenuGroup className={css(className)} label={label} {...props}> | ||
{children} | ||
</MenuGroup> | ||
); | ||
SelectGroup.displayName = 'SelectGroup'; |
21 changes: 21 additions & 0 deletions
21
packages/react-core/src/next/components/Select/SelectList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import React from 'react'; | ||
import { css } from '@patternfly/react-styles'; | ||
import { MenuListProps, MenuList } from '../../../components/Menu'; | ||
|
||
export interface SelectListProps extends MenuListProps { | ||
/** Anything which can be rendered in a select list */ | ||
children: React.ReactNode; | ||
/** Classes applied to root element of select list */ | ||
className?: string; | ||
} | ||
|
||
export const SelectList: React.FunctionComponent<MenuListProps> = ({ | ||
children, | ||
className, | ||
...props | ||
}: SelectListProps) => ( | ||
<MenuList className={css(className)} {...props}> | ||
{children} | ||
</MenuList> | ||
); | ||
SelectList.displayName = 'SelectList'; |
31 changes: 31 additions & 0 deletions
31
packages/react-core/src/next/components/Select/SelectOption.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
import { css } from '@patternfly/react-styles'; | ||
import { MenuItemProps, MenuItem } from '../../../components/Menu'; | ||
|
||
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; | ||
/** Identifies the component in the Select onSelect callback */ | ||
itemId?: any; | ||
/** Indicates the option has a checkbox */ | ||
hasCheck?: boolean; | ||
/** Indicates the option is disabled */ | ||
isDisabled?: boolean; | ||
/** Indicates the option is selected */ | ||
isSelected?: boolean; | ||
/** Indicates the option is focused */ | ||
isFocused?: boolean; | ||
} | ||
|
||
export const SelectOption: React.FunctionComponent<MenuItemProps> = ({ | ||
children, | ||
className, | ||
...props | ||
}: SelectOptionProps) => ( | ||
<MenuItem className={css(className)} {...props}> | ||
{children} | ||
</MenuItem> | ||
); | ||
SelectOption.displayName = 'SelectOption'; |
37 changes: 37 additions & 0 deletions
37
packages/react-core/src/next/components/Select/examples/Select.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
id: Select | ||
section: components | ||
cssPrefix: pf-c-menu | ||
propComponents: ['Select', SelectGroup, 'SelectOption', 'SelectList'] | ||
beta: true | ||
ouia: true | ||
--- | ||
|
||
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; | ||
|
||
## Examples | ||
|
||
### Single | ||
|
||
```ts file="./SelectBasic.tsx" | ||
``` | ||
|
||
### Grouped single | ||
|
||
```ts file="./SelectGrouped.tsx" | ||
``` | ||
|
||
### Checkbox | ||
|
||
```ts file="./SelectCheckbox.tsx" | ||
``` | ||
|
||
### Typeahead | ||
|
||
```ts file="./SelectTypeahead.tsx" | ||
``` | ||
|
||
### Multiple Typeahead | ||
|
||
```ts file="./SelectMultiTypeahead.tsx" | ||
``` |
54 changes: 54 additions & 0 deletions
54
packages/react-core/src/next/components/Select/examples/SelectBasic.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import React from 'react'; | ||
import { Select, SelectOption, SelectList } from '@patternfly/react-core/next'; | ||
import { MenuToggle, MenuToggleElement } 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 menuRef = React.useRef<HTMLDivElement>(null); | ||
|
||
const onToggleClick = () => { | ||
setIsOpen(!isOpen); | ||
}; | ||
|
||
const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => { | ||
// eslint-disable-next-line no-console | ||
console.log('selected', itemId); | ||
|
||
setSelected(itemId as string); | ||
setIsOpen(false); | ||
}; | ||
|
||
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => ( | ||
<MenuToggle | ||
ref={toggleRef} | ||
onClick={onToggleClick} | ||
isExpanded={isOpen} | ||
style={ | ||
{ | ||
width: '200px' | ||
} as React.CSSProperties | ||
} | ||
> | ||
{selected} | ||
</MenuToggle> | ||
); | ||
|
||
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> | ||
); | ||
}; |
67 changes: 67 additions & 0 deletions
67
packages/react-core/src/next/components/Select/examples/SelectCheckbox.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import React from 'react'; | ||
import { Select, SelectOption, SelectList } from '@patternfly/react-core/next'; | ||
import { MenuToggle, MenuToggleElement, Badge } from '@patternfly/react-core'; | ||
|
||
export const SelectCheckbox: React.FunctionComponent = () => { | ||
const [isOpen, setIsOpen] = React.useState(false); | ||
const [selectedItems, setSelectedItems] = React.useState<number[]>([]); | ||
const menuRef = React.useRef<HTMLDivElement>(null); | ||
|
||
const onToggleClick = () => { | ||
setIsOpen(!isOpen); | ||
}; | ||
|
||
const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => { | ||
// eslint-disable-next-line no-console | ||
console.log('selected', itemId); | ||
|
||
if (selectedItems.includes(itemId as number)) { | ||
setSelectedItems(selectedItems.filter(id => id !== itemId)); | ||
} else { | ||
setSelectedItems([...selectedItems, itemId as number]); | ||
} | ||
}; | ||
|
||
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => ( | ||
<MenuToggle | ||
ref={toggleRef} | ||
onClick={onToggleClick} | ||
isExpanded={isOpen} | ||
style={ | ||
{ | ||
width: '200px' | ||
} as React.CSSProperties | ||
} | ||
> | ||
Filter by status | ||
{selectedItems.length > 0 && <Badge isRead>{selectedItems.length}</Badge>} | ||
</MenuToggle> | ||
); | ||
|
||
return ( | ||
<Select | ||
id="checkbox-select" | ||
ref={menuRef} | ||
isOpen={isOpen} | ||
selected={selectedItems} | ||
onSelect={onSelect} | ||
onOpenChange={(nextOpen: boolean) => setIsOpen(nextOpen)} | ||
toggle={toggle} | ||
> | ||
<SelectList> | ||
<SelectOption hasCheck itemId={0} isSelected={selectedItems.includes(0)}> | ||
Debug | ||
</SelectOption> | ||
<SelectOption hasCheck itemId={1} isSelected={selectedItems.includes(1)}> | ||
Info | ||
</SelectOption> | ||
<SelectOption hasCheck itemId={2} isSelected={selectedItems.includes(2)}> | ||
Warn | ||
</SelectOption> | ||
<SelectOption hasCheck isDisabled itemId={4} isSelected={selectedItems.includes(4)}> | ||
Error | ||
</SelectOption> | ||
</SelectList> | ||
</Select> | ||
); | ||
}; |
Oops, something went wrong.