From ea13f67f376d64791a5946541e586fea23c0db81 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 11:24:31 +0200
Subject: [PATCH 01/19] feat: add new component 'uui-input-otp' for one time
 codes

---
 package-lock.json                             |  11 +
 packages/uui-input-otp/README.md              |  31 ++
 packages/uui-input-otp/lib/index.ts           |   1 +
 .../lib/uui-input-otp.element.ts              | 295 ++++++++++++++++++
 .../uui-input-otp/lib/uui-input-otp.story.ts  |  76 +++++
 .../uui-input-otp/lib/uui-input-otp.test.ts   |  18 ++
 packages/uui-input-otp/package.json           |  44 +++
 packages/uui-input-otp/rollup.config.js       |   5 +
 packages/uui-input-otp/tsconfig.json          |  17 +
 packages/uui/lib/index.ts                     |   1 +
 10 files changed, 499 insertions(+)
 create mode 100644 packages/uui-input-otp/README.md
 create mode 100644 packages/uui-input-otp/lib/index.ts
 create mode 100644 packages/uui-input-otp/lib/uui-input-otp.element.ts
 create mode 100644 packages/uui-input-otp/lib/uui-input-otp.story.ts
 create mode 100644 packages/uui-input-otp/lib/uui-input-otp.test.ts
 create mode 100644 packages/uui-input-otp/package.json
 create mode 100644 packages/uui-input-otp/rollup.config.js
 create mode 100644 packages/uui-input-otp/tsconfig.json

diff --git a/package-lock.json b/package-lock.json
index 27d85cb4b..a03d894f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11167,6 +11167,10 @@
       "resolved": "packages/uui-input-lock",
       "link": true
     },
+    "node_modules/@umbraco-ui/uui-input-otp": {
+      "resolved": "packages/uui-input-otp",
+      "link": true
+    },
     "node_modules/@umbraco-ui/uui-input-password": {
       "resolved": "packages/uui-input-password",
       "link": true
@@ -33079,6 +33083,13 @@
         "@umbraco-ui/uui-input": "1.8.0"
       }
     },
