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
1 change: 1 addition & 0 deletions apps/www/src/components/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export * from './list-examples';
export * from './menu-examples';
export * from './menubar-examples';
export * from './number-field-examples';
export * from './otp-field-examples';
export * from './popover-examples';
export * from './preview-card-examples';
export * from './radio-examples';
Expand Down
126 changes: 126 additions & 0 deletions apps/www/src/components/playground/otp-field-examples.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use client';

import { Field, Flex, OTPField, Text } from '@raystack/apsara';
import { useState } from 'react';
import PlaygroundLayout from './playground-layout';

const renderSlots = (length: number, offset = 0) =>
Array.from({ length }, (_, i) => (
<OTPField.Input
key={i + offset}
aria-label={`Character ${i + 1 + offset} of ${length + offset}`}
/>
));
Comment on lines +7 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix incorrect slot aria-label totals in separator example.

Line 11 generates labels like “Character 1 of 3” for the first group in a 6-character field, which gives incorrect context to screen readers. Pass the total field length separately.

Suggested fix
-const renderSlots = (length: number, offset = 0) =>
+const renderSlots = (length: number, offset = 0, totalLength = length + offset) =>
   Array.from({ length }, (_, i) => (
     <OTPField.Input
       key={i + offset}
-      aria-label={`Character ${i + 1 + offset} of ${length + offset}`}
+      aria-label={`Character ${i + 1 + offset} of ${totalLength}`}
     />
   ));
@@
-          {renderSlots(3)}
+          {renderSlots(3, 0, 6)}

