diff --git a/packages/core/src/useOtpField/useOtpField.ts b/packages/core/src/useOtpField/useOtpField.ts index 41f87104..341db0e4 100644 --- a/packages/core/src/useOtpField/useOtpField.ts +++ b/packages/core/src/useOtpField/useOtpField.ts @@ -36,6 +36,11 @@ export interface OTPFieldProps { */ disabled?: boolean; + /** + * Whether the OTP field is masked. + */ + masked?: boolean; + /** * Whether the OTP field is readonly. */ @@ -193,6 +198,7 @@ export function useOtpField(_props: Reactivify) { disabled: prefix.length ? prefix.length > index : isDisabled.value, readonly: toValue(props.readonly), accept: toValue(props.accept), + masked: prefix.length <= index && toValue(props.masked), })); }); diff --git a/packages/core/src/useOtpField/useOtpSlot.ts b/packages/core/src/useOtpField/useOtpSlot.ts index 64e3f479..dc55407a 100644 --- a/packages/core/src/useOtpField/useOtpSlot.ts +++ b/packages/core/src/useOtpField/useOtpSlot.ts @@ -22,6 +22,11 @@ export interface OtpSlotProps { */ readonly?: boolean; + /** + * Whether the slot is masked. + */ + masked?: boolean; + /** * The type of the slot. */ @@ -62,6 +67,14 @@ export function useOtpSlot(_props: Reactivify) { } } + function withMask(value: string | undefined) { + if (!toValue(props.masked) || !value) { + return value ?? ''; + } + + return '•'.repeat(value.length); + } + function setElementValue(value: string) { if (!slotEl.value) { return; @@ -73,7 +86,7 @@ export function useOtpSlot(_props: Reactivify) { return; } - slotEl.value.textContent = value; + slotEl.value.textContent = withMask(value); } const handlers = { @@ -146,6 +159,7 @@ export function useOtpSlot(_props: Reactivify) { baseProps.contenteditable = isDisabled.value ? 'false' : isFirefox() ? 'true' : 'plaintext-only'; } else { baseProps.value = toValue(props.value); + baseProps.type = toValue(props.masked) ? 'password' : 'text'; } return withRefCapture(baseProps, slotEl); @@ -154,7 +168,7 @@ export function useOtpSlot(_props: Reactivify) { return { slotProps, key: id, - value: computed(() => toValue(props.value)), + value: computed(() => withMask(toValue(props.value))), }; } @@ -163,7 +177,7 @@ export function useOtpSlot(_props: Reactivify) { */ export const OtpSlot = /*#__PURE__*/ defineComponent({ name: 'OtpSlot', - props: ['value', 'disabled', 'readonly', 'accept'], + props: ['value', 'disabled', 'readonly', 'accept', 'masked'], setup(props) { const { slotProps, value, key } = useOtpSlot(props); diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 5ffc02c4..bb836a49 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -3,5 +3,5 @@ import OtpField from '@/components/OtpField.vue';