+    "packages/uui-input-otp": {
+      "version": "0.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "@umbraco-ui/uui-base": "1.8.0"
+      }
+    },
     "packages/uui-input-password": {
       "name": "@umbraco-ui/uui-input-password",
       "version": "1.8.0",
diff --git a/packages/uui-input-otp/README.md b/packages/uui-input-otp/README.md
new file mode 100644
index 000000000..678a69cfc
--- /dev/null
+++ b/packages/uui-input-otp/README.md
@@ -0,0 +1,31 @@
+# uui-input-otp
+
+![npm](https://img.shields.io/npm/v/@umbraco-ui/uui-input-otp?logoColor=%231B264F)
+
+Umbraco style input-otp component.
+
+## Installation
+
+### ES imports
+
+```zsh
+npm i @umbraco-ui/uui-input-otp
+```
+
+Import the registration of `<uui-input-otp>` via:
+
+```javascript
+import '@umbraco-ui/uui-input-otp';
+```
+
+When looking to leverage the `UUIInputOtpElement` base class as a type and/or for extension purposes, do so via:
+
+```javascript
+import { UUIInputOtpElement } from '@umbraco-ui/uui-input-otp';
+```
+
+## Usage
+
+```html
+<uui-input-otp></uui-input-otp>
+```
diff --git a/packages/uui-input-otp/lib/index.ts b/packages/uui-input-otp/lib/index.ts
new file mode 100644
index 000000000..dc4dbf594
--- /dev/null
+++ b/packages/uui-input-otp/lib/index.ts
@@ -0,0 +1 @@
+export * from './uui-input-otp.element';
diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
new file mode 100644
index 000000000..f7579a41e
--- /dev/null
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -0,0 +1,295 @@
+import {
+  LabelMixin,
+  UUIFormControlMixin,
+} from '@umbraco-ui/uui-base/lib/mixins';
+import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
+import { UUIInputEvent, type InputType } from '@umbraco-ui/uui-input/lib';
+
+import { css, html, LitElement } from 'lit';
+import { property, state } from 'lit/decorators.js';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { repeat } from 'lit/directives/repeat.js';
+
+/**
+ * @element uui-input-otp
+ */
+@defineElement('uui-input-otp')
+export class UUIInputOtpElement extends UUIFormControlMixin(
+  LabelMixin('', LitElement),
+  '',
+) {
+  /**
+   * This is a static class field indicating that the element is can be used inside a native form and participate in its events. It may require a polyfill, check support here https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals.  Read more about form controls here https://web.dev/more-capable-form-controls/
+   */
+  static readonly formAssociated = true;
+
+  /**
+   * Accepts only numbers
+   * @default false
+   * @attr
+   */
+  @property({ type: Boolean, attribute: 'integer-only' })
+  set integerOnly(value: boolean) {
+    this.inputMode = value ? 'numeric' : 'text';
+  }
+  get integerOnly() {
+    return this.inputMode === 'numeric';
+  }
+
+  /**
+   * If true, the input will be masked
+   * @default false
+   * @attr
+   */
+  @property({ type: Boolean })
+  set masked(value: boolean) {
+    this._input = value ? 'password' : 'text';
+  }
+  get masked() {
+    return this._input === 'password';
+  }
+
+  /**
+   * Set to true to make this input readonly.
+   * @attr
+   * @default false
+   */
+  @property({ type: Boolean, reflect: true })
+  readonly = false;
+
+  /**
+   * Set to true to disable this input.
+   * @attr
+   * @default false
+   */
+  @property({ type: Boolean, reflect: true })
+  disabled = false;
+
+  /**
+   * Set to true to autofocus this input.
+   * @attr
+   * @default false
+   */
+  @property({ type: Boolean, reflect: true, attribute: 'autofocus' })
+  autoFocus = false;
+
+  @property()
+  placeholder = '';
+
+  set value(value: string) {
+    this._tokens = value.split('');
+    this.requestUpdate('_tokens');
+  }
+
+  /**
+   * The number of characters in the input
+   * @default 6
+   * @attr
+   */
+  @property({ type: Number })
+  length = 6;
+
+  @state()
+  _input: InputType = 'text';
+
+  @state()
+  _tokens: string[] = [];
+
+  constructor() {
+    super();
+    this.addEventListener('paste', this.onPaste);
+  }
+
+  protected getFormElement(): HTMLElement | null | undefined {
+    return this.shadowRoot?.querySelector('.otp-input');
+  }
+
+  protected onFocus(event: FocusEvent) {
+    (event.target as HTMLInputElement)?.select();
+    this.dispatchEvent(event);
+  }
+
+  protected onBlur(event: FocusEvent) {
+    this.dispatchEvent(event);
+  }
+
+  protected onInput(event: InputEvent, index: number) {
+    const target = event.target as HTMLInputElement;
+    this._tokens[index] = target?.value;
+    this.#updateValue();
+
+    if (event.inputType === 'deleteContentBackward') {
+      this.moveToPrev(event);
+    } else if (
+      event.inputType === 'insertText' ||
+      event.inputType === 'deleteContentForward'
+    ) {
+      this.moveToNext(event);
+    }
+  }
+
+  protected onKeyDown(event: KeyboardEvent) {
+    if (event.ctrlKey || event.metaKey) {
+      return;
+    }
+
+    switch (event.code) {
+      case 'ArrowLeft':
+        this.moveToPrev(event);
+        event.preventDefault();
+
+        break;
+
+      case 'ArrowUp':
+      case 'ArrowDown':
+        event.preventDefault();
+
+        break;
+
+      case 'Backspace':
+        if ((event.target as HTMLInputElement)?.value.length === 0) {
+          this.moveToPrev(event);
+          event.preventDefault();
+        }
+
+        break;
+
+      case 'ArrowRight':
+        this.moveToNext(event);
+        event.preventDefault();
+
+        break;
+
+      default:
+        if (
+          (this.integerOnly &&
+            !(Number(event.key) >= 0 && Number(event.key) <= 9)) ||
+          (this._tokens.join('').length >= this.length &&
+            event.code !== 'Delete')
+        ) {
+          event.preventDefault();
+        }
+
+        break;
+    }
+  }
+
+  protected onPaste(event: ClipboardEvent) {
+    const paste = event.clipboardData?.getData('text');
+
+    if (paste?.length) {
+      const pastedCode = paste.substring(0, this.length + 1);
+
+      if (!this.integerOnly || !isNaN(pastedCode as any)) {
+        this._tokens = pastedCode.split('');
+        this.#updateValue();
+      }
+    }
+
+    event.preventDefault();
+  }
+
+  protected moveToPrev(event: Event) {
+    if (!event.target) return;
+    const prevInput = this.findPrevInput(event.target);
+
+    if (prevInput) {
+      prevInput.focus();
+      prevInput.select();
+    }
+  }
+
+  protected moveToNext(event: Event) {
+    if (!event.target) return;
+    const nextInput = this.findNextInput(event.target);
+
+    if (nextInput) {
+      nextInput.focus();
+      nextInput.select();
+    }
+  }
+
+  protected findNextInput(element: EventTarget): HTMLInputElement | null {
+    const nextElement = (element as Element).nextElementSibling;
+
+    if (!nextElement) return null;
+
+    return nextElement.nodeName === 'INPUT'
+      ? (nextElement as HTMLInputElement)
+      : this.findNextInput(nextElement);
+  }
+
+  protected findPrevInput(element: EventTarget): HTMLInputElement | null {
+    const prevElement = (element as Element).previousElementSibling;
+
+    if (!prevElement) return null;
+
+    return prevElement.nodeName === 'INPUT'
+      ? (prevElement as HTMLInputElement)
+      : this.findPrevInput(prevElement);
+  }
+
+  #updateValue() {
+    const newValue = this._tokens.join('');
+    if (this.value !== newValue) {
+      this.value = newValue;
+      this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
+    }
+  }
+
+  protected renderInput(index: number) {
+    return html`
+      <input
+        class="otp-input"
+        .value=${this._tokens[index] || ''}
+        .placeholder=${this.placeholder.charAt(index) || ''}
+        type=${this._input}
+        inputmode=${this.inputMode}
+        ?readonly=${this.readonly}
+        ?disabled=${this.disabled}
+        ?autofocus=${this.autoFocus && index === 0}
+        @input=${(e: InputEvent) => this.onInput(e, index)}
+        @keydown=${this.onKeyDown} />
+    `;
+  }
+
+  render() {
+    return html`
+      <fieldset id="otp-input-group" aria-label=${ifDefined(this.label)}>
+        ${repeat(Array.from({ length: this.length }), (_, i) =>
+          this.renderInput(i),
+        )}
+      </fieldset>
+    `;
+  }
+
+  static styles = [
+    css`
+      :host(:not([pristine]):invalid) .otp-input,
+      :host(:not([pristine])) .otp-input:invalid,
+      /* polyfill support */
+      :host(:not([pristine])[internals-invalid]) .otp-input:invalid {
+        border-color: var(--uui-color-danger);
+      }
+
+      #otp-input-group {
+        display: flex;
+        border: 0; /* Reset fieldset */
+      }
+
+      .otp-input {
+        width: 3em;
+        height: 3em;
+        text-align: center;
+        font-size: 1.5em;
+        margin-right: 0.5em;
+      }
+    `,
+  ];
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'uui-input-otp': UUIInputOtpElement;
+  }
+}
diff --git a/packages/uui-input-otp/lib/uui-input-otp.story.ts b/packages/uui-input-otp/lib/uui-input-otp.story.ts
new file mode 100644
index 000000000..03018578a
--- /dev/null
+++ b/packages/uui-input-otp/lib/uui-input-otp.story.ts
@@ -0,0 +1,76 @@
+import type { Meta, StoryObj } from '@storybook/web-components';
+
+import './uui-input-otp.element';
+import type { UUIInputOtpElement } from './uui-input-otp.element';
+import readme from '../README.md?raw';
+
+const meta: Meta<UUIInputOtpElement> = {
+  id: 'uui-input-otp',
+  title: 'Input Otp',
+  component: 'uui-input-otp',
+  parameters: {
+    readme: { markdown: readme },
+    docs: {
+      source: {
+        code: `<uui-input-otp></uui-input-otp>`,
+      },
+    },
+  },
+};
+
+export default meta;
+type Story = StoryObj<UUIInputOtpElement>;
+
+export const Overview: Story = {};
+
+export const IntegerOnly: Story = {
+  args: {
+    integerOnly: true,
+  },
+  parameters: {
+    docs: {
+      source: {
+        code: `<uui-input-otp integer-only></uui-input-otp>`,
+      },
+    },
+  },
+};
+
+export const Masked: Story = {
+  args: {
+    masked: true,
+  },
+  parameters: {
+    docs: {
+      source: {
+        code: `<uui-input-otp masked></uui-input-otp>`,
+      },
+    },
+  },
+};
+
+export const Required: Story = {
+  args: {
+    required: true,
+  },
+  parameters: {
+    docs: {
+      source: {
+        code: `<uui-input-otp required></uui-input-otp>`,
+      },
+    },
+  },
+};
+
+export const Error: Story = {
+  args: {
+    error: true,
+  },
+  parameters: {
+    docs: {
+      source: {
+        code: `<uui-input-otp error></uui-input-otp>`,
+      },
+    },
+  },
+};
diff --git a/packages/uui-input-otp/lib/uui-input-otp.test.ts b/packages/uui-input-otp/lib/uui-input-otp.test.ts
new file mode 100644
index 000000000..0d4dc59a0
--- /dev/null
+++ b/packages/uui-input-otp/lib/uui-input-otp.test.ts
@@ -0,0 +1,18 @@
+import { html, fixture, expect } from '@open-wc/testing';
+import { UUIInputOtpElement } from './uui-input-otp.element';
+
+describe('UUIInputOtpElement', () => {
+  let element: UUIInputOtpElement;
+
+  beforeEach(async () => {
+    element = await fixture(html` <uui-input-otp></uui-input-otp> `);
+  });
+
+  it('is defined with its own instance', () => {
+    expect(element).to.be.instanceOf(UUIInputOtpElement);
+  });
+
+  it('passes the a11y audit', async () => {
+    await expect(element).shadowDom.to.be.accessible();
+  });
+});
diff --git a/packages/uui-input-otp/package.json b/packages/uui-input-otp/package.json
new file mode 100644
index 000000000..e587c922f
--- /dev/null
+++ b/packages/uui-input-otp/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "@umbraco-ui/uui-input-otp",
+  "version": "0.0.0",
+  "license": "MIT",
+  "keywords": [
+    "Umbraco",
+    "Custom elements",
+    "Web components",
+    "UI",
+    "Lit",
+    "Input Otp"
+  ],
+  "description": "Umbraco UI input-otp component",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/umbraco/Umbraco.UI.git",
+    "directory": "packages/uui-input-otp"
+  },
+  "bugs": {
+    "url": "https://github.com/umbraco/Umbraco.UI/issues"
+  },
+  "main": "./lib/index.js",
+  "module": "./lib/index.js",
+  "types": "./lib/index.d.ts",
+  "type": "module",
+  "customElements": "custom-elements.json",
+  "files": [
+    "lib/**/*.d.ts",
+    "lib/**/*.js",
+    "custom-elements.json"
+  ],
+  "dependencies": {
+    "@umbraco-ui/uui-base": "1.8.0"
+  },
+  "scripts": {
+    "build": "npm run analyze && tsc --build --force && rollup -c rollup.config.js",
+    "clean": "tsc --build --clean && rimraf -g dist lib/*.js lib/**/*.js *.tgz lib/**/*.d.ts custom-elements.json",
+    "analyze": "web-component-analyzer **/*.element.ts --outFile custom-elements.json"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "homepage": "https://uui.umbraco.com/?path=/story/uui-input-otp"
+}
diff --git a/packages/uui-input-otp/rollup.config.js b/packages/uui-input-otp/rollup.config.js
new file mode 100644
index 000000000..34524a90d
--- /dev/null
+++ b/packages/uui-input-otp/rollup.config.js
@@ -0,0 +1,5 @@
+import { UUIProdConfig } from '../rollup-package.config.mjs';
+
+export default UUIProdConfig({
+  entryPoints: ['index'],
+});
diff --git a/packages/uui-input-otp/tsconfig.json b/packages/uui-input-otp/tsconfig.json
new file mode 100644
index 000000000..40d176776
--- /dev/null
+++ b/packages/uui-input-otp/tsconfig.json
@@ -0,0 +1,17 @@
+// Don't edit this file directly. It is generated by /scripts/generate-ts-config.js
+
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./lib",
+    "rootDir": "./lib",
+    "composite": true
+  },
+  "include": ["./**/*.ts"],
+  "exclude": ["./**/*.test.ts", "./**/*.story.ts"],
+  "references": [
+    {
+      "path": "../uui-base"
+    }
+  ]
+}
diff --git a/packages/uui/lib/index.ts b/packages/uui/lib/index.ts
index 7a6290236..6a2d7fe33 100644
--- a/packages/uui/lib/index.ts
+++ b/packages/uui/lib/index.ts
@@ -36,6 +36,7 @@ export * from '@umbraco-ui/uui-icon-registry/lib';
 export * from '@umbraco-ui/uui-icon/lib';
 export * from '@umbraco-ui/uui-input-file/lib';
 export * from '@umbraco-ui/uui-input-lock/lib';
