Skip to content

Commit

Permalink
Merge pull request #16 from dmitry-kurmanov/feature/9-extend-menu-to-…
Browse files Browse the repository at this point in the history
…focus-group

PR: menu → focus-group refactoring
  • Loading branch information
ai authored Mar 14, 2024
2 parents b7efb52 + 6bbd944 commit b873f8b
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 56 deletions.
74 changes: 65 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
hiddenKeyUX,
hotkeyKeyUX,
jumpKeyUX,
menuKeyUX,
focusGroupKeyUX,
pressKeyUX,
startKeyUX
} from 'keyux'
Expand All @@ -55,7 +55,7 @@ const overrides = {}

startKeyUX(window, [
hotkeyKeyUX(overrides),
menuKeyUX(),
focusGroupKeyUX(),
pressKeyUX('is-pressed'),
jumpKeyUX(),
hiddenKeyUX()
Expand Down Expand Up @@ -217,13 +217,69 @@ with arrow navigation.
Users will use <kbd>Tab</kbd> to get inside the menu, and will use either arrows or <kbd>Home</kbd>,
<kbd>End</kbd> or an item name to navigate inside.

To enable this feature, call `menuKeyUX`.
To enable this feature, call `focusGroupKeyUX`.

```js
import { menuKeyUX } from 'keyux'
import { focusGroupKeyUX } from 'keyux'

startKeyUX(window, [
menuKeyUX()
focusGroupKeyUX()
])
```


### Listbox

The [`role="listbox"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role)
is used for lists from which a user may select one or
more items which are static and, unlike HTML <select> elements, may contain images.

```html
<ul role="listbox">
<li tabindex="0" role="option">Pizza</li>
<li tabindex="0" role="option">Sushi</li>
<li tabindex="0" role="option">Ramen</li>
</ul>
```

Users will use <kbd>Tab</kbd> to get inside the menu, and will use either arrows or <kbd>Home</kbd>,
<kbd>End</kbd> or an item name to navigate inside.

To enable this feature, call `focusGroupKeyUX`.

```js
import { focusGroupKeyUX } from 'keyux'

startKeyUX(window, [
focusGroupKeyUX()
])
```


### Tablist

The [`role="tablist"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role)
identifies the element that serves as the container for a set of tabs.
The tab content should be marked by `[role="tabpanel']`.

```html
<div role="tablist">
<button role="tab">Home</button>
<button role="tab">About</button>
<button role="tab">Contact</button>
</div>
```

Users will use <kbd>Tab</kbd> to get inside the menu, and will use either arrows or <kbd>Home</kbd>,
<kbd>End</kbd>.

To enable this feature, call `focusGroupKeyUX`.

```js
import { focusGroupKeyUX } from 'keyux'

startKeyUX(window, [
focusGroupKeyUX()
])
```

Expand Down Expand Up @@ -260,10 +316,10 @@ You can add `aria-controls` to `<input>` to make the focus jump on <kbd>Enter</k
To enable this feature, call `jumpKeyUX`.

```js
import { menuKeyUX, jumpKeyUX } from 'keyux'
import { focusGroupKeyUX, jumpKeyUX } from 'keyux'

startKeyUX(window, [
menuKeyUX(),
focusGroupKeyUX(),
jumpKeyUX()
])
```
Expand Down Expand Up @@ -294,10 +350,10 @@ have to set `tabindex="-1"` manually.
To enable this feature, call `hiddenKeyUX`.

```js
import { menuKeyUX, jumpKeyUX, hiddenKeyUX } from 'keyux'
import { focusGroupKeyUX, jumpKeyUX, hiddenKeyUX } from 'keyux'

startKeyUX(window, [
menuKeyUX(),
focusGroupKeyUX(),
jumpKeyUX(),
hiddenKeyUX()
])
Expand Down
59 changes: 42 additions & 17 deletions menu.js → focus-group.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export function menuKeyUX(options) {
const ROLES = {
'menuitem': ["menu", "menubar"],
'option': ["listbox"],
'tab': ["tablist"]
}

export function focusGroupKeyUX(options) {
return window => {
let inMenu = false
let inGroup = false
let typingDelayMs = options?.searchDelayMs || 300
let lastTyped = 0
let searchPrefix = ''
Expand All @@ -11,21 +17,41 @@ export function menuKeyUX(options) {
current.tabIndex = -1
}

function findGroupNodeByEventTarget(eventTarget) {
let itemRole = eventTarget.role
let groupRoles = ROLES[itemRole]
if (!groupRoles) return null

for (let role of groupRoles) {
let node = eventTarget.closest(`[role=${role}]`)
if (node) return node
}
}

function isHorizontalOrientation(group) {
let ariaOrientation = group.getAttribute('aria-orientation')
if (ariaOrientation === "vertical") return false
if (ariaOrientation === "horizontal") return true

let role = group.role
return role === "menubar" || role === "tablist";
}


function keyDown(event) {
if (event.target.role !== 'menuitem') {
let group = findGroupNodeByEventTarget(event.target);

if (!group) {
stop()
return
}

let menu = event.target.closest('[role="menu"]')
if (!menu) return

let items = menu.querySelectorAll('[role="menuitem"]')
let items = group.querySelectorAll(`[role=${event.target.role}]`)
let index = Array.from(items).indexOf(event.target)

let nextKey = 'ArrowDown'
let prevKey = 'ArrowUp'
if (menu.getAttribute('aria-orientation') === 'horizontal') {
if (isHorizontalOrientation(group)) {
if (window.document.dir === 'rtl') {
nextKey = 'ArrowLeft'
prevKey = 'ArrowRight'
Expand All @@ -47,7 +73,7 @@ export function menuKeyUX(options) {
} else if (event.key === 'End') {
event.preventDefault()
focus(event.target, items[items.length - 1])
} else if (event.key.length === 1) {
} else if (event.key.length === 1 && group.role !== "tablist") {
let now = Date.now()
if (now - lastTyped <= typingDelayMs) {
searchPrefix += event.key.toLowerCase()
Expand All @@ -70,26 +96,25 @@ export function menuKeyUX(options) {
}

function stop() {
inMenu = false
inGroup = false
window.removeEventListener('keydown', keyDown)
}

function focusIn(event) {
if (event.target.role === 'menuitem') {
let menu = event.target.closest('[role="menu"]')
if (!menu) return
let group = findGroupNodeByEventTarget(event.target);
if (group) {

if (!inMenu) {
inMenu = true
if (!inGroup) {
inGroup = true
window.addEventListener('keydown', keyDown)
}
let items = menu.querySelectorAll('[role="menuitem"]')
let items = group.querySelectorAll(`[role=${event.target.role}]`)
for (let item of items) {
if (item !== event.target) {
item.setAttribute('tabindex', -1)
}
}
} else if (inMenu) {
} else if (inGroup) {
stop()
}
}
Expand Down
12 changes: 6 additions & 6 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface KeyUXModule {
(window: MinimalWindow): () => void
}

export interface MenuKeyUXOptions {
export interface FocusGroupKeyUXOptions {
/**
* Maximum allowed pause between key presses when searching
* for a list item by name. Default is 300.
Expand All @@ -55,14 +55,14 @@ export function hotkeyKeyUX(overrides?: HotkeyOverride): KeyUXModule
* Add arrow-navigation on `role="menu"`.
*
* ```js
* import { startKeyUX, menuKeyUX } from 'keyux'
* import { startKeyUX, focusGroupKeyUX } from 'keyux'
*
* startKeyUX(window, [
* menuKeyUX()
* focusGroupKeyUX()
* ])
* ```
*/
export function menuKeyUX(options?: MenuKeyUXOptions): KeyUXModule
export function focusGroupKeyUX(options?: FocusGroupKeyUXOptions): KeyUXModule

/**
* Add pressed style on button activation from keyboard.
Expand Down Expand Up @@ -111,15 +111,15 @@ export function hiddenKeyUX(): KeyUXModule
* import {
* startKeyUX,
* hotkeyKeyUX,
* menuKeyUX,
* focusGroupKeyUX,
* pressKeyUX,
* jumpKeyUX,
* hiddenKeyUX
* } from 'keyux'
*
* startKeyUX(window, [
* hotkeyKeyUX(),
* menuKeyUX(),
* focusGroupKeyUX(),
* pressKeyUX('is-pressed'),
* jumpKeyUX(),
* hiddenKeyUX()
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './focus-group.js'
export * from './hotkey.js'
export * from './hidden.js'
export * from './press.js'
export * from './menu.js'
export * from './jump.js'

export function startKeyUX(window, plugins) {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@
{
"name": "All modules",
"import": {
"./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, menuKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }"
"./index.js": "{ startKeyUX, hotkeyKeyUX, pressKeyUX, focusGroupKeyUX, jumpKeyUX, hiddenKeyUX, likelyWithKeyboard, getHotKeyHint }"
},
"limit": "1737 B"
"limit": "1853 B"
}
],
"clean-publish": {
Expand Down
13 changes: 13 additions & 0 deletions test/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@
border-radius: 2px;
margin: 0.5em 0;
}
.tablist_container {
margin-top: 2em;
margin-bottom: 2em;
}
.tablist_tab {
margin-right: 0.5em;
}
.tabcontent {
display: none;
}
.tabcontent--current {
display: block;
}
</style>
</head>
<body>
Expand Down
Loading

0 comments on commit b873f8b

Please # to comment.