forked from immich-app/immich
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): keyboard accessible context menus (immich-app#10017)
* feat(web,a11y): context menu keyboard navigation * wip: all context menus visible * wip: more migrations to the ButtonContextMenu, usability improvements * wip: migrate Administration, PeopleCard * wip: refocus the button on click, docs * fix: more intuitive RightClickContextMenu - configurable title - focus management: tab keys, clicks, closing the menu - automatically closing when an option is selected * fix: refining the little details - adjust the aria attributes - intuitive escape key propagation - extract context into its own file * fix: dropdown options not clickable in a <Portal> * wip: small fixes - export selectedColor to prevent unexpected styling - better context function naming * chore: revert changes to list navigation, to reduce scope of the PR * fix: remove topBorder prop * feat: automatically select the first option on enter or space keypress * fix: use Svelte store instead to handle selecting menu options - better prop naming for ButtonContextMenu * feat: hovering the mouse can change the active element * fix: remove Portal, more predictable open/close behavior * feat: make selected item visible using a scroll - also: minor cleanup of the context-menu-navigation Svelte action * feat: maintain context menu position on resize * fix: use the whole padding class as better tailwind convention * fix: options not announcing with screen reader for ButtonContextMenu * fix: screen reader announcing right click context menu options * fix: handle focus out scenario --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
- Loading branch information
1 parent
99c6fdb
commit b71aa44
Showing
26 changed files
with
638 additions
and
440 deletions.
There are no files selected for viewing
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,108 @@ | ||
import { shortcuts } from '$lib/actions/shortcut'; | ||
import { tick } from 'svelte'; | ||
import type { Action } from 'svelte/action'; | ||
|
||
interface Options { | ||
/** | ||
* A function that is called when the dropdown should be closed. | ||
*/ | ||
closeDropdown: () => void; | ||
/** | ||
* The container element that with direct children that should be navigated. | ||
*/ | ||
container: HTMLElement; | ||
/** | ||
* Indicates if the dropdown is open. | ||
*/ | ||
isOpen: boolean; | ||
/** | ||
* Override the default behavior for the escape key. | ||
*/ | ||
onEscape?: (event: KeyboardEvent) => void; | ||
/** | ||
* A function that is called when the dropdown should be opened. | ||
*/ | ||
openDropdown?: (event: KeyboardEvent) => void; | ||
/** | ||
* The id of the currently selected element. | ||
*/ | ||
selectedId: string | undefined; | ||
/** | ||
* A function that is called when the selection changes, to notify consumers of the new selected id. | ||
*/ | ||
selectionChanged: (id: string | undefined) => void; | ||
} | ||
|
||
export const contextMenuNavigation: Action<HTMLElement, Options> = (node, options: Options) => { | ||
const getCurrentElement = () => { | ||
const { container, selectedId: activeId } = options; | ||
return container?.querySelector(`#${activeId}`) as HTMLElement | null; | ||
}; | ||
|
||
const close = () => { | ||
const { closeDropdown, selectionChanged } = options; | ||
selectionChanged(undefined); | ||
closeDropdown(); | ||
}; | ||
|
||
const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => { | ||
const { selectionChanged, container, openDropdown } = options; | ||
if (openDropdown) { | ||
openDropdown(event); | ||
await tick(); | ||
} | ||
|
||
const children = Array.from(container?.children).filter((child) => child.tagName !== 'HR') as HTMLElement[]; | ||
if (children.length === 0) { | ||
return; | ||
} | ||
|
||
const currentEl = getCurrentElement(); | ||
const currentIndex = currentEl ? children.indexOf(currentEl) : -1; | ||
const directionFactor = (direction === 'up' ? -1 : 1) + (direction === 'up' && currentIndex === -1 ? 1 : 0); | ||
const newIndex = (currentIndex + directionFactor + children.length) % children.length; | ||
const selectedNode = children[newIndex]; | ||
selectedNode?.scrollIntoView({ block: 'nearest' }); | ||
|
||
selectionChanged(selectedNode?.id); | ||
}; | ||
|
||
const onEscape = (event: KeyboardEvent) => { | ||
const { onEscape } = options; | ||
if (onEscape) { | ||
onEscape(event); | ||
return; | ||
} | ||
event.stopPropagation(); | ||
close(); | ||
}; | ||
|
||
const handleClick = (event: KeyboardEvent) => { | ||
const { selectedId, isOpen, closeDropdown } = options; | ||
if (isOpen && !selectedId) { | ||
closeDropdown(); | ||
return; | ||
} | ||
if (!selectedId) { | ||
void moveSelection('down', event); | ||
return; | ||
} | ||
const currentEl = getCurrentElement(); | ||
currentEl?.click(); | ||
}; | ||
|
||
const { destroy } = shortcuts(node, [ | ||
{ shortcut: { key: 'ArrowUp' }, onShortcut: (event) => moveSelection('up', event) }, | ||
{ shortcut: { key: 'ArrowDown' }, onShortcut: (event) => moveSelection('down', event) }, | ||
{ shortcut: { key: 'Escape' }, onShortcut: (event) => onEscape(event) }, | ||
{ shortcut: { key: ' ' }, onShortcut: (event) => handleClick(event) }, | ||
{ shortcut: { key: 'Enter' }, onShortcut: (event) => handleClick(event) }, | ||
]); | ||
|
||
return { | ||
update(newOptions) { | ||
options = newOptions; | ||
}, | ||
destroy, | ||
}; | ||
}; |
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
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
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
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
Oops, something went wrong.