Also applies to: 58-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/src/components/playground/otp-field-examples.tsx` around lines 7 -
13, renderSlots currently computes aria-label totals using length + offset,
which produces incorrect "of" counts for grouped/separated examples; change the
function signature to accept a separate totalLength (e.g., renderSlots(length:
number, offset = 0, totalLength = length)) and use totalLength in the aria-label
for OTPField.Input (use `Character ${i + 1 + offset} of ${totalLength}`). Update
all calls to renderSlots (including the separator example and the other instance
around lines 58-66) to pass the full field length as the third argument so
screen readers see the correct total.


function ControlledOTP() {
const [value, setValue] = useState('');
return (
<Flex direction='column' gap={3} align='start'>
<OTPField length={6} value={value} onValueChange={setValue}>
{renderSlots(6)}
</OTPField>
<Text size='small'>
Current value: <code>{value || '(empty)'}</code>
</Text>
</Flex>
);
}

function CompleteOTP() {
const [submitted, setSubmitted] = useState('');
return (
<Flex direction='column' gap={3} align='start'>
<OTPField length={6} onValueComplete={setSubmitted}>
{renderSlots(6)}
</OTPField>
<Text size='small'>
{submitted ? `Submitted: ${submitted}` : 'Type all 6 digits to submit'}
</Text>
</Flex>
);
}

export function OTPFieldExamples() {
return (
<PlaygroundLayout title='OTPField'>
<Flex direction='column' gap={9}>
<Text>Default (6 digits):</Text>
<OTPField length={6}>{renderSlots(6)}</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>4 digits:</Text>
<OTPField length={4}>{renderSlots(4)}</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>With separator:</Text>
<OTPField length={6}>
{renderSlots(3)}
<OTPField.Separator />
{Array.from({ length: 3 }, (_, i) => (
<OTPField.Input
key={`b-${i}`}
aria-label={`Character ${i + 4} of 6`}
/>
))}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Masked:</Text>
<OTPField length={6} mask>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Alphanumeric:</Text>
<OTPField length={6} validationType='alphanumeric'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Default value:</Text>
<OTPField length={6} defaultValue='123456'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Controlled (value / onValueChange):</Text>
<ControlledOTP />
</Flex>

<Flex direction='column' gap={9}>
<Text>onValueComplete:</Text>
<CompleteOTP />
</Flex>

<Flex direction='column' gap={9}>
<Text>Disabled:</Text>
<OTPField length={6} disabled defaultValue='123'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>Read-only:</Text>
<OTPField length={6} readOnly defaultValue='934821'>
{renderSlots(6)}
</OTPField>
</Flex>

<Flex direction='column' gap={9}>
<Text>With Field:</Text>
<Field
label='Verification code'
description='Enter the 6-digit code we sent to your device.'
>
<OTPField length={6}>{renderSlots(6)}</OTPField>
</Field>
</Flex>
</PlaygroundLayout>
);
}
175 changes: 175 additions & 0 deletions apps/www/src/content/docs/components/otp-field/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use client';

import type { ComponentPropsType } from '@/components/demo/types';
import { getPropsString } from '@/lib/utils';

const renderInputs = (
length: number
) => `Array.from({ length: ${length} }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of ${length}\`} />
))`;

export const preview = {
type: 'code',
code: `<OTPField length={6}>
{${renderInputs(6)}}
</OTPField>`
};

const getCode = (props: ComponentPropsType) => {
const { length = 6, ...rest } = props;
const slotCount = Number(length) || 6;
return `<OTPField length={${slotCount}}${getPropsString(rest)}>
{${renderInputs(slotCount)}}
</OTPField>`;
};

export const playground = {
type: 'playground',
controls: {
length: {
type: 'select',
options: ['4', '6', '8'],
defaultValue: '6'
},
validationType: {
type: 'select',
options: ['numeric', 'alpha', 'alphanumeric', 'none'],
defaultValue: 'numeric'
},
mask: {
type: 'checkbox',
defaultValue: false
},
disabled: {
type: 'checkbox',
defaultValue: false
},
readOnly: {
type: 'checkbox',
defaultValue: false
},
autoSubmit: {
type: 'checkbox',
defaultValue: false
}
},
getCode
};

export const separatorDemo = {
type: 'code',
code: `<OTPField length={6}>
{Array.from({ length: 3 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
<OTPField.Separator />
{Array.from({ length: 3 }, (_, i) => (
<OTPField.Input key={i + 3} aria-label={\`Character \${i + 4} of 6\`} />
))}
</OTPField>`
};

export const maskedDemo = {
type: 'code',
code: `<OTPField length={6} mask>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const alphanumericDemo = {
type: 'code',
code: `<OTPField length={6} validationType="alphanumeric">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const disabledDemo = {
type: 'code',
code: `<OTPField length={6} disabled defaultValue="123">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const readOnlyDemo = {
type: 'code',
code: `<OTPField length={6} readOnly defaultValue="934821">
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>`
};

export const controlledDemo = {
type: 'code',
code: `function ControlledOTP() {
const [value, setValue] = React.useState('');

return (
<Flex direction="column" gap={4} align="start">
<OTPField length={6} value={value} onValueChange={setValue}>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
<Text size="small">Current value: <code>{value || '(empty)'}</code></Text>
</Flex>
);
}`
};

export const onCompleteDemo = {
type: 'code',
code: `function CompleteOTP() {
const [submitted, setSubmitted] = React.useState('');

return (
<Flex direction="column" gap={4} align="start">
<OTPField
length={6}
onValueComplete={(value) => setSubmitted(value)}
>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
<Text size="small">
{submitted ? \`Submitted: \${submitted}\` : 'Type all 6 digits to submit'}
</Text>
</Flex>
);
}`
};

export const customSanitizeDemo = {
type: 'code',
code: `<OTPField
length={4}
validationType="none"
inputMode="numeric"
sanitizeValue={(val) => val.replace(/[^0-3]/g, '')}
>
{Array.from({ length: 4 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 4\`} />
))}
</OTPField>`
};

export const withFieldDemo = {
type: 'code',
code: `<Flex justify="center">
<Field label="Verification code" description="Enter the 6-digit code we sent to your device.">
<OTPField length={6}>
{Array.from({ length: 6 }, (_, i) => (
<OTPField.Input key={i} aria-label={\`Character \${i + 1} of 6\`} />
))}
</OTPField>
</Field>
</Flex>`
};
Loading
Loading