# Keypad and Keyboard

> On-screen keys that type into a text field: a PIN or dial pad, or a full keyboard.

## Anatomy

A keypad or keyboard is a set of on-screen keys that type into a text field.

A `.keypad` is three columns of round keys for PINs, codes and amounts. A `.keyboard` is rows of keys for text. Give either one `role="group"` and a name, and point `aria-controls` at the field it types into. Make every key a `<button type="button">`. A key with a `value` types it at the caret, and the field gets the same `input` event typing would fire. `AutoInit()` starts every `.keypad` and `.keyboard`.

```
<div class="field outlined">
  <input id="pin" type="password" inputmode="none" maxlength="6" placeholder=" " autocomplete="off">
  <label for="pin">PIN</label>
</div>

<div class="keypad" role="group" aria-label="PIN keypad" aria-controls="pin">
  <button type="button" value="1">1</button>
  <button type="button" value="2">2</button>
  <!-- … 3 to 9 … -->
  <span></span>
  <button type="button" value="0">0</button>
  <button type="button" data-key="Backspace" aria-label="Delete">
    <span class="material-symbols" aria-hidden="true">backspace</span>
  </button>
</div>
```

## Dial pad

Put a key's letters in a `<small>` after the digit. A key with a button style class keeps that style, so a call key is a `filled` button. Add `wide` to a key to span two cells as a pill. That fills the last row beside Backspace.

```
<div class="keypad" role="group" aria-label="Dial pad" aria-controls="phone">
  <button type="button" value="2">2<small aria-hidden="true">ABC</small></button>
  <!-- … -->
  <button type="button" class="filled wide" data-key="Enter" aria-label="Call">
    <span class="material-symbols" aria-hidden="true">call</span>
  </button>
  <button type="button" data-key="Backspace" aria-label="Delete">
    <span class="material-symbols" aria-hidden="true">backspace</span>
  </button>
</div>
```

### Backspace in the field

For a centered call key, move Backspace into the text field as a trailing `icon-button suffix`. Hold the cells beside the call key open with empty `<span>` elements. A `data-key` key inside the field's `.field` works like a keypad key, even when the field renders after the keypad. Other buttons there, such as a password toggle, keep their own behavior.

```
<div class="field">
  <input id="phone" type="tel" inputmode="none" placeholder=" " autocomplete="off">
  <label for="phone">Phone number</label>
  <button type="button" class="icon-button suffix" data-key="Backspace" aria-label="Delete">
    <span class="material-symbols" aria-hidden="true">backspace</span>
  </button>
</div>

<div class="keypad" role="group" aria-label="Dial pad" aria-controls="phone">
  <!-- 1 to # -->
  <span></span>
  <button type="button" class="filled" data-key="Enter" aria-label="Call">
    <span class="material-symbols" aria-hidden="true">call</span>
  </button>
  <span></span>
</div>
```

### A third action

To put a page action such as contacts in the empty cell, give the key a `data-key` the script does not know. That key types nothing and calls `onKey` with its name.

```
<div class="keypad" role="group" aria-label="Dial pad" aria-controls="phone">
  <!-- 1 to # -->
  <button type="button" data-key="Contacts" aria-label="Contacts">
    <span class="material-symbols" aria-hidden="true">contacts</span>
  </button>
  <button type="button" class="filled" data-key="Enter" aria-label="Call">
    <span class="material-symbols" aria-hidden="true">call</span>
  </button>
  <button type="button" data-key="Backspace" aria-label="Delete">
    <span class="material-symbols" aria-hidden="true">backspace</span>
  </button>
</div>

<script>
Expressive.Keypad.init(document.querySelector('.keypad'), {
  onKey: (key) => { if (key === 'Contacts') openContacts(); },
});
</script>
```

## Keyboard

Each child of a `.keyboard` is a row. A character key is a tenth of the row and may shrink. A function key is one and a half keys wide, and the space bar takes what is left. A nine-key row is centered, which gives QWERTY its half-key indent. Set `--md-comp-keyboard-columns` for a layout with more or fewer keys in its widest row.

An optional `<div class="numbers">` row of digits sits above the letters, with shorter 40dp keys. To drop it, leave it out or add `hidden`. Remove `hidden` to show it again.

