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 keydisplaychanges 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:
disabledon the provider, orenableVirtualKeyboard={false}on one field, leaves it typing with a real keyboard and nothing else. disabledis 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 clearingdisableddoes 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.