mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat(web): use ui pin input element (#27200)
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||
import PinCodeResetModal from '$lib/modals/PinCodeResetModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { changePinCode } from '@immich/sdk';
|
||||
import { Button, Heading, modalManager, Text, toastManager } from '@immich/ui';
|
||||
import { Button, Field, Heading, modalManager, PinInput, Text, toastManager } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let currentPinCode = $state('');
|
||||
@@ -40,9 +39,15 @@
|
||||
<form autocomplete="off" onsubmit={handleSubmit}>
|
||||
<div class="flex flex-col gap-6 place-items-center place-content-center">
|
||||
<Heading>{$t('change_pin_code')}</Heading>
|
||||
<PinCodeInput label={$t('current_pin_code')} bind:value={currentPinCode} tabindexStart={1} pinLength={6} />
|
||||
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={7} pinLength={6} />
|
||||
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={13} pinLength={6} />
|
||||
<Field label={$t('current_pin_code')}>
|
||||
<PinInput bind:value={currentPinCode} />
|
||||
</Field>
|
||||
<Field label={$t('new_pin_code')}>
|
||||
<PinInput bind:value={newPinCode} />
|
||||
</Field>
|
||||
<Field label={$t('confirm_new_pin_code')}>
|
||||
<PinInput bind:value={confirmPinCode} />
|
||||
</Field>
|
||||
<button type="button" onclick={() => modalManager.show(PinCodeResetModal, {})}>
|
||||
<Text color="muted" class="underline" size="small">{$t('forgot_pin_code_question')}</Text>
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import PinCodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { setupPinCode } from '@immich/sdk';
|
||||
import { Button, Heading, toastManager } from '@immich/ui';
|
||||
import { Button, Field, Heading, PinInput, toastManager } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -47,8 +46,12 @@
|
||||
{#if showLabel}
|
||||
<Heading>{$t('setup_pin_code')}</Heading>
|
||||
{/if}
|
||||
<PinCodeInput label={$t('new_pin_code')} bind:value={newPinCode} tabindexStart={1} pinLength={6} />
|
||||
<PinCodeInput label={$t('confirm_new_pin_code')} bind:value={confirmPinCode} tabindexStart={7} pinLength={6} />
|
||||
<Field label={$t('new_pin_code')}>
|
||||
<PinInput bind:value={newPinCode} />
|
||||
</Field>
|
||||
<Field label={$t('confirm_new_pin_code')}>
|
||||
<PinInput bind:value={confirmPinCode} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value?: string;
|
||||
pinLength?: number;
|
||||
tabindexStart?: number;
|
||||
autofocus?: boolean;
|
||||
onFilled?: (value: string) => void;
|
||||
type?: 'text' | 'password';
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
value = $bindable(''),
|
||||
pinLength = 6,
|
||||
tabindexStart = 0,
|
||||
autofocus = false,
|
||||
onFilled,
|
||||
type = 'text',
|
||||
}: Props = $props();
|
||||
|
||||
let pinValues = $state(Array.from({ length: pinLength }).fill(''));
|
||||
let pinCodeInputElements: HTMLInputElement[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if (value === '') {
|
||||
pinValues = Array.from({ length: pinLength }).fill('');
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (autofocus) {
|
||||
pinCodeInputElements[0]?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const focusNext = (index: number) => {
|
||||
pinCodeInputElements[Math.min(index + 1, pinLength - 1)]?.focus();
|
||||
};
|
||||
|
||||
const focusPrev = (index: number) => {
|
||||
if (index > 0) {
|
||||
pinCodeInputElements[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (event: Event, index: number) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const digits = target.value.replaceAll(/\D/g, '').slice(0, pinLength - index);
|
||||
|
||||
if (digits.length === 0) {
|
||||
pinValues[index] = '';
|
||||
value = pinValues.join('').trim();
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
pinValues[index + i] = digits[i];
|
||||
}
|
||||
|
||||
value = pinValues.join('').trim();
|
||||
|
||||
const lastFilledIndex = Math.min(index + digits.length, pinLength - 1);
|
||||
pinCodeInputElements[lastFilledIndex]?.focus();
|
||||
|
||||
if (value.length === pinLength) {
|
||||
onFilled?.(value);
|
||||
}
|
||||
};
|
||||
|
||||
function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
const index = pinCodeInputElements.indexOf(target);
|
||||
|
||||
switch (event.key) {
|
||||
case 'Tab': {
|
||||
return;
|
||||
}
|
||||
case 'Backspace': {
|
||||
if (target.value === '' && index > 0) {
|
||||
focusPrev(index);
|
||||
pinValues[index - 1] = '';
|
||||
} else if (target.value !== '') {
|
||||
pinValues[index] = '';
|
||||
}
|
||||
value = pinValues.join('').trim();
|
||||
return;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
if (index > 0) {
|
||||
focusPrev(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
if (index < pinLength - 1) {
|
||||
focusNext(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
{#if label}
|
||||
<Label for={pinCodeInputElements[0]?.id}>{label}</Label>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
{#each { length: pinLength } as _, index (index)}
|
||||
<input
|
||||
tabindex={tabindexStart + index}
|
||||
{type}
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
bind:this={pinCodeInputElements[index]}
|
||||
id="pin-code-{index}"
|
||||
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light"
|
||||
bind:value={pinValues[index]}
|
||||
onkeydown={handleKeydown}
|
||||
oninput={(event) => handleInput(event, index)}
|
||||
aria-label={`PIN digit ${index + 1} of ${pinLength}${label ? ` for ${label}` : ''}`}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,11 +2,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
||||
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
|
||||
import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { unlockAuthSession } from '@immich/sdk';
|
||||
import { Button, Icon } from '@immich/ui';
|
||||
import { Button, Icon, PinInput } from '@immich/ui';
|
||||
import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -55,15 +54,7 @@
|
||||
|
||||
<p class="text-center text-sm" style="text-wrap: pretty;">{$t('enter_your_pin_code_subtitle')}</p>
|
||||
|
||||
<PincodeInput
|
||||
type="password"
|
||||
autofocus
|
||||
label=""
|
||||
bind:value={pinCode}
|
||||
tabindexStart={1}
|
||||
pinLength={6}
|
||||
onFilled={handleUnlockSession}
|
||||
/>
|
||||
<PinInput password autofocus bind:value={pinCode} onComplete={handleUnlockSession} />
|
||||
{:else}
|
||||
<div class="text-primary">
|
||||
<Icon icon={mdiLockSmart} size="64" />
|
||||
|
||||
Reference in New Issue
Block a user