-
Notifications
You must be signed in to change notification settings - Fork 616
Action Menu #1152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Action Menu #1152
Changes from all commits
Commits
Show all changes
125 commits
Select commit
Hold shift + click to select a range
2898c3c
chore: Merge branch 'overlay' into dropdownmenu
smockle ae7841e
chore: Merge branch 'main' into dropdownmenu
smockle f45dfdb
feat: Add DropdownMenu
smockle 98f948e
fix: Don’t hardcode a palette emoji in DropdownMenu; it’s typically n…
smockle bb8e06d
feat: Enable deselection; draw checks by selected items and empty spa…
smockle d9576c8
feat: Add DropdownButton; begin adding required roles and other attri…
smockle 7bce122
chore: Merge branch 'main' into dropdownmenu
smockle e3869a7
fix: Imported types, plus Storybook warnings, plus debug List-level c…
smockle b812e73
fix: Restore 'List'-level custom 'Item' rendering.
smockle 0cdaab2
chore: Merge branch 'main' into dropdownmenu
smockle 007ce3b
chore: Merge 'dropdown-with-zones' into 'dropdownmenu'
smockle 16751fb
fix: keep focus on anchor when dropdown menu opens
dgreif 0029c37
fix: Rename focus-related state and values to distinguish/dedupe from…
smockle 5edc447
chore: Run Prettier
smockle f7bd529
fix: Add 'onDismiss' to the dependency list for 'onAnchorKeyDown' to …
smockle fd369a3
fix: force render after overlay ref is set
dgreif 3ec6103
fix(dropdown): treat enter and space on item as an activation
dgreif ccecdd1
fix: only prevent default if item is activated
dgreif 4a545f9
docs: basic docs in dropdown menu
dgreif 6edab5a
docs: add `DropdownMenu` docs
dgreif 9b8e9fd
chore: add basic tests to DropdownMenu
VanAnderson e89cf1e
fix: add placeholder value to DropdownMenu
VanAnderson ff6e266
fix: fix linter by removing unused import
VanAnderson 275ef22
refactor: allow DropdownMenu state to be controlled by parent
dgreif a789c3f
docs: remove positioning form overlay docs
dgreif 9fd1e23
fix: typescript fixes
dgreif 8eb67b4
chore: delete isRefObject
VanAnderson 1904563
add selectedItem to docs
VanAnderson d3cc00d
remove random dash in docs
VanAnderson 718b99d
make DropdownMenu a controlled component in the docs
VanAnderson 6c0ea65
restore custom anchor for DropdownMenu in storybook story
VanAnderson ae943e4
fix: avoid signal monkey patch during gatsby server side render
dgreif 4fd91cf
docs: memoize dropdown menu items in example
dgreif 398a9d8
docs: useRenderForcingRef
dgreif d3d9b87
docs: fix renderItems typo
dgreif 55161b5
feat: Style 'ActionListSectionHeader'
smockle 4ff6835
basic structure for ActionMenu
VanAnderson f921cd5
progress
VanAnderson 52241d6
clean up ActionMenu and add displayNames
VanAnderson 3c9dec9
update ActionMenu Stories to reflect changing API
VanAnderson 8604e25
start to ActionMenu docs
VanAnderson 9f12d0a
tentative updates to ActionList
VanAnderson 9b682f1
ActionMenu tests
VanAnderson 44721f8
update ActionMenu docs
VanAnderson 73caa51
remove commented code
VanAnderson da8b54b
refigure ActionMenuProps interface
VanAnderson 786220a
implement styling fix, storybook fix
VanAnderson d3300c4
ActionMenu supports keyboard keys
VanAnderson b715b4e
Fix some TypeScript errors
VanAnderson d509b91
linter fixes
VanAnderson d298159
passing tests for ActionMenu
VanAnderson eca0669
type get-random-values module
VanAnderson 1c3f4e1
add tests for ActionMenu
VanAnderson 4bf14c2
ActionMenu buttonContent -> triggerContent
VanAnderson cb023bd
linter fixes
VanAnderson ba7fc30
remove cruft from ActionList rebase
VanAnderson cd72015
small spacing fixes
VanAnderson e73eb87
fix typescript error
VanAnderson cd008de
fix some typescript errors
VanAnderson 9276de4
use proper AriaRole prop
VanAnderson fdeada6
revise props on ActionList and remove package dependency
VanAnderson 8e414ad
update docs for ActionMenu
VanAnderson e6dcb7d
Add focus trap and focus zone support to dropdowns.
T-Hugs 4c5df63
Fix lint issues
dgreif 52404d4
Complete implementation of keyboard nav in DropdownMenu.
T-Hugs 54b55c9
remove focus first item behavior from useOpenAndCloseFocus
VanAnderson dd11702
restore implicity initial focus functionality for useOpenAndCloseFocu…
VanAnderson 2746286
add focusTrap and focusZone dependencies, correct anchorClick handler
VanAnderson 2a3c288
add onAnchorKeydown callback to anchor
VanAnderson d196ecb
remove some extra cruft from merges
VanAnderson a96a3e0
Remove d.ts checkbox from pr template (#1155)
dgreif dba11d2
Add as prop to ComponentProps
colebemis 5114a71
Move as prop definition
colebemis f92dfce
Update as prop type
colebemis 4ea9a1a
Update as prop type
colebemis 7532fc2
Update as prop type
colebemis f11d445
Check default portal still exists before using it (#1153)
dgreif 6db244a
Background styles for focused action list item
dgreif 45d1ee7
swap 'trailing' for 'auxilary' in ListItem
VanAnderson a3f11d3
trainlingText and trailingIcon are grouped together
VanAnderson 0006819
add groupId to ActionList/Item
VanAnderson d59d5d8
adjust styling on trailingText and trailingIcon
VanAnderson 5f18587
fix: rebase mishaps
VanAnderson c77ad1b
Revise some details with ActionMenu tests
VanAnderson 63b1ebb
item input is optional on Item
VanAnderson ea1e4f3
fix typo in ActionMenu docs
VanAnderson b45d0b0
fix typo in docs
VanAnderson 538c933
revisions to ActionMenu and associated tests
VanAnderson 1fa39d4
fix: Readable yet equivalent 'aria-selected'. Fix typo.
smockle 800a738
fix: Replace 'ChevronIcon' with 'TriangleDownIcon' per latest style g…
smockle 710123a
refactor: use ternary for conditional child component rendering
dgreif c64b970
test: update snapshot for DropdownMenu
dgreif 33863d8
fix: use custom theme provider
dgreif edf01d5
fix: export tweaks
dgreif b68acdf
onActivate -> onAction for ActionMenu
VanAnderson 716cd2c
triggerContent -> anchorContent for ActionMenu
VanAnderson 8493e6a
add renderAnchor to ActionMenu docs
VanAnderson 1e89b2a
Merge branch 'main' into dropdownmenu
dgreif a2d4df1
Change wording in docs/content/ActionMenu.mdx
VanAnderson 42e8658
fix: cleanup after merging master
dgreif 7e74ab1
refactor: `setSelectedItem` -> `onChange`
dgreif f8b0609
chore: add comment explaining lack of DropdownMenu type exports
dgreif 7440928
refactor: `includes` instead of `indexOf`
dgreif 77a2e38
docs: replace `setSelectedItem` with `onChange`
dgreif 848d2fd
refactor: default value for dependency arrays
dgreif 9735116
refactor: simplify event key checks
dgreif 04946b4
chore: remove display name for `DropdownButton`
dgreif 75f9ed5
refactor: optional chaining for item event handlers
dgreif 33c4374
refactor: `itemActivated` -> `handleSelection`
dgreif 8eabeee
refactor: remove `randomId` in favor of `uniqueId`
dgreif 807d6ae
Merge branch 'dropdownmenu' into VanAnderson/action-menu
VanAnderson 9978e13
implement uniqueId for ActionMenu
VanAnderson 9c8b666
don't extend ActionList from div
VanAnderson d6095b9
registerPortalRoot root is optional
VanAnderson 9a14317
Merge branch 'main' into VanAnderson/action-menu
VanAnderson 7cf5cc8
linter fix on Item
VanAnderson b90b79a
clean up Update src/ActionMenu.tsx click and keypress handlers
VanAnderson 62f6fc4
knock out right margin for trailingContainer
VanAnderson 4a3afe2
add ActionMenu groupMetadata example
VanAnderson b2bd62f
tweaks to Grouped ActionMenu examples.
VanAnderson 88ac566
useRenderForcingRef in ActionMenu
VanAnderson 7f6a811
Revert "registerPortalRoot root is optional"
VanAnderson 93cdab5
import useRenderForcingRef in ActionMenu
VanAnderson 38bbce1
remove registerPortalRoot(undefined)
VanAnderson 91da62d
remove registerPortalRoot from imports
VanAnderson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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,74 @@ | ||
--- | ||
title: ActionMenu | ||
--- | ||
|
||
An `ActionMenu` is an ActionList-based component for creating a menu of actions that expands through a trigger button. | ||
|
||
## Default example | ||
|
||
```jsx live | ||
<ActionMenu | ||
anchorContent="Menu" | ||
onAction={({text}) => console.log(text)} | ||
items={[ | ||
{text: 'New file'}, | ||
ActionMenu.Divider, | ||
{text: 'Copy link'}, | ||
{text: 'Edit file'}, | ||
{text: 'Delete file', variant: 'danger'} | ||
]} | ||
/> | ||
``` | ||
|
||
## Example with grouped items | ||
|
||
```jsx live | ||
<ActionMenu | ||
anchorContent="Menu" | ||
onAction={({text}) => console.log(text)} | ||
groupMetadata={[ | ||
{groupId: '0'}, | ||
{groupId: '1', header: {title: 'Live query', variant: 'subtle'}}, | ||
{groupId: '2', header: {title: 'Layout', variant: 'subtle'}}, | ||
{groupId: '3'}, | ||
{groupId: '4'} | ||
]} | ||
items={[ | ||
{leadingVisual: TypographyIcon, text: 'Rename', groupId: '0'}, | ||
{leadingVisual: VersionsIcon, text: 'Duplicate', groupId: '0'}, | ||
{leadingVisual: SearchIcon, text: 'repo:github/github', groupId: '1'}, | ||
{ | ||
leadingVisual: NoteIcon, | ||
text: 'Table', | ||
description: 'Information-dense table optimized for operations across teams', | ||
descriptionVariant: 'block', | ||
groupId: '2' | ||
}, | ||
{ | ||
leadingVisual: ProjectIcon, | ||
text: 'Board', | ||
description: 'Kanban-style board focused on visual states', | ||
descriptionVariant: 'block', | ||
groupId: '2' | ||
}, | ||
{ | ||
leadingVisual: FilterIcon, | ||
text: 'Save sort and filters to current view', | ||
groupId: '3' | ||
}, | ||
{leadingVisual: FilterIcon, text: 'Save sort and filters to new view', groupId: '3'}, | ||
{leadingVisual: GearIcon, text: 'View settings', groupId: '4'} | ||
]} | ||
/> | ||
``` | ||
|
||
## Component props | ||
|
||
| Name | Type | Default | Description | | ||
| :------------ | :------------------------------------ | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| items | `ItemProps[]` | `undefined` | Required. A list of item objects conforming to the `ActionList.Item` props interface. | | ||
| renderItem | `(props: ItemProps) => JSX.Element` | `ActionList.Item` | Optional. If defined, each item in `items` will be passed to this function, allowing for `ActionList`-wide custom item rendering. | | ||
| groupMetadata | `GroupProps[]` | `undefined` | Optional. If defined, `ActionList` will group `items` into `ActionList.Group`s separated by `ActionList.Divider` according to their `groupId` property. | | ||
| renderAnchor | `(props: ButtonProps) => JSX.Element` | `Button` | Optional. If defined, provided component will be used to render the menu anchor. Will receive the selected `Item` text as `children` prop when an item is activated. | | ||
| anchorContent | React.ReactNode | `undefined` | Optional. If defined, it will be passed to the trigger as the elements child. | | ||
| onAction | (props: ItemProps) => void | `undefined` | Optional. If defined, this function will be called when a menu item is activated either by a click or a keyboard press. | |
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,132 @@ | ||
import {List, ListPropsBase, GroupedListProps} from './ActionList/List' | ||
import {Item, ItemProps} from './ActionList/Item' | ||
import {Divider} from './ActionList/Divider' | ||
import Button, {ButtonProps} from './Button' | ||
import React, {useCallback, useRef, useState} from 'react' | ||
import Overlay from './Overlay' | ||
import {useFocusTrap} from './hooks/useFocusTrap' | ||
import {useFocusZone} from './hooks/useFocusZone' | ||
import {useAnchoredPosition} from './hooks/useAnchoredPosition' | ||
import {useRenderForcingRef} from './hooks/useRenderForcingRef' | ||
import {uniqueId} from './utils/uniqueId' | ||
|
||
export interface ActionMenuProps extends Partial<Omit<GroupedListProps, keyof ListPropsBase>>, ListPropsBase { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
renderAnchor?: (props: any) => JSX.Element | ||
anchorContent?: React.ReactNode | ||
onAction?: (props: ItemProps) => void | ||
} | ||
|
||
const ActionMenuItem = (props: ItemProps) => <Item role="menuitem" {...props} /> | ||
|
||
ActionMenuItem.displayName = 'ActionMenu.Item' | ||
|
||
const ActionMenuBase = ({ | ||
anchorContent, | ||
renderAnchor = <T extends ButtonProps>(props: T) => <Button {...props}>{anchorContent}</Button>, | ||
renderItem = Item, | ||
onAction, | ||
...listProps | ||
}: ActionMenuProps): JSX.Element => { | ||
const anchorRef = useRef<HTMLElement>(null) | ||
const [overlayRef, updateOverlayRef] = useRenderForcingRef<HTMLDivElement>() | ||
const anchorId = `dropdownMenuAnchor-${uniqueId()}` | ||
const [open, setOpen] = useState<boolean>(false) | ||
const [state, setState] = useState<'closed' | 'buttonFocus' | 'listFocus'>('closed') | ||
const onDismiss = useCallback(() => { | ||
setOpen(false) | ||
setState('closed') | ||
}, []) | ||
|
||
const onAnchorKeyDown = useCallback( | ||
(event: React.KeyboardEvent<HTMLElement>) => { | ||
if (!event.defaultPrevented) { | ||
if (state === 'closed') { | ||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { | ||
setState('listFocus') | ||
setOpen(true) | ||
event.preventDefault() | ||
} else if (event.key === ' ' || event.key === 'Enter') { | ||
setState('buttonFocus') | ||
setOpen(true) | ||
event.preventDefault() | ||
} | ||
} else if (state === 'buttonFocus') { | ||
if (['ArrowDown', 'ArrowUp', 'Tab', 'Enter'].indexOf(event.key) !== -1) { | ||
setState('listFocus') | ||
event.preventDefault() | ||
} else if (event.key === 'Escape') { | ||
setState('closed') | ||
onDismiss() | ||
event.preventDefault() | ||
} | ||
} | ||
} | ||
}, | ||
[state, onDismiss] | ||
) | ||
const onAnchorClick = useCallback( | ||
(event: React.MouseEvent<HTMLElement>) => { | ||
if (!event.defaultPrevented && event.button === 0 && !open) { | ||
setOpen(true) | ||
setState('buttonFocus') | ||
} | ||
}, | ||
[open] | ||
) | ||
|
||
const {position} = useAnchoredPosition({anchorElementRef: anchorRef, floatingElementRef: overlayRef}) | ||
|
||
useFocusZone({containerRef: overlayRef, disabled: !open || state !== 'listFocus'}, [position]) | ||
useFocusTrap({containerRef: overlayRef, disabled: !open || state !== 'listFocus'}, [position]) | ||
|
||
return ( | ||
<> | ||
{renderAnchor({ | ||
ref: anchorRef, | ||
id: anchorId, | ||
'aria-labelledby': anchorId, | ||
'aria-haspopup': 'listbox', | ||
'aria-label': 'menu', | ||
onClick: onAnchorClick, | ||
onKeyDown: onAnchorKeyDown, | ||
children: anchorContent, | ||
tabIndex: 0 | ||
})} | ||
{open ? ( | ||
<Overlay | ||
initialFocusRef={anchorRef} | ||
returnFocusRef={anchorRef} | ||
onClickOutside={onDismiss} | ||
onEscape={onDismiss} | ||
ref={updateOverlayRef} | ||
{...position} | ||
> | ||
<List | ||
{...listProps} | ||
role="menu" | ||
renderItem={({onClick, ...itemProps}) => | ||
renderItem({ | ||
...itemProps, | ||
role: 'menuitem', | ||
onKeyPress: _event => { | ||
onAction?.(itemProps as ItemProps) | ||
onDismiss() | ||
}, | ||
onClick: event => { | ||
onAction?.(itemProps as ItemProps) | ||
onClick?.(event) | ||
onDismiss() | ||
} | ||
}) | ||
} | ||
/> | ||
</Overlay> | ||
) : null} | ||
</> | ||
) | ||
} | ||
|
||
ActionMenuBase.displayName = 'ActionMenu' | ||
|
||
export const ActionMenu = Object.assign(ActionMenuBase, {Divider: Divider, Item: ActionMenuItem}) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add an example that uses
groupMetadata
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, just pulled from our
ActionList
docs on this one and kept them parallel.