```
<div class="keyboard" role="group" aria-label="Keyboard" aria-controls="message">
  <div class="numbers">
    <button type="button" value="1">1</button>
    <!-- … 2 to 0 … -->
  </div>
  <div>
    <button type="button" value="q">q</button>
    <!-- … w to p … -->
  </div>
  <div><!-- a to l --></div>
  <div>
    <button type="button" data-key="Shift" aria-pressed="false" aria-label="Shift">
      <span class="material-symbols" aria-hidden="true">shift</span>
    </button>
    <!-- z to m -->
    <button type="button" data-key="Backspace" aria-label="Delete">
      <span class="material-symbols" aria-hidden="true">backspace</span>
    </button>
  </div>
  <div>
    <button type="button" value=",">,</button>
    <button type="button" value=" " aria-label="Space"></button>
    <button type="button" value=".">.</button>
    <button type="button" data-key="Enter" aria-label="New line">
      <span class="material-symbols" aria-hidden="true">keyboard_return</span>
    </button>
  </div>
</div>
```

## Keys

| Key | Effect |
| --- | --- |
| `value="…"` | Types the value at the caret, replacing any selection. Typing stops at the field's `maxlength`. |
| `data-key="Backspace"` | Deletes the selection, or the character before the caret. |
| `data-key="Enter"` | Submits the field's form with `requestSubmit()`, so validation runs. In a `<textarea>` it types a new line. |
| `data-key="Shift"` | Toggles `aria-pressed`. While pressed the character keys show capitals, and the next one types a capital and releases Shift. |
| `data-key="CapsLock"` | Toggles `aria-pressed`. While pressed the character keys show and type capitals, until it is pressed again. |
| `data-key="Layer"` | Shows the elements in the keyboard whose `data-layer` matches the key's `value`, such as a row of symbols in place of the letters. Hides those with another `data-layer`. Elements without `data-layer` always show. |
| Any other `data-key` | Types nothing and calls `onKey` with its name. |

The script ignores a `<button>` without `value` or `data-key`, because it is not a key. Keys type at the caret. In a field nobody has clicked into, they type at the end. While the field has focus, holding down a hardware key presses the matching on-screen key, so the keyboard shows what is typed.

Number and email inputs have no caret position to read, so keys type at the end. A number input empties itself on a partial value such as `12.` or `-`, so the script ignores that key there. For decimals and signs, use a text field with `inputmode="none"`.

## Accessibility

-   Name the group with `aria-label` and point `aria-controls` at the field. Give the field its own label.
-   Give every key `type="button"`, or a key inside a form submits it. Name icon keys and the space bar with `aria-label`.
-   The keypad checks `minlength` itself, because browsers check it only on what a person types and keypad input comes from a script. A shorter value gets the `i18n.tooShort` message until it is long enough. The keypad never replaces a message the page set, and a form reset or `destroy()` clears its own. `pattern` and `required` apply as usual.
-   Add `inputmode="none"` to the field so a phone or tablet keeps its own keyboard closed. A hardware keyboard still types into the field.
-   A pointer press keeps focus and the caret in the field while it has focus. Keys stay native buttons, so Tab, Enter and Space reach them.
-   Keys are 72dp on a keypad and 48dp tall on a keyboard. Keep a keyboard wide enough that a character key stays at least 24px wide.

## Tokens

| Token | Default |
| --- | --- |
| `--md-comp-keypad-key-size` | 72px |
| `--md-comp-keypad-gap` | 12px |
| `--md-comp-keypad-key-color` | `surface-container-highest` |
| `--md-comp-keypad-key-label-color` | `on-surface` |
| `--md-comp-keypad-function-key-color` | `secondary-container` |
| `--md-comp-keypad-function-key-label-color` | `on-secondary-container` |
| `--md-comp-keypad-key-pressed-shape` | 12px; keyboard: 6px |
| `--md-comp-keyboard-columns` | 10 |
| `--md-comp-keyboard-gap` | 6px |
| `--md-comp-keyboard-key-height` | 48px |
| `--md-comp-keyboard-number-key-height` | 40px |
| `--md-comp-keyboard-key-shape` | 12px |
| `--md-comp-keyboard-container-color` | `surface-container` |
| `--md-comp-keyboard-container-shape` | 16px |

## Options

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `onKey` | Function | `null` | Called after every key press with the text typed (`'7'`, `'A'`, `' '`) or the function key's name (`'Backspace'`, `'Enter'`, `'Shift'`, `'CapsLock'`, `'Layer'`). Works without `aria-controls`. |
| `i18n.tooShort` | Function | English | Takes the field's `minlength` and returns the validation message for a shorter keypad-typed value: `Use at least 3 characters.` |

## Methods

```
const keypad = Expressive.Keypad.getInstance(el);
keypad.target;    // the field named by aria-controls, or null
keypad.destroy(); // removes the listeners
```
