Skip to content

Commit

Permalink
Add menu support
Browse files Browse the repository at this point in the history
  • Loading branch information
ai committed Feb 14, 2024
1 parent 9f493ab commit 803ca33
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 14 deletions.
13 changes: 13 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ export interface KeyUXModule {
*/
export function hotkeysKeyUX(): KeyUXModule

/**
* Add arrow-navigation on `role="menu"`.
*
* ```js
* import { startKeyUX, menuKeyUX } from 'keyux'
*
* startKeyUX(window, [
* menuKeyUX()
* ])
* ```
*/
export function menuKeyUX(): KeyUXModule

/**
* Add pressed style on button activation from keyboard.
*
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './hotkeys.js'
export * from './press.js'
export * from './menu.js'

export function startKeyUX(window, plugins) {
let unbinds = plugins.map(plugin => plugin(window))
Expand Down
85 changes: 85 additions & 0 deletions menu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
export function menuKeyUX() {
return window => {
let inMenu = false

function focus(current, next) {
next.tabIndex = 0
next.focus()
current.tabIndex = -1
}

function keyDown(event) {
if (event.target.role !== 'menuitem') {
stop()
return
}

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

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

let nextKey = 'ArrowDown'
let prevKey = 'ArrowUp'
if (menu.getAttribute('aria-orientation') === 'horizontal') {
if (window.document.dir === 'rtl') {
nextKey = 'ArrowLeft'
prevKey = 'ArrowRight'
} else {
nextKey = 'ArrowRight'
prevKey = 'ArrowLeft'
}
}

if (event.key === nextKey) {
focus(event.target, items[index + 1] || items[0])
} else if (event.key === prevKey) {
focus(event.target, items[index - 1] || items[items.length - 1])
} else if (event.key === 'Home') {
focus(event.target, items[0])
} else if (event.key === 'End') {
focus(event.target, items[items.length - 1])
}
}

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

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

if (!inMenu) {
inMenu = true
window.addEventListener('keydown', keyDown)
}
let items = menu.querySelectorAll('[role="menuitem"]')
for (let item of items) {
if (item !== event.target) {
item.setAttribute('tabindex', -1)
}
}
} else if (inMenu) {
stop()
}
}

function focusOut(event) {
if (!event.relatedTarget || event.relatedTarget === window.document) {
stop()
}
}

window.addEventListener('focusin', focusIn)
window.addEventListener('focusout', focusOut)
return () => {
stop()
window.removeEventListener('focusin', focusIn)
window.removeEventListener('focusout', focusOut)
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@
{
"name": "All modules",
"import": {
"./index.js": "{ startKeyUX, hotkeysKeyUX, pressKeyUX }"
"./index.js": "{ startKeyUX, hotkeysKeyUX, pressKeyUX, menuKeyUX }"
},
"limit": "764 B"
"limit": "777 B"
}
],
"clean-publish": {
Expand Down
41 changes: 34 additions & 7 deletions test/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KeyUX</title>
<style>
:focus-visible {
z-index: 10;
outline: 3px solid #4d90fe;
outline-offset: 3px;
transition: outline-width 200ms cubic-bezier(0.34, 1.56, 0.64, 1),
outline-offset 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
button,
a {
&:active,
&.is-pressed {
padding-top: calc(0.5em + 1px);
padding-bottom: calc(0.5em - 1px);
box-shadow: inset 0 1px 2px rgba(0 0 0 / 0.3);
}
}
button {
padding: 0.5em 1em;
border: 1px solid #aaa;
Expand All @@ -15,13 +31,6 @@
&:hover {
background: #f4f4f4;
}

&:active,
&.is-pressed {
padding-top: calc(0.5em + 1px);
padding-bottom: calc(0.5em - 1px);
box-shadow: inset 0 1px 2px rgba(0 0 0 / 0.3);
}
}
button strong {
min-width: 1.5em;
Expand All @@ -40,6 +49,24 @@
font-weight: bold;
color: #444;
}
.menu {
border-radius: 6px;
border: 1px solid #aaa;
display: flex;
margin-top: 1em;
width: fit-content;
}
.menu_item {
display: block;
padding: 0.5em 1em;
color: black;
&:first-child {
border-radius: 5px 0 0 5px;
}
&:last-child {
border-radius: 0 5px 5px 0;
}
}
</style>
</head>
<body>
Expand Down
50 changes: 46 additions & 4 deletions test/demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { type FC, createElement as h, useState } from 'react'
import { type FC, Fragment, createElement as h, useState } from 'react'
import { createRoot } from 'react-dom/client'

import { hotkeysKeyUX, pressKeyUX, startKeyUX } from '../../index.js'
import { hotkeysKeyUX, menuKeyUX, pressKeyUX, startKeyUX } from '../../index.js'

startKeyUX(window, [hotkeysKeyUX(), pressKeyUX('is-pressed')])
startKeyUX(window, [hotkeysKeyUX(), menuKeyUX(), pressKeyUX('is-pressed')])

const App: FC = () => {
const Counter: FC = () => {
let [clicked, setClicked] = useState(0)
return h(
'button',
Expand All @@ -21,4 +21,46 @@ const App: FC = () => {
)
}

const Menu: FC = () => {
return h(
'nav',
{
'aria-orientation': 'horizontal',
'className': 'menu',
'role': 'menu'
},
h(
'a',
{
className: 'menu_item',
href: '#home',
role: 'menuitem'
},
'Home'
),
h(
'a',
{
className: 'menu_item',
href: '#about',
role: 'menuitem'
},
'About'
),
h(
'a',
{
className: 'menu_item',
href: '#contact',
role: 'menuitem'
},
'Contact'
)
)
}

const App: FC = () => {
return h(Fragment, {}, h(Counter), h(Menu))
}

createRoot(document.getElementById('app')!).render(h(App))
Loading

0 comments on commit 803ca33

Please sign in to comment.