Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@
color: var(--color-gray-600);
}

.Code {
font-family:
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
}

.ScreenReaderOnly {
position: absolute;
width: 1px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import styles from './index.module.css';

const CODE_LENGTH = 6;

function sanitizeTierCode(value: string) {
return value.replace(/[^0-3]/g, '');
function normalizeRecoveryCode(value: string) {
return value.toUpperCase();
}

function getInvalidClassName(invalidPulse: number, evenClassName: string, oddClassName: string) {
Expand All @@ -18,7 +18,7 @@ function getInvalidClassName(invalidPulse: number, evenClassName: string, oddCla
return invalidPulse % 2 === 0 ? evenClassName : oddClassName;
}

export default function OTPFieldCustomSanitizeDemo() {
export default function OTPFieldCustomNormalizeDemo() {
const id = React.useId();
const descriptionId = `${id}-description`;

Expand All @@ -40,14 +40,13 @@ export default function OTPFieldCustomSanitizeDemo() {
return (
<div className={styles.Field}>
<label htmlFor={id} className={styles.Label}>
Tier code
Recovery code
</label>
<OTPField.Root
id={id}
length={CODE_LENGTH}
validationType="none"
inputMode="numeric"
sanitizeValue={sanitizeTierCode}
validationType="alphanumeric"
normalizeValue={normalizeRecoveryCode}
onValueChange={handleValueChange}
onValueInvalid={handleValueInvalid}
aria-describedby={descriptionId}
Expand All @@ -65,7 +64,7 @@ export default function OTPFieldCustomSanitizeDemo() {
))}
</OTPField.Root>
<p id={descriptionId} className={styles.Description}>
Digits <span className={styles.Code}>0-3</span> only.
Letters and digits only. Letters are converted to uppercase.
</p>
<span aria-live="polite" className={styles.ScreenReaderOnly}>
{statusMessage}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createDemoWithVariants } from 'docs/src/utils/createDemo';
import CssModules from './css-modules';

export const DemoOTPFieldCustomSanitize = createDemoWithVariants(import.meta.url, {
export const DemoOTPFieldCustomNormalize = createDemoWithVariants(import.meta.url, {
CssModules,
});
16 changes: 9 additions & 7 deletions docs/src/app/(docs)/react/components/otp-field/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,18 @@ import { DemoOTPFieldFocusedPlaceholder } from './demos/focused-placeholder';

<DemoOTPFieldFocusedPlaceholder compact />

### Custom sanitization
### Custom normalization

Set `validationType="none"` with `sanitizeValue` when you need to normalize pasted values before
they reach state or apply custom validation rules. Use `inputMode` when a custom rule still needs a
specific virtual keyboard hint, and `onValueInvalid` when you want to react to rejected
characters.
Use `normalizeValue` to normalize accepted values before state updates, such as converting
alphanumeric codes to uppercase. It runs after `validationType` filtering, and the result is filtered
against `validationType` again. Use `validationType="none"` when the normalizer should provide the
full validation rule.

import { DemoOTPFieldCustomSanitize } from './demos/custom-sanitize';
Pair custom rules with `inputMode` for keyboard hints and `onValueInvalid` for rejected characters.

<DemoOTPFieldCustomSanitize compact />
import { DemoOTPFieldCustomNormalize } from './demos/custom-sanitize';

<DemoOTPFieldCustomNormalize compact />

### Masked entry

Expand Down
65 changes: 36 additions & 29 deletions docs/src/app/(docs)/react/components/otp-field/types.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/src/app/(docs)/react/components/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1199,15 +1199,15 @@ A one-time password input composed of individual character slots.
- Alphanumeric verification codes
- Grouped layouts
- Placeholder hints
- Custom sanitization
- Custom normalization
- Masked entry
- API reference
- Root
- Input
- Separator
- Exports:
- OTP Field - Root
- Props: autoComplete, autoSubmit, className, defaultValue, disabled, form, id, inputMode, length, mask, name, onValueChange, onValueComplete, onValueInvalid, readOnly, render, required, sanitizeValue, style, validationType, value
- Props: autoComplete, autoSubmit, className, defaultValue, disabled, form, id, inputMode, length, mask, name, normalizeValue, onValueChange, onValueComplete, onValueInvalid, readOnly, render, required, style, validationType, value
- Data Attributes: data-complete, data-dirty, data-disabled, data-filled, data-focused, data-invalid, data-readonly, data-required, data-touched, data-valid
- OTP Field - Input
- Props: className, render, style
Expand Down
32 changes: 15 additions & 17 deletions packages/react/src/otp-field/input/OTPFieldInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ import { REASONS } from '../../internals/reasons';
import { useOTPFieldRootContext, getOTPFieldInputState } from '../root/OTPFieldRootContext';
import type { OTPFieldRootState } from '../root/OTPFieldRoot';
import { inputStateAttributesMapping } from '../utils/stateAttributesMapping';
import {
normalizeOTPValue,
removeOTPCharacter,
replaceOTPValue,
stripOTPWhitespace,
} from '../utils/otp';
import { normalizeOTPValueWithDetails, removeOTPCharacter, replaceOTPValue } from '../utils/otp';

/**
* An individual OTP character input.
Expand Down Expand Up @@ -62,7 +57,7 @@ export const OTPFieldInput = React.forwardRef(function OTPFieldInput(
reportValueInvalid,
readOnly,
required,
sanitizeValue,
normalizeValue,
setValue,
state,
validationType,
Expand Down Expand Up @@ -143,15 +138,14 @@ export const OTPFieldInput = React.forwardRef(function OTPFieldInput(
}

const rawValue = event.currentTarget.value;
const nextDigits = normalizeOTPValue(
event.currentTarget.value,
const [nextDigits, didRejectCharacters] = normalizeOTPValueWithDetails(
rawValue,
length,
validationType,
sanitizeValue,
normalizeValue,
);
const didSanitize = stripOTPWhitespace(rawValue).length > nextDigits.length;

if (didSanitize) {
if (didRejectCharacters) {
reportValueInvalid(
rawValue,
createGenericEventDetails(REASONS.inputChange, event.nativeEvent),
Expand All @@ -177,7 +171,7 @@ export const OTPFieldInput = React.forwardRef(function OTPFieldInput(
nextDigits,
length,
validationType,
sanitizeValue,
normalizeValue,
);

const committedValue = setValue(
Expand Down Expand Up @@ -291,10 +285,14 @@ export const OTPFieldInput = React.forwardRef(function OTPFieldInput(

event.preventDefault();

const nextDigits = normalizeOTPValue(rawValue, length, validationType, sanitizeValue);
const didSanitize = stripOTPWhitespace(rawValue).length > nextDigits.length;
const [nextDigits, didRejectCharacters] = normalizeOTPValueWithDetails(
rawValue,
length,
validationType,
normalizeValue,
);

if (didSanitize) {
if (didRejectCharacters) {
reportValueInvalid(
rawValue,
createGenericEventDetails(REASONS.inputPaste, event.nativeEvent),
Expand All @@ -306,7 +304,7 @@ export const OTPFieldInput = React.forwardRef(function OTPFieldInput(
}

const committedValue = setValue(
replaceOTPValue(value, index, nextDigits, length, validationType, sanitizeValue),
replaceOTPValue(value, index, nextDigits, length, validationType, normalizeValue),
createChangeEventDetails(REASONS.inputPaste, event.nativeEvent),
);

Expand Down
13 changes: 12 additions & 1 deletion packages/react/src/otp-field/root/OTPFieldRoot.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const otpFieldEventNarrowing = (
form="verification-form"
mask
validationType="alphanumeric"
sanitizeValue={(value) => value.toUpperCase()}
normalizeValue={(value) => value.toUpperCase()}
onValueChange={handleOTPFieldChange}
onValueInvalid={handleOTPFieldInvalid}
onValueComplete={handleOTPFieldComplete}
Expand All @@ -100,3 +100,14 @@ const customInputModeWithBuiltInValidation = (
<OTPField.Root length={6} validationType="numeric" inputMode="tel" />
);
void customInputModeWithBuiltInValidation;

const normalizesValue = (
<OTPField.Root length={6} validationType="alphanumeric" normalizeValue={(value) => value} />
);
void normalizesValue;

const removedSanitizeValue = (
// @ts-expect-error - sanitizeValue was renamed to normalizeValue
<OTPField.Root length={6} validationType="alphanumeric" sanitizeValue={(value) => value} />
);
void removedSanitizeValue;
Loading
Loading