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) 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