elements

Members

(static, constant) ElementBase

HTMLElement when there is one, a plain class when there is not (e.g. Node under test), so custom element modules stay importable outside the browser.

Source:
Example
class MyElement extends ElementBase {}

Methods

(static) announcer(host, optionsopt) → {Announcer}

A live region, and the one way of putting something in it that a screen reader reads.

Two things go wrong with live regions, both silently, which is what makes them worth a helper rather than four lines at each call site.

A region announces text that arrives in one already in the document. Creating the element and filling it in the same breath announces nothing at all - there was no live region in the accessibility tree yet for the change to happen in. So this is called when there is nothing to say, and the saying comes later.

A region announces a change. Setting the same sentence twice is not one, so the second copy, the second failed save and the second press of a toggle are all silent. Clearing first and setting in a later task is what makes the second one a change.

delay is how much later. 0 - the next task - is enough for two mutations to be recorded where one would otherwise be, and is what this has always used. It is an option rather than a constant because live region behaviour is, in ARIA's own words, a strong suggestion that may be overridden by the browser, the assistive technology or the user: a page that finds its own pairing needs longer can say so without patching this. The number has not been measured against NVDA, JAWS or VoiceOver, and is not presented as tuned.

The node is a <span> the caller names, because where it sits and how it is hidden are the caller's business. Hide it by clipping - display: none and visibility: hidden both take it out of the accessibility tree, and a region nothing can read is the whole point undone.

Source:
Parameters:
Name Type Attributes Description
host Element

Element the region is appended to. One already there under the same class is adopted rather than a second one added, so calling this twice is safe.

options object <optional>
Name Type Attributes Default Description
className string <optional>
live-region

The class the node carries, and how it is found again

role string <optional>
status

status waits for a gap in what is being read, alert interrupts. Answering something the reader just did is status - they are not being interrupted with the result of their own action

delay number <optional>
0

Milliseconds between clearing the region and setting it

Returns:
Type:
Announcer
Example
const status = announcer(el, { className: 'copy-status' })
status.say('Copied') // and again later, and it is read again
status.destroy() // when the element goes

(static) define(tag, ctor)

Registers a custom element - in the browser only, and only once, so the module is safe to import twice or to import under Node.

Source:
Parameters:
Name Type Description
tag string
ctor function
Example
define('my-element', MyElement)
define('my-element', MyElement) // second call is a no-op

(static) fits(at, size, limit) → {boolean}

Whether a box of size starting at at is inside a viewport of limit.

Both ends, because a panel that runs off the top is as unreachable as one that runs off the bottom.

Source:
Parameters:
Name Type Description
at number

Where the box starts, in viewport coordinates

size number
limit number

The viewport's extent on the same axis

Returns:
Type:
boolean
Example
fits(700, 300, 800) // => false, runs off the bottom
fits(100, 300, 800) // => true

(static) nextIndex(current, key, length) → {number|null}

Where an arrow, Home or End key moves focus in a wrapping list of widgets - the key map the APG patterns with a roving tabindex (menu, tablist, accordion headers) share.

Source:
Parameters:
Name Type Description
current number

Index of the currently focused item, -1 for none

key string

KeyboardEvent.key value

length number

Number of items in the list

Returns:
Type:
number | null

Target index, or null if the key is unhandled

Example
nextIndex(0, 'ArrowDown', 3) // => 1
nextIndex(2, 'ArrowDown', 3) // => 0, wraps
nextIndex(0, 'End', 3) // => 2
nextIndex(0, 'Tab', 3) // => null

(static) placeFlyout(trigger, panel, viewport, rtl, centredopt) → {Object}

Where a floating panel goes relative to its trigger: under it, or over it when there is no room under; and running from the trigger's inline start, or back the other way when that would take it off the edge.

The preferred placement wins ties and wins when neither fits, because a panel with nowhere good to go should at least land where the reader expects it.

centred asks for the panel to sit on the trigger's middle - what a tooltip wants, where an edge-aligned bubble points at nothing. It is a preference and not an instruction: a trigger near the edge cannot be centred on without the panel hanging off it, so the answer falls back to the edge that fits. Off by default, because align is spent as a CSS keyword and a caller whose stylesheet answers only start and end must not be handed a third value it has no rule for.

