From dee488fbe65ea25398a8b4bfc9f7435a7db27674 Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sat, 17 Feb 2024 22:59:51 -0300
Subject: [PATCH 1/8] test(playwright): add non-headless slow mode

---
 package.json                                  |   3 +-
 playwright.config.ts                          |  12 +-
 .../(pages)/(home)/_components/showcase.tsx   | 116 ++++++++----------
 3 files changed, 58 insertions(+), 73 deletions(-)

diff --git a/package.json b/package.json
index ebf99a7..0ac7560 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
     "dev:website": "turbo run dev --filter=website...",
     "dev:test": "turbo run dev --filter=test...",
     "format": "prettier --write .",
-    "test": "playwright test"
+    "test": "playwright test",
+    "test:slow": "WINDOWED_TESTS=true playwright test"
   },
   "keywords": [
     "react",
diff --git a/playwright.config.ts b/playwright.config.ts
index bd7631b..08919b6 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig, devices } from '@playwright/test';
+import { defineConfig, devices } from '@playwright/test'
 
 /**
  * Read environment variables from file.
@@ -21,11 +21,11 @@ export default defineConfig({
     timeout: 5000,
   },
   /* Run tests in files in parallel */
-  fullyParallel: true,
+  fullyParallel: process.env.WINDOWED_TESTS ? false : true,
   /* Fail the build on CI if you accidentally left test.only in the source code. */
   forbidOnly: !!process.env.CI,
   /* Retry on CI only */
-  retries: process.env.CI ? 2 : 0,
+  retries: process.env.CI ? 1 : 0,
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
@@ -34,6 +34,10 @@ export default defineConfig({
   use: {
     trace: 'on-first-retry',
     baseURL: 'http://localhost:3039',
+    headless: process.env.WINDOWED_TESTS ? false : true,
+    launchOptions: {
+      slowMo: process.env.WINDOWED_TESTS ? 500 : 0,
+    },
   },
   webServer: {
     command: 'npm run dev',
@@ -52,4 +56,4 @@ export default defineConfig({
   //     use: { ...devices['Pixel 5'] },
   //   },
   // ],
-});
+})
diff --git a/website/src/app/(pages)/(home)/_components/showcase.tsx b/website/src/app/(pages)/(home)/_components/showcase.tsx
index 9c91f66..3c51437 100644
--- a/website/src/app/(pages)/(home)/_components/showcase.tsx
+++ b/website/src/app/(pages)/(home)/_components/showcase.tsx
@@ -1,46 +1,31 @@
 'use client'
 
 import React from 'react'
-import { useForm, Controller } from 'react-hook-form'
 
 import { OTPInput } from 'otp-input'
 import { cn } from '@/lib/utils'
 
-type FormValues = {
-  otp: string
-}
-
 export function Showcase({ className, ...props }: { className?: string }) {
-  const [formDisabled, setFormDisabled] = React.useState(false)
-
-  const {
-    control,
-    handleSubmit,
-    setFocus,
-    reset,
-    setValue,
-    formState,
-    register,
-  } = useForm<FormValues>({
-    defaultValues: {
-      otp: '12',
-    },
-    disabled: formDisabled,
-  })
+  const [value, setValue] = React.useState('')
+  const inputRef = React.useRef<HTMLInputElement>(null)
 
   React.useEffect(() => {
     setTimeout(() => {
-      setFocus('otp')
+      inputRef.current?.focus()
     }, 2000)
-  }, [setFocus])
+  }, [])
 