+export * from '@umbraco-ui/uui-input-otp/lib/index.js';
 export * from '@umbraco-ui/uui-input-password/lib';
 export * from '@umbraco-ui/uui-input/lib';
 export * from '@umbraco-ui/uui-keyboard-shortcut/lib';

From 3c26c7fca8c8a77c3b3a42f2d574389765d8828b Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 11:45:30 +0200
Subject: [PATCH 02/19] feat: unify value/tokenisation setter

---
 .../uui-input-otp/lib/uui-input-otp.element.ts  | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index f7579a41e..454d58fbf 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -78,7 +78,9 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
 
   set value(value: string) {
     this._tokens = value.split('');
-    this.requestUpdate('_tokens');
+
+    super.value = value;
+    this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
   }
 
   /**
@@ -116,7 +118,7 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   protected onInput(event: InputEvent, index: number) {
     const target = event.target as HTMLInputElement;
     this._tokens[index] = target?.value;
-    this.#updateValue();
+    this.value = this._tokens.join('');
 
     if (event.inputType === 'deleteContentBackward') {
       this.moveToPrev(event);
@@ -181,8 +183,7 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
       const pastedCode = paste.substring(0, this.length + 1);
 
       if (!this.integerOnly || !isNaN(pastedCode as any)) {
-        this._tokens = pastedCode.split('');
-        this.#updateValue();
+        this.value = pastedCode;
       }
     }
 
@@ -229,14 +230,6 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
       : this.findPrevInput(prevElement);
   }
 
-  #updateValue() {
-    const newValue = this._tokens.join('');
-    if (this.value !== newValue) {
-      this.value = newValue;
-      this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
-    }
-  }
-
   protected renderInput(index: number) {
     return html`
       <input

From 767b6931ec77816513ba5c7c4ca52f308625314f Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 11:49:12 +0200
Subject: [PATCH 03/19] docs: add jsdoc for placeholder

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index 454d58fbf..b6b0aac32 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -73,6 +73,12 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @property({ type: Boolean, reflect: true, attribute: 'autofocus' })
   autoFocus = false;
 
+  /**
+   * Add a placeholder to the inputs in the group
+   * @remark The placeholder should be a string with the same length as the `length` attribute and will be distributed to each input in the group
+   * @attr
+   * @default ''
+   */
   @property()
   placeholder = '';
 