Only the inline axis: which side of the trigger the panel goes on is a separate question, and this does not change its answer.

Source:
Parameters:
Name Type Attributes Default Description
trigger DOMRect | object

Rect of the trigger, in viewport coordinates

panel Object

Size of the panel

viewport Object
rtl boolean

Whether the layout runs right to left

centred boolean <optional>
false

Prefer the trigger's middle over either of its edges

Returns:
Type:
Object

side is block-end/block-start, align is start/end, or center when centred was asked for and there was room - the CSS spelling, since that is where the value is spent

Examples
const viewport = { width: 1000, height: 800 }
placeFlyout(button.getBoundingClientRect(), { width: 200, height: 300 }, viewport, false)
// => { side: 'block-end', align: 'start' } when there is room below
placeFlyout(rect, { width: 200, height: 300 }, viewport, false, true)
// => { side: 'block-end', align: 'center' } when the middle has room for it

(static) placeSubmenu(item, panel, viewport, rtl) → {Object}

Where a nested panel goes: beside the item that opens it, on the inline end unless the edge is there, and running down from the item unless the bottom is.

Which is how a submenu near the bottom right corner ends up opening up and to the left - one decision per axis rather than a list of corners.

Source:
Parameters:
Name Type Description
item DOMRect | object

Rect of the item that opens it, in viewport coordinates

panel Object
viewport Object
rtl boolean

Whether the layout runs right to left

Returns:
Type:
Object

side is inline-end/inline-start, align is start/end

Example
placeSubmenu(item.getBoundingClientRect(), { width: 200, height: 300 }, viewport, false)
// => { side: 'inline-end', align: 'start' } when there is room beside it

(static) stepIndex(current, key, length) → {number|null}

Where an arrow, Home or End key moves focus in a list with ends - the non-wrapping counterpart of nextIndex, for sets where running off one end is how you get back to the rest of the page (the APG disclosure navigation, a toolbar).

Answers to both axes, so a horizontal bar and a vertical stack share it.

Source:
Parameters:
Name Type Description
current number

Index of the focused item, -1 for none

key string

KeyboardEvent.key value

length number

Number of items in the set

Returns:
Type:
number | null

Target index, or null if the key is unhandled or there is nowhere to go

Example
stepIndex(0, 'ArrowRight', 4) // => 1
stepIndex(3, 'ArrowRight', 4) // => null, the ends do not wrap
stepIndex(2, 'Home', 4) // => 0

(static) typeAheadIndex(labels, current, buffer) → {number|null}

Where a type-ahead lands in a list, given what has been typed so far.

Two rules that look like edge cases and are not. Holding or repeating one letter cycles the items starting with it - aaa is someone pressing a three times looking for the next "Archive", not an item named "aaa". And a search that is one character long starts after the focused item, so pressing that letter again moves on, while a buffer still being typed starts at it, so the match narrows onto the item the reader is already on instead of skipping past it.

Source:
Parameters:
Name Type Description
labels Array.<string>

The items' text, in list order

current number

Index of the focused item, -1 for none

buffer string

What has been typed inside the type-ahead window

Returns:
Type:
number | null

Target index, or null if nothing matches

Example
const labels = ['Profile', 'Preferences', 'Archive']
typeAheadIndex(labels, 0, 'a') // => 2
typeAheadIndex(labels, 0, 'p') // => 1, starts after the focused item
typeAheadIndex(labels, 1, 'pre') // => 1, a buffer narrows onto it

Type Definitions

Announcer

Properties:
Name Type Description
node Element

The live region itself

say AnnouncerSay

Announces, and announces again for a repeated message

clear AnnouncerAction

Empties the region at once

destroy AnnouncerAction

Drops a message still in the air and removes the node

Source:
Type:
  • object

AnnouncerAction() → {void}

Source:
Returns:
Type:
void

AnnouncerSay(message) → {void}

Source:
Parameters:
Name Type Description
message string

What is read out

Returns:
Type:
void