-  function onSubmit(values: FormValues) {
-    if (values.otp !== '123   ') {
-      window.alert('Invalid OTP')
-      reset()
+  function onSubmit(e: React.FormEvent<HTMLFormElement>) {
+    e.preventDefault()
+
+    if (value === '123456') {
+      // Easter egg...
       return
     }
-    window.alert('Valid OTP')
+
+    const firstDemoInput =
+      document.querySelector<HTMLInputElement>('#first-demo-input')
+    firstDemoInput!.focus()
   }
 
   return (
@@ -49,49 +34,44 @@ export function Showcase({ className, ...props }: { className?: string }) {
         'mx-auto flex max-w-[980px] justify-center pt-6 pb-4',
         className,
       )}
-      onSubmit={handleSubmit(onSubmit)}
+      onSubmit={onSubmit}
     >
-      <Controller
-        name="otp"
-        control={control}
-        render={({ field }) => (
-          <OTPInput
-            {...field}
-            containerClassName={cn('group flex items-center')}
-            maxLength={6}
-            // regexp={null} // Allow everything
-            render={({ slots, isFocused }) => (
-              <>
-                <div className="flex">
-                  {slots.slice(0, 3).map((slot, idx) => (
-                    <Slot
-                      isFocused={isFocused}
-                      key={idx}
-                      slotChar={slot.char}
-                      isSlotActive={slot.isActive}
-                      animateIdx={idx}
-                    />
-                  ))}
-                </div>
+      <OTPInput
+        value={value}
+        onChange={setValue}
+        containerClassName={cn('group flex items-center')}
+        maxLength={6}
+        // regexp={null} // Allow everything
+        render={({ slots, isFocused }) => (
+          <>
+            <div className="flex">
+              {slots.slice(0, 3).map((slot, idx) => (
+                <Slot
+                  isFocused={isFocused}
+                  key={idx}
+                  slotChar={slot.char}
+                  isSlotActive={slot.isActive}
+                  animateIdx={idx}
+                />
+              ))}
+            </div>
 
-                {/* Layout inspired by Stripe */}
-                <div className="flex w-10 justify-center items-center">
-                  <div className="w-3 h-1 rounded-full bg-border"></div>
-                </div>
+            {/* Layout inspired by Stripe */}
+            <div className="flex w-10 justify-center items-center">
+              <div className="w-3 h-1 rounded-full bg-border"></div>
+            </div>
 
-                <div className="flex">
-                  {slots.slice(3).map((slot, idx) => (
-                    <Slot
-                      isFocused={isFocused}
-                      key={idx}
-                      slotChar={slot.char}
-                      isSlotActive={slot.isActive}
-                    />
-                  ))}
-                </div>
-              </>
-            )}
-          />
+            <div className="flex">
+              {slots.slice(3).map((slot, idx) => (
+                <Slot
+                  isFocused={isFocused}
+                  key={idx}
+                  slotChar={slot.char}
+                  isSlotActive={slot.isActive}
+                />
+              ))}
+            </div>
+          </>
         )}
       />
     </form>

From 413197a6ccfcfa7cb8dd002c836d725e9f54f2ba Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 19:28:11 -0300
Subject: [PATCH 2/8] feat(v3): create faster engine

---
 .prettierrc.json                              |  11 +
 package.json                                  |   2 +-
 src/index.ts                                  |   2 +
 src/index.tsx                                 | 533 ------------------
 src/input.tsx                                 | 352 ++++++++++++
 src/regexp.tsx                                |   3 +
 src/sync-timeouts.ts                          |   9 +
 src/types.ts                                  |  31 +
 website/next.config.mjs                       |   4 +-
 .../(pages)/(home)/_components/showcase.tsx   |   7 +-
 website/src/app/(pages)/(home)/page.tsx       |  16 +-
 11 files changed, 424 insertions(+), 546 deletions(-)
 create mode 100644 .prettierrc.json
 create mode 100644 src/index.ts
 delete mode 100644 src/index.tsx
 create mode 100644 src/input.tsx
 create mode 100644 src/regexp.tsx
 create mode 100644 src/sync-timeouts.ts
 create mode 100644 src/types.ts

diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..fa1b7ff
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,11 @@
+{
+  "resolveGlobalModules": true,
+  "tabWidth": 2,
+  "printWidth": 80,
+  "useTabs": false,
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "all",
+  "arrowParens": "avoid",
+  "endOfLine": "lf"
+}
diff --git a/package.json b/package.json
index 0ac7560..fe9a08e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "otp-input",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "description": "One-time password input component for React.",
   "main": "index.js",
   "module": "./dist/index.mjs",
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..00f2393
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,2 @@
+export * from './input'
+export * from './regexp'
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
deleted file mode 100644
index 12925f7..0000000
--- a/src/index.tsx
+++ /dev/null
@@ -1,533 +0,0 @@
-'use client'
-
-import * as React from 'react'
-
-const SPECIAL_KEYS = ['Meta', 'Alt', 'Control', 'Tab', 'Z']
-
-interface OTPInputRenderProps {
-  slots: { isActive: boolean; char: string | null }[]
-  isFocused: boolean
-  isHovering: boolean
-}
-interface OTPInputProps {
-  id?: string
-  name?: string
-  onBlur?: (...args: any[]) => unknown
-  disabled?: boolean
-
-  value: string
-  onChange: (value: string) => unknown
-
-  maxLength: number
-  regexp?: RegExp | null
-  inputMode?: 'numeric' | 'text'
-  allowSpaces?: boolean
-  allowNavigation?: boolean
-
-  autoFocus?: boolean
-
-  onComplete?: (...args: any[]) => unknown
-
-  render: (props: OTPInputRenderProps) => React.ReactElement
-
-  containerClassName?: string
-}
-export const OTPInput = React.forwardRef<HTMLDivElement, OTPInputProps>(
-  (
-    {
-      id,
-      name,
-      onBlur,
-      disabled,
-
-      value: _value,
-      onChange,
-
-      maxLength,
-      regexp = /^\d+$/,
-      inputMode = 'numeric',
-      allowSpaces = false,
-      allowNavigation = true,
-
-      autoFocus = false,
-
-      onComplete,
-
-      render,
-
-      containerClassName,
-
-      ...props
-    },
-    ref,
-  ) => {
-    // console.count('rerender')
-
-    // TODO: refactor like https://github.com/emilkowalski/vaul/blob/main/src/index.tsx
-    /** Logic */
-    const value = typeof _value === 'string' ? _value : ''
-
-    const inputRef = React.useRef<HTMLInputElement>(null)
-    React.useImperativeHandle(
-      ref,
-      () => {
-        const el = inputRef.current as HTMLInputElement
-
-        // TODO: support `otp` SMS transport
-        // if ('OTPCredential' in window) {
-        //   const ac = new AbortController()
-        //   navigator.credentials
-        //     .get({
-        //       ...{ otp: { transport: ['sms'] } },
-        //       signal: ac.signal,
-        //     })
-        //     .then(otp => {
-        //       input.value = otp.code
-        //     })
-        //     .catch(err => {
-        //       console.log(err)
-        //     })
-        // }
-
-        const _select = el.select.bind(el)
-        el.select = () => {
-          if (!allowNavigation) {
-            // Cannot select all chars as navigation is disabled
-            return
-          }
-
-          _select()
-          // Proxy to update the caretData
-          setSelectionMirror([0, el.value.length])
-        }
-        return el
-      },
-      [],
-    )
-
-    const [isHovering, setIsHovering] = React.useState<boolean>(false)
-    const [isFocused, setIsFocused] = React.useState<boolean>(false)
-
-    React.useEffect(() => {
-      if (!autoFocus || !inputRef.current) {
-        return
-      }
-
-      setTimeout(() => {
-        const isAutoFocused = document.activeElement === inputRef.current
-
-        if (isAutoFocused) {
-          setIsFocused(true)
-          onInputSelect({
-            overrideStart: inputRef.current.selectionStart,
-            overrideEnd: inputRef.current.selectionEnd,
-          })
-        }
-      }, 1_0)
-    }, [autoFocus])
-
-    React.useEffect(() => {
-      if (value.length === maxLength) {
-        onComplete?.()
-      }
-    }, [value, onComplete])
-
-    React.useEffect(() => {
-      if (disabled) {
-        onInputBlur()
-      }
-    }, [disabled])
-
-    const [selectionMirror, setSelectionMirror] = React.useState<
-      [number | null, number | null]
-    >([null, null])
-
-    // TODO: rename to `mutateInputSelectionAndSyncCaretData`
-    const mutateInputSelectionAndUpdateMirror = React.useCallback(
-      (start: number | null, end: number | null) => {
-        if (!inputRef.current) {
-          return
-        }
-
-        if (start === null || end === null) {
-          setSelectionMirror([start, end])
-          inputRef.current.setSelectionRange(start, end)
-          return
-        }
-
-        const n = start === maxLength ? maxLength - 1 : start
-
-        const _start = Math.min(n, maxLength - 1)
-        const _end = n + 1
-
-        // mutate input selection
-        inputRef.current.setSelectionRange(_start, _end)
-        // force UI update
-        setSelectionMirror([_start, _end])
-      },
-      [selectionMirror, maxLength],
-    )
-
-    const onInputSelect = React.useCallback(
-      (params: {
-        e?: React.SyntheticEvent<HTMLInputElement>
-        overrideStart?: number | null
-        overrideEnd?: number | null
-      }) => {
-        if (!inputRef.current) {
-          return
-        }
-
-        if (
-          !params.e &&
-          params.overrideStart === undefined &&
-          params.overrideEnd === undefined
-        ) {
-          return
-        }
-
-        const start =
-          params.overrideStart === undefined
-            ? params.e!.currentTarget.selectionStart
-            : params.overrideStart
-        const end =
-          params.overrideEnd === undefined
-            ? params.e!.currentTarget.selectionEnd
-            : params.overrideEnd
-
-        // Check if there is no selection range
-        if (start === end && start !== null) {
-          mutateInputSelectionAndUpdateMirror(start, end)
-          return
-        }
-
-        if (selectionMirror[0] === start && selectionMirror[1] === end) {
-          return
-        }
-        setSelectionMirror([start, end])
-      },
-      [selectionMirror, mutateInputSelectionAndUpdateMirror],
-    )
-
-    // Workaround to track the input's  selection even if Meta key is pressed
-    // This was necessary due to the input `onSelect` only being called either 1. before Meta key is pressed or 2. after Meta key is released
-    // TODO: track `Meta` and `Tab`
-    const [isSpecialPressed, setIsSpecialPressed] =
-      React.useState<boolean>(false)
-
-    function syncTimeout() {
-      return [5, 10, 20, 50].map(delayMs =>
-        setTimeout(() => {
-          if (!inputRef.current) {
-            return
-          }
-
-          onInputSelect({
-            overrideStart: inputRef.current.selectionStart,
-            overrideEnd: inputRef.current.selectionEnd,
-          })
-        }, delayMs),
-      )
-    }
-
-    function onInputKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
-      if (SPECIAL_KEYS.indexOf(e.key) !== -1) {
-        setIsSpecialPressed(true)
-
-        syncTimeout()
-      }
-
-      // Sync to update UI
-      if (isSpecialPressed) {
-        syncTimeout()
-      }
-
-      if (e.metaKey && e.key.toLowerCase() === 'a' && !allowNavigation) {
-        e.preventDefault()
-        return
-      }
-
-      if (
-        !allowNavigation &&
-        (e.key === 'ArrowLeft' ||
-          e.key === 'ArrowRight' ||
-          e.key === 'ArrowUp' ||
-          e.key === 'ArrowDown')
-      ) {
-        e.preventDefault()
-        mutateInputSelectionAndUpdateMirror(
-          selectionMirror[0],
-          selectionMirror[1],
-        )
-      }
-
-      if (!inputRef.current) {
-        return
-      }
-
-      if (selectionMirror[0] === null) {
-        return
-      }
-
-      if (e.key === 'Backspace' && (e.metaKey || e.altKey)) {
-        // Check if there is a range selection
-        if (
-          inputRef.current.selectionStart !== null &&
-          inputRef.current.selectionEnd !== null &&
-          inputRef.current.selectionStart !== inputRef.current.selectionEnd
-        ) {
-          e.preventDefault()
-
-          const valueAfterDeletion = value.slice(
-            inputRef.current.selectionEnd,
-            value.length,
-          )
-
-          onChange(valueAfterDeletion)
-        }
-      }
-
-      if (
-        e.key === 'ArrowLeft' &&
-        allowNavigation &&
-        !e.shiftKey &&
-        !e.ctrlKey &&
-        !e.metaKey &&
-        !e.altKey
-      ) {
-        e.preventDefault()
-
-        if (selectionMirror[0] !== null && selectionMirror[1] !== null) {
-          const start = Math.max(0, selectionMirror[0] - 1)
-          const end = start + 1
-
-          mutateInputSelectionAndUpdateMirror(start, end)
-        }
-      }
-      if (
-        e.key === 'ArrowRight' &&
-        allowNavigation &&
-        !e.shiftKey &&
-        !e.ctrlKey &&
-        !e.metaKey &&
-        !e.altKey
-      ) {
-        e.preventDefault()
-
-        if (selectionMirror[0] !== null && selectionMirror[1] !== null) {
-          const start = Math.min(
-            selectionMirror[1],
-            Math.min(value.length, maxLength - 1),
-          )
-          const end = Math.min(maxLength, start + 1)
-
-          mutateInputSelectionAndUpdateMirror(start, end)
-        }
-      }
-    }
-
-    function onInputFocus(e: React.SyntheticEvent<HTMLInputElement>) {
-      if (!inputRef.current) {
-        return
-      }
-
-      setIsFocused(true)
-
-      // Default to the last slot or insert position
-      const end = Math.min(maxLength, value.length + 1)
-      const start = Math.max(0, end - 1)
-
-      mutateInputSelectionAndUpdateMirror(start, end)
-    }
-
-    function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
-      const prevValue = e.currentTarget.value
-      const newValue = e.target.value
-
-      e.preventDefault()
-
-      const valueToTest = allowSpaces
-        ? newValue.replace(/ /g, '').trim()
-        : newValue
-      if (
-        regexp !== null &&
-        valueToTest.length > 0 &&
-        !regexp.test(valueToTest)
-      ) {
-        return
-      }
-
-      onChange(newValue.slice(0, maxLength))
-      if (newValue.length === maxLength) {
-        syncTimeout()
-      }
-    }
-
-    function onInputKeyUp(e: React.KeyboardEvent<HTMLInputElement>) {
-      if (SPECIAL_KEYS.indexOf(e.key) !== -1) {
-        setIsSpecialPressed(false)
-
-        syncTimeout()
-      }
-
-      if (!inputRef.current) {
-        return
-      }
-
-      const start = inputRef.current.selectionStart
-      const end = inputRef.current.selectionEnd
-
-      if (
-        (e.key === 'Meta' || e.key === 'Alt' || e.key === 'Control') &&
-        start !== null &&
-        end !== null &&
-        start === end
-      ) {
-        if (value.length === 0) {
-          // Do nothing
-        } else if (start === 0) {
-          mutateInputSelectionAndUpdateMirror(0, 1)
-        } else if (start === maxLength) {
-          mutateInputSelectionAndUpdateMirror(maxLength - 1, maxLength)
-        } else if (start === value.length) {
-          mutateInputSelectionAndUpdateMirror(value.length, value.length)
-        }
-      }
-    }
-
-    function onInputBlur() {
-      setIsFocused(false)
-      setSelectionMirror([null, null])
-
-      setIsSpecialPressed(false)
-
-      onBlur?.()
-    }
-
-    function onInputBeforeInput(e: React.FormEvent<HTMLInputElement>) {
-      if (e.currentTarget.selectionStart === e.currentTarget.selectionEnd) {
-        if (value.length === maxLength) {
-          e.nativeEvent.preventDefault()
-          return
-        }
-      }
-    }
-
-    function onContainerClick(e: React.MouseEvent<HTMLInputElement>) {
-      e.preventDefault()
-
-      if (!inputRef.current) {
-        return
-      }
-
-      if (document.activeElement === inputRef.current) {
-        return
-      }
-
-      inputRef.current.focus()
-    }
-
-    const isSelected = React.useCallback(
-      (slotIdx: number) => {
-        return (
-          selectionMirror[0] !== null &&
-          selectionMirror[1] !== null &&
-          slotIdx >= selectionMirror[0] &&
-          slotIdx < selectionMirror[1]
-        )
-      },
-      [selectionMirror],
-    )
-
-    const isCurrent = React.useCallback(
-      (slotIdx: number) => {
-        if (selectionMirror[0] === null || selectionMirror[1] === null) {
-          return false
-        }
-
-        return slotIdx >= selectionMirror[0] && slotIdx < selectionMirror[1]
-      },
-      [selectionMirror],
-    )
-
-    /** JSX */
-    const renderedChildren = React.useMemo<ReturnType<typeof render>>(() => {
-      return render({
-        slots: Array.from({ length: maxLength }).map((_, slotIdx) => ({
-          char: value[slotIdx] !== undefined ? value[slotIdx] : null,
-          isActive: isFocused && (isCurrent(slotIdx) || isSelected(slotIdx)),
-        })),
-        isFocused,
-        isHovering,
-      })
-    }, [
-      disabled,
-      isCurrent,
-      isHovering,
-      isFocused,
-      isSelected,
-      maxLength,
-      render,
-      value,
-    ])
-
-    // TODO: allow for custom container
-    return (
-      <div
-        ref={ref}
-        style={{
-          position: 'relative',
-          cursor: disabled ? 'default' : 'text',
-          userSelect: 'none',
-          WebkitUserSelect: 'none',
-        }}
-        onMouseDown={disabled ? undefined : onContainerClick}
-        onMouseOver={() => setIsHovering(true)}
-        onMouseLeave={() => setIsHovering(false)}
-        className={containerClassName}
-        {...props}
-      >
-        {renderedChildren}
-
-        <input
-          inputMode={inputMode}
-          pattern={regexp ? regexp.source : undefined}
-          style={{
-            position: 'absolute',
-            inset: 0,
-            opacity: 0,
-            pointerEvents: 'none',
-            outline: 'none !important',
-
-            // debug purposes
-            // color: 'black',
-            // background: 'white',
-            // opacity: '1',
-            // pointerEvents: 'all',
-            // inset: undefined,
-            // position: undefined,
-          }}
-          // autoComplete="" // TODO: add support
-          autoComplete="one-time-code"
-          autoFocus={autoFocus}
-          name={name}
-          id={id}
-          disabled={disabled}
-          ref={inputRef}
-          maxLength={maxLength}
-          value={value}
-          onFocus={onInputFocus}
-          onChange={onInputChange}
-          onSelect={e => onInputSelect({ e })}
-          onKeyDown={onInputKeyDown}
-          onKeyUp={onInputKeyUp}
-          onBlur={onInputBlur}
-          onBeforeInput={onInputBeforeInput}
-        />
-
-        {/* {JSON.stringify({ caretData: selectionMirror })} */}
-      </div>
-    )
-  },
-)
-OTPInput.displayName = 'OTPInput'
diff --git a/src/input.tsx b/src/input.tsx
new file mode 100644
index 0000000..a87d3bf
--- /dev/null
+++ b/src/input.tsx
@@ -0,0 +1,352 @@
+import * as React from 'react'
+
+import { syncTimeouts } from './sync-timeouts'
+import { OTPInputProps, SelectionType } from './types'
+import { REGEXP_ONLY_DIGITS } from './regexp'
+
+export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
+  (
+    {
+      value: uncheckedValue,
+      onChange,
+
+      maxLength,
+      pattern = REGEXP_ONLY_DIGITS,
+      inputMode = 'numeric',
+      allowNavigation = true,
+
+      autoFocus = false,
+
+      onComplete,
+
+      render,
+
+      containerClassName,
+
+      ...props
+    },
+    ref,
+  ) => {
+    // Workarounds
+    const value = typeof uncheckedValue === 'string' ? uncheckedValue : ''
+    const regexp = pattern
+      ? typeof pattern === 'string'
+        ? new RegExp(pattern)
+        : pattern
+      : null
+
+    /** useRef */
+    const inputRef = React.useRef<HTMLInputElement>(null)
+    React.useImperativeHandle(
+      ref,
+      () => {
+        const el = inputRef.current as HTMLInputElement
+
+        const _select = el.select.bind(el)
+        el.select = () => {
+          if (!allowNavigation) {
+            // Cannot select all chars as navigation is disabled
+            return
+          }
+
+          _select()
+          // Workaround proxy to update UI as native `.select()` does not trigger focus event
+          setMirrorSelectionStart(0)
+          setMirrorSelectionEnd(el.value.length)
+        }
+
+        return el
+      },
+      [allowNavigation],
+    )
+
+    /** Mirrors for UI rendering purpose only */
+    const [isHoveringContainer, setIsHoveringContainer] = React.useState(false)
+    const [isFocused, setIsFocused] = React.useState(false)
+    const [mirrorSelectionStart, setMirrorSelectionStart] = React.useState<
+      number | null
+    >(null)
+    const [mirrorSelectionEnd, setMirrorSelectionEnd] = React.useState<
+      number | null
+    >(null)
+
+    /** Effects */
+    React.useEffect(() => {
+      if (value.length === maxLength) {
+        onComplete?.(value)
+      }
+    }, [maxLength, onComplete, value])
+
+    /** Event handlers */
+    function _selectListener() {
+      if (!inputRef.current) {
+        return
+      }
+
+      const _start = inputRef.current.selectionStart
+      const _end = inputRef.current.selectionEnd
+      const isSelected = _start !== null && _end !== null
+
+      if (value.length !== 0 && isSelected) {
+        const isSingleCaret = _start === _end
+        const isInsertMode = _start === value.length && value.length < maxLength
+
+        if (isSingleCaret && !isInsertMode) {
+          const caretPos = _start
+
+          let start: number = -1
+          let end: number = -1
+
+          if (caretPos === 0) {
+            start = 0
+            end = 1
+          } else if (caretPos === value.length) {
+            start = value.length - 1
+            end = value.length
+          } else {
+            start = caretPos
+            end = caretPos + 1
+          }
+
+          if (start !== -1 && end !== -1) {
+            inputRef.current.setSelectionRange(start, end)
+          }
+        }
+      }
+
+      syncTimeouts(() => {
+        setMirrorSelectionStart(inputRef.current?.selectionStart ?? null)
+        setMirrorSelectionEnd(inputRef.current?.selectionEnd ?? null)
+      })
+    }
+
+    function _changeListener(e: React.ChangeEvent<HTMLInputElement>) {
+      if (
+        e.currentTarget.value.length > 0 &&
+        regexp &&
+        !regexp.test(e.currentTarget.value)
+      ) {
+        e.preventDefault()
+        return
+      }
+      onChange(e.currentTarget.value)
+    }
+
+    function _keyDownListener(e: React.KeyboardEvent<HTMLInputElement>) {
+      if (!inputRef.current) {
+        return
+      }
+
+      const inputSel = [
+        inputRef.current.selectionStart,
+        inputRef.current.selectionEnd,
+      ]
+      if (inputSel[0] === null || inputSel[1] === null) {
+        return
+      }
+
+      let selectionType: SelectionType
+      if (inputSel[0] === inputSel[1]) {
+        selectionType = SelectionType.CARET
+      } else if (inputSel[1] - inputSel[0] === 1) {
+        selectionType = SelectionType.CHAR
+      } else if (inputSel[1] - inputSel[0] > 1) {
+        selectionType = SelectionType.MULTI
+      } else {
+        throw new Error('Could not determine OTPInput selection type')
+      }
+
+      if (
+        e.key === 'ArrowLeft' ||
+        e.key === 'ArrowRight' ||
+        e.key === 'ArrowUp' ||
+        e.key === 'ArrowDown' ||
+        e.key === 'Home' ||
+        e.key === 'End'
+      ) {
+        if (!allowNavigation) {
+          e.preventDefault()
+        } else {
+          if (
+            e.key === 'ArrowLeft' &&
+            selectionType === SelectionType.CHAR &&
+            !e.shiftKey &&
+            !e.metaKey &&
+            !e.ctrlKey &&
+            !e.altKey
+          ) {
+            e.preventDefault()
+
+            const start = Math.max(inputSel[0] - 1, 0)
+            const end = Math.max(inputSel[1] - 1, 1)
+
+            inputRef.current.setSelectionRange(start, end)
+          }
+
+          if (
+            e.altKey &&
+            !e.shiftKey &&
+            (e.key === 'ArrowLeft' || e.key === 'ArrowRight')
+          ) {
+            e.preventDefault()
+
+            if (e.key === 'ArrowLeft') {
+              inputRef.current.setSelectionRange(0, Math.min(1, value.length))
+            }
+            if (e.key === 'ArrowRight') {
+              inputRef.current.setSelectionRange(
+                Math.max(0, value.length - 1),
+                value.length,
+              )
+            }
+          }
+        }
+      }
+    }
+
+    function onContainerClick(e: React.MouseEvent<HTMLInputElement>) {
+      e.preventDefault()
+      if (!inputRef.current || document.activeElement === inputRef.current) {
+        return
+      }
+      inputRef.current.focus()
+    }
+
+    /** Rendering */
+    // TODO: memoize
+    const renderedInput = (
+      <input
+        autoComplete={props.autoComplete || 'one-time-code'}
+        {...props}
+        inputMode={inputMode}
+        pattern={regexp?.source}
+        style={inputStyle}
+        autoFocus={autoFocus}
+        maxLength={maxLength}
+        value={value}
+        ref={inputRef}
+        onChange={_changeListener}
+        onSelect={_selectListener}
+        // onSelectionChange={_selectListener}
+        // onSelectStart={_selectListener}
+        // onBeforeXrSelect={_selectListener}
+
+        onInput={e => {
+          syncTimeouts(_selectListener)
+
+          props.onInput?.(e)
+        }}
+        onKeyDown={e => {
+          _keyDownListener(e)
+          syncTimeouts(_selectListener)
+
+          props.onKeyDown?.(e)
+        }}
+        onKeyUp={e => {
+          syncTimeouts(_selectListener)
+
+          props.onKeyUp?.(e)
+        }}
+        onFocus={e => {
+          if (!allowNavigation) {
+            inputRef.current?.setSelectionRange(
+              Math.min(inputRef.current.value.length, maxLength - 1),
+              inputRef.current.value.length,
+            )
+          }
+          setIsFocused(true)
+
+          props.onFocus?.(e)
+        }}
+        onBlur={e => {
+          setIsFocused(false)
+
+          props.onBlur?.(e)
+        }}
+      />
+    )
+
+    const renderedChildren = React.useMemo<ReturnType<typeof render>>(() => {
+      return render({
+        slots: Array.from({ length: maxLength }).map((_, slotIdx) => {
+          const isActive =
+            isFocused &&
+            mirrorSelectionStart !== null &&
+            mirrorSelectionEnd !== null &&
+            ((mirrorSelectionStart === mirrorSelectionEnd &&
+              slotIdx === mirrorSelectionStart) ||
+              (slotIdx >= mirrorSelectionStart && slotIdx < mirrorSelectionEnd))
+
+          const char = value[slotIdx] !== undefined ? value[slotIdx] : null
+
+          return {
+            char,
+            isActive,
+            hasFakeCaret: isActive && char === null,
+          }
+        }),
+        isFocused,
+        isHovering: !props.disabled && isHoveringContainer,
+      })
+    }, [
+      render,
+      maxLength,
+      isFocused,
+      props.disabled,
+      isHoveringContainer,
+      value,
+      mirrorSelectionStart,
+      mirrorSelectionEnd,
+    ])
+
+    return (
+      <div
+        style={rootStyle({ disabled: props.disabled })}
+        className={containerClassName}
+        {...props}
+        ref={ref}
+        onMouseOver={(e: any) => {
+          setIsHoveringContainer(true)
+          props.onMouseOver?.(e)
+        }}
+        onMouseLeave={(e: any) => {
+          setIsHoveringContainer(false)
+          props.onMouseLeave?.(e)
+        }}
+        onMouseDown={(e: any) => {
+          if (!props.disabled) {
+            onContainerClick(e)
+          }
+          props.onMouseDown?.(e)
+        }}
+      >
+        {renderedChildren}
+        {renderedInput}
+      </div>
+    )
+  },
+)
+OTPInput.displayName = 'Input'
+
+const rootStyle = (params: { disabled?: boolean }) =>
+  ({
+    position: 'relative',
+    cursor: params.disabled ? 'default' : 'text',
+    userSelect: 'none',
+    WebkitUserSelect: 'none',
+  } satisfies React.CSSProperties)
+
+const inputStyle = {
+  position: 'absolute',
+  inset: 0,
+  opacity: 0,
+  pointerEvents: 'none',
+  outline: 'none !important',
+  // debugging purposes
+  // color: 'black',
+  // background: 'white',
+  // opacity: '1',
+  // pointerEvents: 'all',
+  // inset: undefined,
+  // position: undefined,
+} satisfies React.CSSProperties
diff --git a/src/regexp.tsx b/src/regexp.tsx
new file mode 100644
index 0000000..f39b71c
--- /dev/null
+++ b/src/regexp.tsx
@@ -0,0 +1,3 @@
+export const REGEXP_ONLY_DIGITS = '^\\d+$'
+export const REGEXP_ONLY_CHARS = '^[a-zA-Z]+$'
+export const REGEXP_ONLY_DIGITS_AND_CHARS = '^[a-zA-Z0-9]+$'
diff --git a/src/sync-timeouts.ts b/src/sync-timeouts.ts
new file mode 100644
index 0000000..ac908be
--- /dev/null
+++ b/src/sync-timeouts.ts
@@ -0,0 +1,9 @@
+export function syncTimeouts(cb: (...args: any[]) => unknown): number[] {
+  const t1 = setTimeout(cb, 0) // For faster machines
+  const t2 = setTimeout(cb, 1_0)
+  const t3 = setTimeout(cb, 5_0)
+  return [t1,t2,t3]
+
+  // const t3 = setTimeout(cb, 5_0)
+  // return [t3]
+}
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..9feee0c
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,31 @@
+export interface OTPInputRenderProps {
+  slots: { isActive: boolean; char: string | null; hasFakeCaret: boolean }[]
+  isFocused: boolean
+  isHovering: boolean
+}
+type OverrideProps<T, R> = Omit<T, keyof R> & R
+export type OTPInputProps = OverrideProps<
+  React.InputHTMLAttributes<HTMLInputElement>,
+  {
+    value: string
+    onChange: (...args: any[]) => unknown
+
+    maxLength: number
+
+    autoFocus?: boolean
+    allowNavigation?: boolean
+    inputMode?: 'numeric' | 'text'
+
+    onComplete?: (...args: any[]) => unknown
+    onBlur?: (...args: any[]) => unknown
+
+    render: (props: OTPInputRenderProps) => React.ReactElement
+
+    containerClassName?: string
+  }
+>
+export enum SelectionType {
+  CARET = 0,
+  CHAR = 1,
+  MULTI = 2,
+}
diff --git a/website/next.config.mjs b/website/next.config.mjs
index 4678774..61cd5ed 100644
--- a/website/next.config.mjs
+++ b/website/next.config.mjs
@@ -1,4 +1,6 @@
 /** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+  reactStrictMode: false,
+};
 
 export default nextConfig;
diff --git a/website/src/app/(pages)/(home)/_components/showcase.tsx b/website/src/app/(pages)/(home)/_components/showcase.tsx
index 3c51437..fe3a362 100644
--- a/website/src/app/(pages)/(home)/_components/showcase.tsx
+++ b/website/src/app/(pages)/(home)/_components/showcase.tsx
@@ -2,11 +2,11 @@
 
 import React from 'react'
 
-import { OTPInput } from 'otp-input'
 import { cn } from '@/lib/utils'
+import { OTPInput, REGEXP_ONLY_DIGITS } from 'otp-input'
 
 export function Showcase({ className, ...props }: { className?: string }) {
-  const [value, setValue] = React.useState('')
+  const [value, setValue] = React.useState('12')
   const inputRef = React.useRef<HTMLInputElement>(null)
 
   React.useEffect(() => {
@@ -41,7 +41,8 @@ export function Showcase({ className, ...props }: { className?: string }) {
         onChange={setValue}
         containerClassName={cn('group flex items-center')}
         maxLength={6}
-        // regexp={null} // Allow everything
+        allowNavigation={true}
+        pattern={REGEXP_ONLY_DIGITS}
         render={({ slots, isFocused }) => (
           <>
             <div className="flex">
diff --git a/website/src/app/(pages)/(home)/page.tsx b/website/src/app/(pages)/(home)/page.tsx
index 643d062..4758ba4 100644
--- a/website/src/app/(pages)/(home)/page.tsx
+++ b/website/src/app/(pages)/(home)/page.tsx
@@ -1,19 +1,19 @@
-import Link from 'next/link'
-import { Icons } from '../../../components/icons'
+import { Icons } from '@/components/icons'
 import {
   PageActions,
   PageHeader,
   PageHeaderDescription,
   PageHeaderHeading,
-} from '../../../components/page-header'
-import { buttonVariants } from '../../../components/ui/button'
-import { siteConfig } from '../../../config/site'
-import { cn } from '../../../lib/utils/cn'
+} from '@/components/page-header'
+import { buttonVariants } from '@/components/ui/button'
+import { siteConfig } from '@/config/site'
+import { cn } from '@/lib/utils'
+import Link from 'next/link'
 import { Showcase } from './_components/showcase'
 
-export default function IndexPage() {
-  const fadeUpClassname = 'motion-safe:opacity-0 motion-safe:animate-fade-up'
+const fadeUpClassname = 'motion-safe:opacity-0 motion-safe:animate-fade-up'
 
+export default function IndexPage() {
   return (
     <div className="container relative flex-1 flex flex-col justify-center">
       <PageHeader>

From e74d79ba9c88347e9d2f277bb977223296d04fda Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 19:37:33 -0300
Subject: [PATCH 3/8] test: adapt to v3 and remove unused tests

---
 test/src/app/props/page.tsx              |  2 +-
 test/src/tests/base.delete-word.spec.ts  |  4 ++--
 test/src/tests/base.props.spec.ts        |  2 +-
 test/src/tests/with-allow-spaces.spec.ts | 23 -----------------------
 4 files changed, 4 insertions(+), 27 deletions(-)
 delete mode 100644 test/src/tests/with-allow-spaces.spec.ts

diff --git a/test/src/app/props/page.tsx b/test/src/app/props/page.tsx
index 644b944..88645c0 100644
--- a/test/src/app/props/page.tsx
+++ b/test/src/app/props/page.tsx
@@ -13,7 +13,7 @@ export default function Page() {
       <BaseOTPInput data-testid="otp-input-wrapper-4" containerClassName='testclassname' />
       <BaseOTPInput data-testid="otp-input-wrapper-5" maxLength={3} />
       <BaseOTPInput data-testid="otp-input-wrapper-6" id='testid' name='testname'  />
-      <BaseOTPInput data-testid="otp-input-wrapper-7" regexp={/ /}  />
+      <BaseOTPInput data-testid="otp-input-wrapper-7" pattern={' '}  />
     </div>
   )
 }
diff --git a/test/src/tests/base.delete-word.spec.ts b/test/src/tests/base.delete-word.spec.ts
index 88be7f0..31d91da 100644
--- a/test/src/tests/base.delete-word.spec.ts
+++ b/test/src/tests/base.delete-word.spec.ts
@@ -17,7 +17,7 @@ test.describe('Delete words', () => {
     await input.press(`${modifier}+Backspace`)
     await expect(input).toHaveValue('')
   })
-  test('should backspace previous word (including selected character)', async ({ page }) => {
+  test('should backspace selected char', async ({ page }) => {
     const input = page.getByTestId('otp-input-wrapper').getByRole('textbox')
 
     await input.pressSequentially('123456')
@@ -27,7 +27,7 @@ test.describe('Delete words', () => {
     await input.press('ArrowLeft')
     await input.press(`${modifier}+Backspace`)
 
-    await expect(input).toHaveValue('56')
+    await expect(input).toHaveValue('12356')
   })
   test('should forward-delete character when pressing delete', async ({ page }) => {
     const input = page.getByTestId('otp-input-wrapper').getByRole('textbox')
diff --git a/test/src/tests/base.props.spec.ts b/test/src/tests/base.props.spec.ts
index 66c6642..702f54a 100644
--- a/test/src/tests/base.props.spec.ts
+++ b/test/src/tests/base.props.spec.ts
@@ -9,7 +9,7 @@ test.describe('Props tests', () => {
     const input1 = page.getByTestId('otp-input-wrapper-1').getByRole('textbox')
     const input2 = page.getByTestId('otp-input-wrapper-2').getByRole('textbox')
     const input3 = page.getByTestId('otp-input-wrapper-3').getByRole('textbox')
-    const inputWrapper4 = page.getByTestId('otp-input-wrapper-4')
+    const inputWrapper4 = page.getByTestId('otp-input-wrapper-4').first()
     const input5 = page.getByTestId('otp-input-wrapper-5').getByRole('textbox')
     const input6 = page.getByTestId('otp-input-wrapper-6').getByRole('textbox')
     const input7 = page.getByTestId('otp-input-wrapper-7').getByRole('textbox')
diff --git a/test/src/tests/with-allow-spaces.spec.ts b/test/src/tests/with-allow-spaces.spec.ts
deleted file mode 100644
index b294786..0000000
--- a/test/src/tests/with-allow-spaces.spec.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { expect, test } from '@playwright/test'
-
-test.beforeEach(async ({ page }) => {
-  await page.goto('/with-allow-spaces')
-})
-
-test.describe('With allow spaces tests', () => {
-  test('should prevent spaces in the input value', async ({ page }) => {
-    const input = page.getByTestId('otp-input-wrapper-1').getByRole('textbox')
-
-    await input.pressSequentially('1234567')
-    await expect(input).toHaveValue('123457')
-  })
-  test('should allow spaces in the input value', async ({ page }) => {
-    const input = page.getByTestId('otp-input-wrapper-2').getByRole('textbox')
-
-    await input.pressSequentially('1')
-    await expect(input).toHaveValue('1')
-
-    await input.pressSequentially('  34')
-    await expect(input).toHaveValue('1  34')
-  })
-})

From 77b8e97de20ac908c108c265060b5af736a437fd Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 19:39:58 -0300
Subject: [PATCH 4/8] chore(website): use react strict mode

---
 website/next.config.mjs | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/website/next.config.mjs b/website/next.config.mjs
index 61cd5ed..1d61478 100644
--- a/website/next.config.mjs
+++ b/website/next.config.mjs
@@ -1,6 +1,4 @@
 /** @type {import('next').NextConfig} */
-const nextConfig = {
-  reactStrictMode: false,
-};
+const nextConfig = {}
 
-export default nextConfig;
+export default nextConfig

From ccaeb493e584e19e3becca657a02f3a50ba5385a Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 19:40:38 -0300
Subject: [PATCH 5/8] test: remove unused `with-allow-spaces` route

---
 test/src/app/with-allow-spaces/page.tsx | 14 --------------
 1 file changed, 14 deletions(-)
 delete mode 100644 test/src/app/with-allow-spaces/page.tsx

diff --git a/test/src/app/with-allow-spaces/page.tsx b/test/src/app/with-allow-spaces/page.tsx
deleted file mode 100644
index 9045227..0000000
--- a/test/src/app/with-allow-spaces/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client'
-
-import * as React from 'react'
-
-import { BaseOTPInput } from '@/components/base-input'
-
-export default function Page() {
-  return (
-    <div className="container relative flex-1 flex flex-col justify-center items-center">
-      <BaseOTPInput data-testid="otp-input-wrapper-1" allowSpaces={false} />
-      <BaseOTPInput data-testid="otp-input-wrapper-2" allowSpaces={true} />
-    </div>
-  )
-}

From e15cc7fe64d8e5730cbcb05cd62e562eb84673e7 Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 19:45:05 -0300
Subject: [PATCH 6/8] chore(otp-input): always select last char on focus

---
 src/input.tsx | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/src/input.tsx b/src/input.tsx
index a87d3bf..e56292c 100644
--- a/src/input.tsx
+++ b/src/input.tsx
@@ -248,12 +248,10 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(
           props.onKeyUp?.(e)
         }}
         onFocus={e => {
-          if (!allowNavigation) {
-            inputRef.current?.setSelectionRange(
-              Math.min(inputRef.current.value.length, maxLength - 1),
-              inputRef.current.value.length,
-            )
-          }
+          inputRef.current?.setSelectionRange(
+            Math.min(inputRef.current.value.length, maxLength - 1),
+            inputRef.current.value.length,
+          )
           setIsFocused(true)
 
           props.onFocus?.(e)

From 469c9455e52853ea64c5fc7da5b04a7b1b68e2ab Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 21:52:28 -0300
Subject: [PATCH 7/8] test(skip): do not skip in CI

---
 test/src/tests/base.delete-word.spec.ts      | 2 +-
 test/src/tests/base.selections.spec.ts       | 8 ++++----
 test/src/tests/with-allow-navigation.spec.ts | 8 ++++----
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/test/src/tests/base.delete-word.spec.ts b/test/src/tests/base.delete-word.spec.ts
index 31d91da..4b3983a 100644
--- a/test/src/tests/base.delete-word.spec.ts
+++ b/test/src/tests/base.delete-word.spec.ts
@@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => {
 })
 
 test.describe('Delete words', () => {
-  test.skip(process.env.CI === 'true', 'Breaks in CI as it cannot handle Arrow or Shift keys')
+  // test.skip(process.env.CI === 'true', 'Breaks in CI as it cannot handle Arrow or Shift keys')
   
   test('should backspace previous word (even if there is not a selected character)', async ({ page }) => {
     const input = page.getByTestId('otp-input-wrapper').getByRole('textbox')
diff --git a/test/src/tests/base.selections.spec.ts b/test/src/tests/base.selections.spec.ts
index e3ff020..eb90590 100644
--- a/test/src/tests/base.selections.spec.ts
+++ b/test/src/tests/base.selections.spec.ts
@@ -5,10 +5,10 @@ test.beforeEach(async ({ page }) => {
 })
 
 test.describe('Base tests - Selections', () => {
-  test.skip(
-    process.env.CI === 'true',
-    'Breaks in CI as it cannot handle Arrow or Shift keys',
-  )
+  // test.skip(
+  //   process.env.CI === 'true',
+  //   'Breaks in CI as it cannot handle Arrow or Shift keys',
+  // )
 
   test('should replace selected char if another is pressed', async ({
     page,
diff --git a/test/src/tests/with-allow-navigation.spec.ts b/test/src/tests/with-allow-navigation.spec.ts
index 21f830e..fcd6ba7 100644
--- a/test/src/tests/with-allow-navigation.spec.ts
+++ b/test/src/tests/with-allow-navigation.spec.ts
@@ -23,10 +23,10 @@ async function copyAndGetClipboardContent(params: {
 }
 
 test.describe('With allow navigation tests', () => {
-  test.skip(
-    process.env.CI === 'true',
-    'Breaks in CI as it cannot handle Arrow or Shift keys',
-  )
+  // test.skip(
+  //   process.env.CI === 'true',
+  //   'Breaks in CI as it cannot handle Arrow or Shift keys',
+  // )
 
   test('should allow navigation to the sides (arrows only)', async ({
     page,

From bac6e92a6e04879f86bb8565180074fef0c3d991 Mon Sep 17 00:00:00 2001
From: Guilherme Rodz <gui.rodz.dev@gmail.com>
Date: Sun, 18 Feb 2024 21:59:58 -0300
Subject: [PATCH 8/8] test(skip): skip only `base.selections` test

---
 test/src/tests/base.delete-word.spec.ts      | 2 --
 test/src/tests/base.selections.spec.ts       | 8 ++++----
 test/src/tests/with-allow-navigation.spec.ts | 5 -----
 3 files changed, 4 insertions(+), 11 deletions(-)

diff --git a/test/src/tests/base.delete-word.spec.ts b/test/src/tests/base.delete-word.spec.ts
index 4b3983a..7d1fa9b 100644
--- a/test/src/tests/base.delete-word.spec.ts
+++ b/test/src/tests/base.delete-word.spec.ts
@@ -6,8 +6,6 @@ test.beforeEach(async ({ page }) => {
 })
 
 test.describe('Delete words', () => {
-  // test.skip(process.env.CI === 'true', 'Breaks in CI as it cannot handle Arrow or Shift keys')
-  
   test('should backspace previous word (even if there is not a selected character)', async ({ page }) => {
     const input = page.getByTestId('otp-input-wrapper').getByRole('textbox')
 
diff --git a/test/src/tests/base.selections.spec.ts b/test/src/tests/base.selections.spec.ts
index eb90590..f933077 100644
--- a/test/src/tests/base.selections.spec.ts
+++ b/test/src/tests/base.selections.spec.ts
@@ -5,10 +5,10 @@ test.beforeEach(async ({ page }) => {
 })
 
 test.describe('Base tests - Selections', () => {
-  // test.skip(
-  //   process.env.CI === 'true',
-  //   'Breaks in CI as it cannot handle Arrow or Shift keys',
-  // )
+  test.skip(
+    process.env.CI === 'true',
+    'Breaks in CI as it cannot handle Shift key',
+  )
 
   test('should replace selected char if another is pressed', async ({
     page,
diff --git a/test/src/tests/with-allow-navigation.spec.ts b/test/src/tests/with-allow-navigation.spec.ts
index fcd6ba7..38b25f9 100644
--- a/test/src/tests/with-allow-navigation.spec.ts
+++ b/test/src/tests/with-allow-navigation.spec.ts
@@ -23,11 +23,6 @@ async function copyAndGetClipboardContent(params: {
 }
 
 test.describe('With allow navigation tests', () => {
-  // test.skip(
-  //   process.env.CI === 'true',
-  //   'Breaks in CI as it cannot handle Arrow or Shift keys',
-  // )
-
   test('should allow navigation to the sides (arrows only)', async ({
     page,
     context,