From 281209a82e4ce2e295f32ef68bd19428196bd172 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 11:55:42 +0200
Subject: [PATCH 04/19] feat: add autocomplete attribute

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index b6b0aac32..dcd6cfbce 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -82,6 +82,16 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @property()
   placeholder = '';
 
+  /**
+   * The autocomplete attribute specifies whether or not an input field should have autocomplete enabled.
+   * @remark Set the autocomplete attribute to "one-time-code" to enable autofill of one-time-code inputs
+   * @attr autocomplete
+   * @default ''
+   * @type {string}
+   */
+  @property({ type: String, reflect: true, attribute: 'autocomplete' })
+  autoComplete?: string;
+
   set value(value: string) {
     this._tokens = value.split('');
 

From cd9751db69931aa9e109e2a6bf494e83bd388287 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 11:57:29 +0200
Subject: [PATCH 05/19] feat: rename "autoComplete" to "autocomplete"

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index dcd6cfbce..77989bb19 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -85,12 +85,12 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   /**
    * The autocomplete attribute specifies whether or not an input field should have autocomplete enabled.
    * @remark Set the autocomplete attribute to "one-time-code" to enable autofill of one-time-code inputs
-   * @attr autocomplete
+   * @attr
    * @default ''
    * @type {string}
    */
-  @property({ type: String, reflect: true, attribute: 'autocomplete' })
-  autoComplete?: string;
+  @property({ type: String, reflect: true })
+  autocomplete?: string;
 
   set value(value: string) {
     this._tokens = value.split('');

From a50bbe56ee908648e6c56f599a6843f4daaaffe7 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 12:16:00 +0200
Subject: [PATCH 06/19] docs: add storybook mdx for automatic otp

---
 .../stories/uui-input-otp.automatic-otp.mdx   | 48 +++++++++++++++++++
 .../{lib => stories}/uui-input-otp.story.ts   | 20 ++++++--
 2 files changed, 65 insertions(+), 3 deletions(-)
 create mode 100644 packages/uui-input-otp/stories/uui-input-otp.automatic-otp.mdx
 rename packages/uui-input-otp/{lib => stories}/uui-input-otp.story.ts (75%)

diff --git a/packages/uui-input-otp/stories/uui-input-otp.automatic-otp.mdx b/packages/uui-input-otp/stories/uui-input-otp.automatic-otp.mdx
new file mode 100644
index 000000000..733a31945
--- /dev/null
+++ b/packages/uui-input-otp/stories/uui-input-otp.automatic-otp.mdx
@@ -0,0 +1,48 @@
+import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks';
+
+import * as OtpStories from './uui-input-otp.story';
+
+<Meta title="Inputs/Input OTP/Automatic Code Input" />
+
+# OTP With Autocomplete
+
+The OTP input field can be used with an autocomplete feature. This feature is useful to utilise the browser's autocomplete feature to autofill the OTP code.
+
+## Usage
+
+First you need to configure the input field to autocomplete the OTP code. This can be done by setting the `autocomplete` attribute to `one-time-code`.
+
+<Canvas of={OtpStories.AutocompleteOtp} />
+
+This allows browsers that support the `one-time-code` autocomplete to autofill the OTP code. It is mostly used in Safari.
+
+Safari does not yet support the Web OTP API, so this is a workaround to autofill the OTP code.
+
+Other browsers that support the Web OTP API will not autofill the OTP code, as the Web OTP API is more secure than the `one-time-code` autocomplete.
+
+Therefore, we need to listen for Web OTP API events to autofill the OTP code in browsers that support it.
+We can add the following code to listen for the Web OTP API events:
+
+```js
+// Feature detect the Web OTP API
+if ('OTPCredential' in window) {
+  // Request the OTP credential
+  navigator.credentials
+    .get({
+      otp: { transport: ['sms'] }, // This can be 'sms' or other transports
+    })
+    .then(cred => {
+      // Locate the OTP input from before
+      const otpInput = document.getElementById('otp-input');
+
+      // Autofill the OTP input if the credential is available and the OTP input is found
+      if (cred && otpInput) {
+        this.otpInput.value = cred.id;
+      }
+    })
+    .catch(() => {
+      // optionally catch errors, which could mean the OTP retrieval has timed out
+      // in most cases, it is not necessary to handle this error
+    });
+}
+```
diff --git a/packages/uui-input-otp/lib/uui-input-otp.story.ts b/packages/uui-input-otp/stories/uui-input-otp.story.ts
similarity index 75%
rename from packages/uui-input-otp/lib/uui-input-otp.story.ts
rename to packages/uui-input-otp/stories/uui-input-otp.story.ts
index 03018578a..c48aa9635 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.story.ts
+++ b/packages/uui-input-otp/stories/uui-input-otp.story.ts
@@ -1,12 +1,12 @@
 import type { Meta, StoryObj } from '@storybook/web-components';
 
-import './uui-input-otp.element';
-import type { UUIInputOtpElement } from './uui-input-otp.element';
+import '../lib/uui-input-otp.element';
+import type { UUIInputOtpElement } from '../lib/uui-input-otp.element';
 import readme from '../README.md?raw';
 
 const meta: Meta<UUIInputOtpElement> = {
   id: 'uui-input-otp',
-  title: 'Input Otp',
+  title: 'Inputs/Input Otp',
   component: 'uui-input-otp',
   parameters: {
     readme: { markdown: readme },
@@ -74,3 +74,17 @@ export const Error: Story = {
     },
   },
 };
+
+export const AutocompleteOtp: Story = {
+  name: 'Autocomplete OTP',
+  args: {
+    autocomplete: 'one-time-code',
+  },
+  parameters: {
+    docs: {
+      source: {
+        code: `<uui-input-otp autocomplete="one-time-code"></uui-input-otp>`,
+      },
+    },
+  },
+};

From 766fe1d77de35c4419c2af41c70d00435ff3d526 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 14:05:54 +0200
Subject: [PATCH 07/19] chore: make styles property readonly

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index 77989bb19..3f6ab6c7d 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -272,7 +272,7 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
     `;
   }
 
-  static styles = [
+  static readonly styles = [
     css`
       :host(:not([pristine]):invalid) .otp-input,
       :host(:not([pristine])) .otp-input:invalid,

From a748553184cc3b8c55d95f9e82916640c976d4fe Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 14:06:13 +0200
Subject: [PATCH 08/19] docs: find a better name than 'Error'

---
 packages/uui-input-otp/stories/uui-input-otp.story.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/uui-input-otp/stories/uui-input-otp.story.ts b/packages/uui-input-otp/stories/uui-input-otp.story.ts
index c48aa9635..a8cf61bd7 100644
--- a/packages/uui-input-otp/stories/uui-input-otp.story.ts
+++ b/packages/uui-input-otp/stories/uui-input-otp.story.ts
@@ -62,7 +62,7 @@ export const Required: Story = {
   },
 };
 
-export const Error: Story = {
+export const WithError: Story = {
   args: {
     error: true,
   },

From 4ff950cd4f0705a9cb65625390333e18574d5a1b Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 14:56:13 +0200
Subject: [PATCH 09/19] chore: sort props and states

---
 .../uui-input-otp/lib/uui-input-otp.element.ts    | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index 3f6ab6c7d..63c883332 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -92,13 +92,6 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @property({ type: String, reflect: true })
   autocomplete?: string;
 
-  set value(value: string) {
-    this._tokens = value.split('');
-
-    super.value = value;
-    this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
-  }
-
   /**
    * The number of characters in the input
    * @default 6
@@ -113,6 +106,13 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @state()
   _tokens: string[] = [];
 
+  set value(value: string) {
+    this._tokens = value.split('');
+
+    super.value = value;
+    this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
+  }
+
   constructor() {
     super();
     this.addEventListener('paste', this.onPaste);
@@ -254,6 +254,7 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
         .placeholder=${this.placeholder.charAt(index) || ''}
         type=${this._input}
         inputmode=${this.inputMode}
+        .ariaLabel=${this.label}
         ?readonly=${this.readonly}
         ?disabled=${this.disabled}
         ?autofocus=${this.autoFocus && index === 0}

From 1a585e26d392c8fe7756918fea179c865e9cbd49 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 14:56:38 +0200
Subject: [PATCH 10/19] chore: put custom props at the top

---
 .../uui-input-otp/lib/uui-input-otp.element.ts   | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index 63c883332..a2f3e25f4 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -49,6 +49,14 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
     return this._input === 'password';
   }
 
+  /**
+   * The number of characters in the input
+   * @default 6
+   * @attr
+   */
+  @property({ type: Number })
+  length = 6;
+
   /**
    * Set to true to make this input readonly.
    * @attr
@@ -92,14 +100,6 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @property({ type: String, reflect: true })
   autocomplete?: string;
 
-  /**
-   * The number of characters in the input
-   * @default 6
-   * @attr
-   */
-  @property({ type: Number })
-  length = 6;
-
   @state()
   _input: InputType = 'text';
 

From 9d09c704ea676343b3c51cc58ea455f733eb6b6e Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 15:03:06 +0200
Subject: [PATCH 11/19] fix: add an itemLabelTemplate to allow a11y for the
 individual elements

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index a2f3e25f4..eebbe5317 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -57,6 +57,12 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @property({ type: Number })
   length = 6;
 
+  /**
+   * The template for the item label
+   */
+  @property({ type: String, attribute: false })
+  itemLabelTemplate = (index: number) => `Character number ${index + 1}`;
+
   /**
    * Set to true to make this input readonly.
    * @attr
@@ -250,14 +256,14 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
     return html`
       <input
         class="otp-input"
+        type=${this._input}
         .value=${this._tokens[index] || ''}
         .placeholder=${this.placeholder.charAt(index) || ''}
-        type=${this._input}
-        inputmode=${this.inputMode}
-        .ariaLabel=${this.label}
+        .inputMode=${this.inputMode}
         ?readonly=${this.readonly}
         ?disabled=${this.disabled}
         ?autofocus=${this.autoFocus && index === 0}
+        aria-label=${this.itemLabelTemplate(index)}
         @input=${(e: InputEvent) => this.onInput(e, index)}
         @keydown=${this.onKeyDown} />
     `;

From 598ebea1f07c4e333c6edb0f922046c5b4a690aa Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 15:03:17 +0200
Subject: [PATCH 12/19] test: add label to elements

---
 packages/uui-input-otp/lib/uui-input-otp.test.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.test.ts b/packages/uui-input-otp/lib/uui-input-otp.test.ts
index 0d4dc59a0..063ff059e 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.test.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.test.ts
@@ -5,7 +5,9 @@ describe('UUIInputOtpElement', () => {
   let element: UUIInputOtpElement;
 
   beforeEach(async () => {
-    element = await fixture(html` <uui-input-otp></uui-input-otp> `);
+    element = await fixture(html`
+      <uui-input-otp label="otp input"></uui-input-otp>
+    `);
   });
 
   it('is defined with its own instance', () => {

From 0a2ff2dff0321bd98b785b398fc9a98c984fb6d7 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 15:57:48 +0200
Subject: [PATCH 13/19] fix: add getter for value

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index eebbe5317..c520ec23f 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -118,6 +118,9 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
     super.value = value;
     this.dispatchEvent(new UUIInputEvent(UUIInputEvent.CHANGE));
   }
+  get value() {
+    return super.value.toString();
+  }
 
   constructor() {
     super();

From e84a74cdb06ce8ecb255e8acf397b0c2904d68ac Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 15:58:18 +0200
Subject: [PATCH 14/19] fix: bind paste event

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index c520ec23f..d757c2abe 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -124,7 +124,7 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
 
   constructor() {
     super();
-    this.addEventListener('paste', this.onPaste);
+    this.addEventListener('paste', this.onPaste.bind(this));
   }
 
   protected getFormElement(): HTMLElement | null | undefined {

From 73bcb35cf1efe1bcd86f168796839d15d908cfd1 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 15:58:39 +0200
Subject: [PATCH 15/19] add validation for tooShort

---
 .../uui-input-otp/lib/uui-input-otp.element.ts     | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index d757c2abe..7cf50ac8c 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -106,6 +106,14 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   @property({ type: String, reflect: true })
   autocomplete?: string;
 
+  /**
+   * Min length validation message.
+   * @attr
+   * @default
+   */
+  @property({ type: String, attribute: 'minlength-message' })
+  minlengthMessage = 'This field need more characters';
+
   @state()
   _input: InputType = 'text';
 
@@ -125,6 +133,12 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   constructor() {
     super();
     this.addEventListener('paste', this.onPaste.bind(this));
+
+    this.addValidator(
+      'tooShort',
+      () => this.minlengthMessage,
+      () => !!this.length && String(this.value).length < this.length,
+    );
   }
 
   protected getFormElement(): HTMLElement | null | undefined {

From 5f6023d6c9b4a7510a86ccbc2cac6e396dc4c131 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 15:59:24 +0200
Subject: [PATCH 16/19] test: add tests for element

---
 .../uui-input-otp/lib/uui-input-otp.test.ts   | 132 ++++++++++++++++++
 1 file changed, 132 insertions(+)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.test.ts b/packages/uui-input-otp/lib/uui-input-otp.test.ts
index 063ff059e..1c962c56a 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.test.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.test.ts
@@ -17,4 +17,136 @@ describe('UUIInputOtpElement', () => {
   it('passes the a11y audit', async () => {
     await expect(element).shadowDom.to.be.accessible();
   });
+
+  describe('properties', () => {
+    it('has a default length of 6', () => {
+      expect(element.length).to.equal(6);
+      expect(element.shadowRoot?.querySelectorAll('input').length).to.equal(6);
+    });
+
+    it('can set the length', async () => {
+      element.length = 4;
+      await element.updateComplete;
+      expect(element.shadowRoot?.querySelectorAll('input').length).to.equal(4);
+    });
+
+    it('can set integerOnly', async () => {
+      element.integerOnly = true;
+      await element.updateComplete;
+      expect(element.inputMode).to.equal('numeric');
+
+      element.integerOnly = false;
+      await element.updateComplete;
+      expect(element.inputMode).to.equal('text');
+    });
+
+    it('can set masked', async () => {
+      element.masked = true;
+      await element.updateComplete;
+      expect(element._input).to.equal('password');
+
+      element.masked = false;
+      await element.updateComplete;
+      expect(element._input).to.equal('text');
+    });
+
+    it('can set readonly', async () => {
+      element.readonly = true;
+      await element.updateComplete;
+      expect(element.hasAttribute('readonly')).to.be.true;
+
+      element.readonly = false;
+      await element.updateComplete;
+      expect(element.hasAttribute('readonly')).to.be.false;
+    });
+
+    it('can set disabled', async () => {
+      element.disabled = true;
+      await element.updateComplete;
+      expect(element.hasAttribute('disabled')).to.be.true;
+
+      element.disabled = false;
+      await element.updateComplete;
+      expect(element.hasAttribute('disabled')).to.be.false;
+    });
+
+    it('can set autofocus', async () => {
+      element.autofocus = true;
+      await element.updateComplete;
+      expect(element.hasAttribute('autofocus')).to.be.true;
+
+      element.autofocus = false;
+      await element.updateComplete;
+      expect(element.hasAttribute('autofocus')).to.be.false;
+    });
+
+    it('can set required', async () => {
+      element.required = true;
+      await element.updateComplete;
+      expect(element.hasAttribute('required')).to.be.true;
+
+      element.required = false;
+      await element.updateComplete;
+      expect(element.hasAttribute('required')).to.be.false;
+    });
+
+    it('can set error', async () => {
+      element.error = true;
+      await element.updateComplete;
+      expect(element.hasAttribute('error')).to.be.true;
+
+      element.error = false;
+      await element.updateComplete;
+      expect(element.hasAttribute('error')).to.be.false;
+    });
+
+    it('can set autocomplete', async () => {
+      element.autocomplete = 'one-time-code';
+      await element.updateComplete;
+      expect(element.getAttribute('autocomplete')).to.equal('one-time-code');
+    });
+  });
+
+  describe('logic', () => {
+    it('can distribute a value', async () => {
+      element.value = '123456';
+      await element.updateComplete;
+      expect(element.shadowRoot?.querySelectorAll('input')[0].value).to.equal(
+        '1',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[1].value).to.equal(
+        '2',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[2].value).to.equal(
+        '3',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[3].value).to.equal(
+        '4',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[4].value).to.equal(
+        '5',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[5].value).to.equal(
+        '6',
+      );
+    });
+
+    it('can distribute a value with a different length', async () => {
+      element.length = 4;
+      element.value = '123456';
+      await element.updateComplete;
+      expect(element.shadowRoot?.querySelectorAll('input')[0].value).to.equal(
+        '1',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[1].value).to.equal(
+        '2',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[2].value).to.equal(
+        '3',
+      );
+      expect(element.shadowRoot?.querySelectorAll('input')[3].value).to.equal(
+        '4',
+      );
+    });
+  });
 });

From 42647a365a204832a20362a072bb537c769afc73 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 16:04:43 +0200
Subject: [PATCH 17/19] test: avoid duplication

---
 .../uui-input-otp/lib/uui-input-otp.test.ts   | 49 +++++++------------
 1 file changed, 19 insertions(+), 30 deletions(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.test.ts b/packages/uui-input-otp/lib/uui-input-otp.test.ts
index 1c962c56a..7b2611130 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.test.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.test.ts
@@ -111,42 +111,31 @@ describe('UUIInputOtpElement', () => {
     it('can distribute a value', async () => {
       element.value = '123456';
       await element.updateComplete;
-      expect(element.shadowRoot?.querySelectorAll('input')[0].value).to.equal(
-        '1',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[1].value).to.equal(
-        '2',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[2].value).to.equal(
-        '3',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[3].value).to.equal(
-        '4',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[4].value).to.equal(
-        '5',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[5].value).to.equal(
-        '6',
-      );
+      const inputs = element.shadowRoot?.querySelectorAll('input');
+      if (!inputs) {
+        throw new Error('inputs not found');
+      }
+      expect(inputs[0].value).to.equal('1');
+      expect(inputs[1].value).to.equal('2');
+      expect(inputs[2].value).to.equal('3');
+      expect(inputs[3].value).to.equal('4');
+      expect(inputs[4].value).to.equal('5');
+      expect(inputs[5].value).to.equal('6');
     });
 
     it('can distribute a value with a different length', async () => {
       element.length = 4;
       element.value = '123456';
       await element.updateComplete;
-      expect(element.shadowRoot?.querySelectorAll('input')[0].value).to.equal(
-        '1',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[1].value).to.equal(
-        '2',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[2].value).to.equal(
-        '3',
-      );
-      expect(element.shadowRoot?.querySelectorAll('input')[3].value).to.equal(
-        '4',
-      );
+
+      const inputs = element.shadowRoot?.querySelectorAll('input');
+      if (!inputs) {
+        throw new Error('inputs not found');
+      }
+      expect(inputs[0].value).to.equal('1');
+      expect(inputs[1].value).to.equal('2');
+      expect(inputs[2].value).to.equal('3');
+      expect(inputs[3].value).to.equal('4');
     });
   });
 });

From 14ff014cf0233e1a2a6f3b74efff4454a279ea99 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 16:11:00 +0200
Subject: [PATCH 18/19] fix: getformelement

---
 packages/uui-input-otp/lib/uui-input-otp.element.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.element.ts b/packages/uui-input-otp/lib/uui-input-otp.element.ts
index 7cf50ac8c..20fdaa5e4 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.element.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.element.ts
@@ -142,7 +142,7 @@ export class UUIInputOtpElement extends UUIFormControlMixin(
   }
 
   protected getFormElement(): HTMLElement | null | undefined {
-    return this.shadowRoot?.querySelector('.otp-input');
+    return this;
   }
 
   protected onFocus(event: FocusEvent) {

From a13d41bb58a7aa0c68538fba2aee018aa3c10934 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 21 Jun 2024 16:11:23 +0200
Subject: [PATCH 19/19] test: formatting

---
 packages/uui-input-otp/lib/uui-input-otp.test.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/packages/uui-input-otp/lib/uui-input-otp.test.ts b/packages/uui-input-otp/lib/uui-input-otp.test.ts
index 7b2611130..2a472706c 100644
--- a/packages/uui-input-otp/lib/uui-input-otp.test.ts
+++ b/packages/uui-input-otp/lib/uui-input-otp.test.ts
@@ -115,6 +115,7 @@ describe('UUIInputOtpElement', () => {
       if (!inputs) {
         throw new Error('inputs not found');
       }
+
       expect(inputs[0].value).to.equal('1');
       expect(inputs[1].value).to.equal('2');
       expect(inputs[2].value).to.equal('3');
@@ -132,6 +133,7 @@ describe('UUIInputOtpElement', () => {
       if (!inputs) {
         throw new Error('inputs not found');
       }
+
       expect(inputs[0].value).to.equal('1');
       expect(inputs[1].value).to.equal('2');
       expect(inputs[2].value).to.equal('3');