Skip to contentAsheeUIAsheeUI

Keyboard

An on-screen keyboard that types into the field you focus, for a screen without a real one.

An on-screen keyboard is a keyboard the page draws: a grid of keys at the foot of the window that types into whichever field has focus. You reach for one where the platform's keyboard is not there to be had, a kiosk, a touch panel, a till, or where the keys should follow the product's theme rather than the operating system's.

It is a provider's keyboard, not a global mode: a field opens it only while a KeyboardProvider sits above it, and with no provider the field behaves exactly as it otherwise would. Under a provider every field is wired by default, enableVirtualKeyboard defaults to true, so a page opts in once and switches individual fields out only when it needs to.

Usage

import { Input, KeyboardProvider } from "@asheeui/web";

<KeyboardProvider>
  <Input label="Recipient" enableVirtualKeyboard />
</KeyboardProvider>

The provider holds the state, whether the keyboard is open, which element it is typing into, which layout is showing, and renders the keyboard itself, so a page never mounts one by hand. A field opens it on focus and requests a close on blur, which the provider honours after closeDelay so that moving between two fields does not make the keyboard flicker. A blur whose new focus is inside the keyboard is not a close: that is a reader moving to the keys, not leaving the field.

It reads the application's configuration, so it sits inside AsheeUIProvider like every other component. One provider around a form, a screen or the whole application is enough; it costs nothing until a field in its tree is focused, and fields outside its tree are untouched. A nested provider owns the keyboard for its own subtree, which is how a page gives a group of fields a different set of keys or a different treatment without restating the application's configuration.

Layouts and keys

Four layouts ship with the component: default (lowercase alphabet and digits), shift (uppercase), symbols (punctuation) and numeric (a keypad). A layout is a list of rows, and a row is a space-separated list of tokens, so the {space} token is how a row states its space bar. The provider's defaultLayout names the one that opens for a field that does not ask for another.

Stating layouts replaces the set rather than extending it, so a custom layout is written beside the ones it still needs, in the demo, ...defaultKeyboardLayouts keeps the alphabet, shift and symbols keys available while code adds a keypad. A layout name is a free string: the four standard names have meaning to the component, and anything else is simply a layout you supply.

A field can name the layout it opens: enableVirtualKeyboard={{ layout: "numeric" }} gives that field a keypad while the rest of the form keeps the provider's alphabet. The layout goes back to the provider's default when the keyboard closes, so the next field starts from the provider's choice, and refocusing a field whose keyboard is already open keeps whatever layout is showing. A group of fields that wants another set of keys rather than another layout still states it on a nested provider, which restates the whole keyboard for its subtree.

A token is either one of the named keys or literal text, a single character, usually, though nothing stops a token being a whole word. The named keys carry behaviour:

Token What the key does Default label
{space} Inserts a space space
{bksp} Deletes backwards from the caret, or the selection ⌫
{enter} Blurs a single-line field; inserts a line break in a textarea ↵
{shift} Switches to the shift layout, and back ⇧
{symbols} Switches to the symbols layout ?123
{abc} Returns to the default layout ABC
{clear} Empties the field C

autoShiftBack returns to the default layout after one shifted character, so a reader who wants a capital does not have to press ⇧ twice. display replaces the label a token is drawn with, an icon, an abbreviation, a word in another language, and defaults to the glyphs in the table. What a screen reader says is the other half of the label: a named key announces its action however it is drawn, and the Accessibility section has the names.

Wiring a field

Input and Textarea each take enableVirtualKeyboard, which is true by default and only means anything under a provider. Passing an object instead of true states that field's own layout, size, variant, color, radius and portal, so the keyboard can take the numeric layout and the large keys on one field without restating the provider around it.

The provider's props and a field's options are the two levels a keyboard's look is stated at; the field's options win for that field, and the provider's values, which themselves resolve through the application's configuration, stand everywhere else. Keys are drawn with the framework's Button, so a keyboard takes the same treatments a button does. className reaches the keyboard's container and keyClassName every key.

A field that is not one of the family connects through useKeyboardField(id, ref, enabled, options), which returns handleFocus, handleBlur and hasProvider to pass to any input-like element. hasProvider says whether a provider sits above at all, the difference between a field that could open a keyboard and one that cannot, so a custom field can draw its own affordance only where a keyboard exists:

import { useKeyboardField } from "@asheeui/web";
import { useRef } from "react";

function Nickname() {
  const ref = useRef<HTMLInputElement>(null);
  const { handleFocus, handleBlur } = useKeyboardField("nickname", ref, true, {
    size: "lg",
  });

  return (
    <input
      ref={ref}
      aria-label="Nickname"
      onFocus={handleFocus}
      onBlur={handleBlur}
    />
  );
}

The keyboard writes through the element it is attached to: it sets the value, dispatches the event a controlled field listens for, and restores the caret. So a field that keeps its value in state updates normally, and typing into the middle of a value inserts there rather than at the end.

Props

layouts, display, defaultLayout, closeDelay, autoShiftBack, size, portal and disabled are registered under components.keyboard, so a configuration file can state the keyboard's habits once. variant, color and radius are deliberately not registered: they resolve from the application's defaultVariant, defaultColor and defaultRadius and then the framework's own fallbacks, which is what keeps the keys the same accent as the rest of the page without restating it.

closeDelay resolves through the cascade like the rest: the instance, then components.keyboard.closeDelay, then the component's registered 500 ms. It is the one value worth stating in configuration rather than per provider, because a product's fields should all wait the same length of time before the keyboard goes.

OnScreenKeyboard is what the provider renders, and its props are the provider's state passed down: whether it is open, the element it is typing into, the resolved configuration and the resolved size, variant, colour and rounding. Its documentation marks it internal: the package exports it because the provider is built from it, and mounting one by hand gives you keys with no provider behind them, so mount a provider instead.

Accessibility

  • The keyboard is a <section aria-label="Virtual Keyboard">, so it is announced as a region named "Virtual Keyboard", and every key is a real button. A letter, digit or punctuation key takes its name from the label it draws. A named key announces the action it performs, Backspace, Enter, Shift, Symbols, Letters, Space and Clear, while the button draws the glyph (⌫, ↵, ⇧, ?123, ABC, C), so a screen reader says what the key does rather than reading a symbol. For a named key display changes the drawing alone, not the spoken name.
  • A press on the pointer is cancelled by the container, so a pointer reader's field keeps focus and the character lands at the caret. A key pressed from the keyboard leaves focus on that key instead of pulling it back to the field, which is where a reader who arrived by tab expects to stay. While the keyboard is open, keys typed on a physical keyboard are echoed as highlights, and Escape closes it.
  • The keys are ordinary buttons and therefore in the tab order, and tabbing into them keeps the keyboard open: a blur whose new focus is inside the keyboard is ignored rather than treated as a close. The arrow keys walk the grid, left and right along a row, up and down between rows, because a grid of forty keys would otherwise cost a tab for each one, and Enter or Space presses the key that has focus. A reader who would rather type on the keyboard they already have is served by the field itself: disabled on the provider, or enableVirtualKeyboard={false} on one field, leaves it typing with a real keyboard and nothing else.
  • disabled is checked both when a field opens the keyboard and when a key is pressed, so it prevents a new one. Setting it while the keyboard is open closes that keyboard at once and forgets the field it belonged to, so clearing disabled does not bring one back until a field is focused again, the setting a page uses when the operating system already provides a keyboard.

Native

The platform provides its own keyboard, so @asheeui/native ships none and there is nothing to import: an on-screen keyboard exists to compensate for a browser constraint, and a device has already solved it. The compatibility matrix records the pair as not applicable rather than as a difference in behaviour; see React Native.