@dashforge/tw 0.1.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.swcrc +29 -0
- package/CHANGELOG.md +131 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/components/AppShell/AppShell.d.ts +32 -0
- package/dist/components/AppShell/AppShell.d.ts.map +1 -0
- package/dist/components/AppShell/appShell.types.d.ts +62 -0
- package/dist/components/AppShell/appShell.types.d.ts.map +1 -0
- package/dist/components/AppShell/appShell.variants.d.ts +65 -0
- package/dist/components/AppShell/appShell.variants.d.ts.map +1 -0
- package/dist/components/Autocomplete/Autocomplete.d.ts +32 -0
- package/dist/components/Autocomplete/Autocomplete.d.ts.map +1 -0
- package/dist/components/Autocomplete/autocomplete.types.d.ts +214 -0
- package/dist/components/Autocomplete/autocomplete.types.d.ts.map +1 -0
- package/dist/components/Autocomplete/autocomplete.variants.d.ts +214 -0
- package/dist/components/Autocomplete/autocomplete.variants.d.ts.map +1 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +23 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts +98 -0
- package/dist/components/Breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts +85 -0
- package/dist/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts +44 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/button.types.d.ts +66 -0
- package/dist/components/Button/button.types.d.ts.map +1 -0
- package/dist/components/Button/button.variants.d.ts +104 -0
- package/dist/components/Button/button.variants.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.d.ts +31 -0
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox/checkbox.types.d.ts +86 -0
- package/dist/components/Checkbox/checkbox.types.d.ts.map +1 -0
- package/dist/components/Checkbox/checkbox.variants.d.ts +109 -0
- package/dist/components/Checkbox/checkbox.variants.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts +38 -0
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts +80 -0
- package/dist/components/ConfirmDialog/confirmDialog.types.d.ts.map +1 -0
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts +90 -0
- package/dist/components/ConfirmDialog/confirmDialog.variants.d.ts.map +1 -0
- package/dist/components/DateTimePicker/DateTimePicker.d.ts +49 -0
- package/dist/components/DateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts +95 -0
- package/dist/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -0
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts +165 -0
- package/dist/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -0
- package/dist/components/LeftNav/LeftNav.d.ts +34 -0
- package/dist/components/LeftNav/LeftNav.d.ts.map +1 -0
- package/dist/components/LeftNav/leftNav.types.d.ts +136 -0
- package/dist/components/LeftNav/leftNav.types.d.ts.map +1 -0
- package/dist/components/LeftNav/leftNav.variants.d.ts +143 -0
- package/dist/components/LeftNav/leftNav.variants.d.ts.map +1 -0
- package/dist/components/NumberField/NumberField.d.ts +18 -0
- package/dist/components/NumberField/NumberField.d.ts.map +1 -0
- package/dist/components/NumberField/numberField.types.d.ts +79 -0
- package/dist/components/NumberField/numberField.types.d.ts.map +1 -0
- package/dist/components/NumberField/numberField.variants.d.ts +169 -0
- package/dist/components/NumberField/numberField.variants.d.ts.map +1 -0
- package/dist/components/OTPField/OTPField.d.ts +25 -0
- package/dist/components/OTPField/OTPField.d.ts.map +1 -0
- package/dist/components/OTPField/otpField.types.d.ts +74 -0
- package/dist/components/OTPField/otpField.types.d.ts.map +1 -0
- package/dist/components/OTPField/otpField.variants.d.ts +105 -0
- package/dist/components/OTPField/otpField.variants.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts +35 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/components/RadioGroup/radioGroup.types.d.ts +103 -0
- package/dist/components/RadioGroup/radioGroup.types.d.ts.map +1 -0
- package/dist/components/RadioGroup/radioGroup.variants.d.ts +166 -0
- package/dist/components/RadioGroup/radioGroup.variants.d.ts.map +1 -0
- package/dist/components/Snackbar/Snackbar.d.ts +37 -0
- package/dist/components/Snackbar/Snackbar.d.ts.map +1 -0
- package/dist/components/Snackbar/snackbar.types.d.ts +86 -0
- package/dist/components/Snackbar/snackbar.types.d.ts.map +1 -0
- package/dist/components/Snackbar/snackbar.variants.d.ts +153 -0
- package/dist/components/Snackbar/snackbar.variants.d.ts.map +1 -0
- package/dist/components/Switch/Switch.d.ts +15 -0
- package/dist/components/Switch/Switch.d.ts.map +1 -0
- package/dist/components/Switch/switch.types.d.ts +47 -0
- package/dist/components/Switch/switch.types.d.ts.map +1 -0
- package/dist/components/Switch/switch.variants.d.ts +105 -0
- package/dist/components/Switch/switch.variants.d.ts.map +1 -0
- package/dist/components/TextField/TextField.d.ts +25 -0
- package/dist/components/TextField/TextField.d.ts.map +1 -0
- package/dist/components/TextField/textField.types.d.ts +72 -0
- package/dist/components/TextField/textField.types.d.ts.map +1 -0
- package/dist/components/TextField/textField.variants.d.ts +160 -0
- package/dist/components/TextField/textField.variants.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.d.ts +16 -0
- package/dist/components/Textarea/Textarea.d.ts.map +1 -0
- package/dist/components/Textarea/textarea.types.d.ts +63 -0
- package/dist/components/Textarea/textarea.types.d.ts.map +1 -0
- package/dist/components/Textarea/textarea.variants.d.ts +197 -0
- package/dist/components/Textarea/textarea.variants.d.ts.map +1 -0
- package/dist/components/TopBar/TopBar.d.ts +33 -0
- package/dist/components/TopBar/TopBar.d.ts.map +1 -0
- package/dist/components/TopBar/topBar.types.d.ts +49 -0
- package/dist/components/TopBar/topBar.types.d.ts.map +1 -0
- package/dist/components/TopBar/topBar.variants.d.ts +79 -0
- package/dist/components/TopBar/topBar.variants.d.ts.map +1 -0
- package/dist/components/_shared/resolveValidationState.d.ts +42 -0
- package/dist/components/_shared/resolveValidationState.d.ts.map +1 -0
- package/dist/hooks/useAccessState.d.ts +37 -0
- package/dist/hooks/useAccessState.d.ts.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +4958 -0
- package/dist/src/components/AppShell/AppShell.d.ts +32 -0
- package/dist/src/components/AppShell/AppShell.d.ts.map +1 -0
- package/dist/src/components/AppShell/appShell.types.d.ts +62 -0
- package/dist/src/components/AppShell/appShell.types.d.ts.map +1 -0
- package/dist/src/components/AppShell/appShell.variants.d.ts +65 -0
- package/dist/src/components/AppShell/appShell.variants.d.ts.map +1 -0
- package/dist/src/components/Autocomplete/Autocomplete.d.ts +32 -0
- package/dist/src/components/Autocomplete/Autocomplete.d.ts.map +1 -0
- package/dist/src/components/Autocomplete/autocomplete.types.d.ts +214 -0
- package/dist/src/components/Autocomplete/autocomplete.types.d.ts.map +1 -0
- package/dist/src/components/Autocomplete/autocomplete.variants.d.ts +214 -0
- package/dist/src/components/Autocomplete/autocomplete.variants.d.ts.map +1 -0
- package/dist/src/components/Breadcrumbs/Breadcrumbs.d.ts +23 -0
- package/dist/src/components/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.types.d.ts +98 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.types.d.ts.map +1 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts +85 -0
- package/dist/src/components/Breadcrumbs/breadcrumbs.variants.d.ts.map +1 -0
- package/dist/src/components/Button/Button.d.ts +44 -0
- package/dist/src/components/Button/Button.d.ts.map +1 -0
- package/dist/src/components/Button/button.types.d.ts +66 -0
- package/dist/src/components/Button/button.types.d.ts.map +1 -0
- package/dist/src/components/Button/button.variants.d.ts +104 -0
- package/dist/src/components/Button/button.variants.d.ts.map +1 -0
- package/dist/src/components/Checkbox/Checkbox.d.ts +31 -0
- package/dist/src/components/Checkbox/Checkbox.d.ts.map +1 -0
- package/dist/src/components/Checkbox/checkbox.types.d.ts +86 -0
- package/dist/src/components/Checkbox/checkbox.types.d.ts.map +1 -0
- package/dist/src/components/Checkbox/checkbox.variants.d.ts +109 -0
- package/dist/src/components/Checkbox/checkbox.variants.d.ts.map +1 -0
- package/dist/src/components/ConfirmDialog/ConfirmDialog.d.ts +38 -0
- package/dist/src/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.types.d.ts +80 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.types.d.ts.map +1 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.variants.d.ts +90 -0
- package/dist/src/components/ConfirmDialog/confirmDialog.variants.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/DateTimePicker.d.ts +49 -0
- package/dist/src/components/DateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts +95 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.types.d.ts.map +1 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts +165 -0
- package/dist/src/components/DateTimePicker/dateTimePicker.variants.d.ts.map +1 -0
- package/dist/src/components/LeftNav/LeftNav.d.ts +34 -0
- package/dist/src/components/LeftNav/LeftNav.d.ts.map +1 -0
- package/dist/src/components/LeftNav/leftNav.types.d.ts +136 -0
- package/dist/src/components/LeftNav/leftNav.types.d.ts.map +1 -0
- package/dist/src/components/LeftNav/leftNav.variants.d.ts +143 -0
- package/dist/src/components/LeftNav/leftNav.variants.d.ts.map +1 -0
- package/dist/src/components/NumberField/NumberField.d.ts +18 -0
- package/dist/src/components/NumberField/NumberField.d.ts.map +1 -0
- package/dist/src/components/NumberField/numberField.types.d.ts +79 -0
- package/dist/src/components/NumberField/numberField.types.d.ts.map +1 -0
- package/dist/src/components/NumberField/numberField.variants.d.ts +169 -0
- package/dist/src/components/NumberField/numberField.variants.d.ts.map +1 -0
- package/dist/src/components/OTPField/OTPField.d.ts +25 -0
- package/dist/src/components/OTPField/OTPField.d.ts.map +1 -0
- package/dist/src/components/OTPField/otpField.types.d.ts +74 -0
- package/dist/src/components/OTPField/otpField.types.d.ts.map +1 -0
- package/dist/src/components/OTPField/otpField.variants.d.ts +105 -0
- package/dist/src/components/OTPField/otpField.variants.d.ts.map +1 -0
- package/dist/src/components/RadioGroup/RadioGroup.d.ts +35 -0
- package/dist/src/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/src/components/RadioGroup/radioGroup.types.d.ts +103 -0
- package/dist/src/components/RadioGroup/radioGroup.types.d.ts.map +1 -0
- package/dist/src/components/RadioGroup/radioGroup.variants.d.ts +166 -0
- package/dist/src/components/RadioGroup/radioGroup.variants.d.ts.map +1 -0
- package/dist/src/components/Snackbar/Snackbar.d.ts +37 -0
- package/dist/src/components/Snackbar/Snackbar.d.ts.map +1 -0
- package/dist/src/components/Snackbar/snackbar.types.d.ts +86 -0
- package/dist/src/components/Snackbar/snackbar.types.d.ts.map +1 -0
- package/dist/src/components/Snackbar/snackbar.variants.d.ts +153 -0
- package/dist/src/components/Snackbar/snackbar.variants.d.ts.map +1 -0
- package/dist/src/components/Switch/Switch.d.ts +15 -0
- package/dist/src/components/Switch/Switch.d.ts.map +1 -0
- package/dist/src/components/Switch/switch.types.d.ts +47 -0
- package/dist/src/components/Switch/switch.types.d.ts.map +1 -0
- package/dist/src/components/Switch/switch.variants.d.ts +105 -0
- package/dist/src/components/Switch/switch.variants.d.ts.map +1 -0
- package/dist/src/components/TextField/TextField.d.ts +25 -0
- package/dist/src/components/TextField/TextField.d.ts.map +1 -0
- package/dist/src/components/TextField/textField.types.d.ts +72 -0
- package/dist/src/components/TextField/textField.types.d.ts.map +1 -0
- package/dist/src/components/TextField/textField.variants.d.ts +160 -0
- package/dist/src/components/TextField/textField.variants.d.ts.map +1 -0
- package/dist/src/components/Textarea/Textarea.d.ts +16 -0
- package/dist/src/components/Textarea/Textarea.d.ts.map +1 -0
- package/dist/src/components/Textarea/textarea.types.d.ts +63 -0
- package/dist/src/components/Textarea/textarea.types.d.ts.map +1 -0
- package/dist/src/components/Textarea/textarea.variants.d.ts +197 -0
- package/dist/src/components/Textarea/textarea.variants.d.ts.map +1 -0
- package/dist/src/components/TopBar/TopBar.d.ts +33 -0
- package/dist/src/components/TopBar/TopBar.d.ts.map +1 -0
- package/dist/src/components/TopBar/topBar.types.d.ts +49 -0
- package/dist/src/components/TopBar/topBar.types.d.ts.map +1 -0
- package/dist/src/components/TopBar/topBar.variants.d.ts +79 -0
- package/dist/src/components/TopBar/topBar.variants.d.ts.map +1 -0
- package/dist/src/components/_shared/resolveValidationState.d.ts +42 -0
- package/dist/src/components/_shared/resolveValidationState.d.ts.map +1 -0
- package/dist/src/hooks/useAccessState.d.ts +37 -0
- package/dist/src/hooks/useAccessState.d.ts.map +1 -0
- package/dist/src/index.d.ts +82 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/utils/cn.d.ts +26 -0
- package/dist/src/utils/cn.d.ts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/utils/cn.d.ts +26 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/eslint.config.mjs +47 -0
- package/package.json +37 -0
- package/project.json +15 -0
- package/rollup.config.cjs +16 -0
- package/src/components/AppShell/AppShell.test.tsx +223 -0
- package/src/components/AppShell/AppShell.tsx +114 -0
- package/src/components/AppShell/appShell.types.ts +49 -0
- package/src/components/AppShell/appShell.variants.ts +48 -0
- package/src/components/Autocomplete/Autocomplete.perf.test.tsx +225 -0
- package/src/components/Autocomplete/Autocomplete.test.tsx +556 -0
- package/src/components/Autocomplete/Autocomplete.tsx +996 -0
- package/src/components/Autocomplete/autocomplete.types.ts +198 -0
- package/src/components/Autocomplete/autocomplete.variants.ts +135 -0
- package/src/components/Breadcrumbs/Breadcrumbs.perf.test.tsx +125 -0
- package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +231 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +203 -0
- package/src/components/Breadcrumbs/breadcrumbs.types.ts +87 -0
- package/src/components/Breadcrumbs/breadcrumbs.variants.ts +49 -0
- package/src/components/Button/Button.test.tsx +183 -0
- package/src/components/Button/Button.tsx +139 -0
- package/src/components/Button/button.types.ts +74 -0
- package/src/components/Button/button.variants.ts +159 -0
- package/src/components/Checkbox/Checkbox.test.tsx +146 -0
- package/src/components/Checkbox/Checkbox.tsx +228 -0
- package/src/components/Checkbox/checkbox.types.ts +88 -0
- package/src/components/Checkbox/checkbox.variants.ts +68 -0
- package/src/components/ConfirmDialog/ConfirmDialog.perf.test.tsx +172 -0
- package/src/components/ConfirmDialog/ConfirmDialog.test.tsx +311 -0
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +252 -0
- package/src/components/ConfirmDialog/confirmDialog.types.ts +70 -0
- package/src/components/ConfirmDialog/confirmDialog.variants.ts +76 -0
- package/src/components/DateTimePicker/DateTimePicker.perf.test.tsx +153 -0
- package/src/components/DateTimePicker/DateTimePicker.test.tsx +204 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +319 -0
- package/src/components/DateTimePicker/dateTimePicker.types.ts +83 -0
- package/src/components/DateTimePicker/dateTimePicker.variants.ts +84 -0
- package/src/components/LeftNav/LeftNav.perf.test.tsx +172 -0
- package/src/components/LeftNav/LeftNav.test.tsx +302 -0
- package/src/components/LeftNav/LeftNav.tsx +330 -0
- package/src/components/LeftNav/leftNav.types.ts +120 -0
- package/src/components/LeftNav/leftNav.variants.ts +96 -0
- package/src/components/NumberField/NumberField.test.tsx +83 -0
- package/src/components/NumberField/NumberField.tsx +293 -0
- package/src/components/NumberField/numberField.types.ts +72 -0
- package/src/components/NumberField/numberField.variants.ts +94 -0
- package/src/components/OTPField/OTPField.test.tsx +121 -0
- package/src/components/OTPField/OTPField.tsx +250 -0
- package/src/components/OTPField/otpField.types.ts +58 -0
- package/src/components/OTPField/otpField.variants.ts +72 -0
- package/src/components/RadioGroup/RadioGroup.test.tsx +140 -0
- package/src/components/RadioGroup/RadioGroup.tsx +261 -0
- package/src/components/RadioGroup/radioGroup.types.ts +85 -0
- package/src/components/RadioGroup/radioGroup.variants.ts +87 -0
- package/src/components/Snackbar/Snackbar.perf.test.tsx +144 -0
- package/src/components/Snackbar/Snackbar.test.tsx +327 -0
- package/src/components/Snackbar/Snackbar.tsx +298 -0
- package/src/components/Snackbar/snackbar.types.ts +83 -0
- package/src/components/Snackbar/snackbar.variants.ts +72 -0
- package/src/components/Switch/Switch.test.tsx +131 -0
- package/src/components/Switch/Switch.tsx +158 -0
- package/src/components/Switch/switch.types.ts +36 -0
- package/src/components/Switch/switch.variants.ts +65 -0
- package/src/components/TextField/TextField.test.tsx +194 -0
- package/src/components/TextField/TextField.tsx +216 -0
- package/src/components/TextField/textField.types.ts +70 -0
- package/src/components/TextField/textField.variants.ts +87 -0
- package/src/components/Textarea/Textarea.test.tsx +75 -0
- package/src/components/Textarea/Textarea.tsx +190 -0
- package/src/components/Textarea/textarea.types.ts +60 -0
- package/src/components/Textarea/textarea.variants.ts +85 -0
- package/src/components/TopBar/TopBar.test.tsx +129 -0
- package/src/components/TopBar/TopBar.tsx +66 -0
- package/src/components/TopBar/topBar.types.ts +42 -0
- package/src/components/TopBar/topBar.variants.ts +38 -0
- package/src/components/_shared/resolveValidationState.ts +60 -0
- package/src/hooks/useAccessState.spec.tsx +161 -0
- package/src/hooks/useAccessState.ts +81 -0
- package/src/index.ts +191 -0
- package/src/utils/cn.spec.ts +16 -0
- package/src/utils/cn.ts +29 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +55 -0
- package/tsconfig.spec.json +34 -0
- package/vite.config.ts +25 -0
- package/vitest.config.mts +22 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useContext, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { DashFormContext, useEngineVisibility } from '@dashforge/ui-core';
|
|
3
|
+
import type { DashFormBridge, FieldRegistration } from '@dashforge/ui-core';
|
|
4
|
+
import { useDashFieldMeta } from '@dashforge/forms';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import { useAccessState } from '../../hooks/useAccessState.js';
|
|
7
|
+
import { resolveValidationState } from '../_shared/resolveValidationState.js';
|
|
8
|
+
import { otpFieldVariants } from './otpField.variants.js';
|
|
9
|
+
import type { OTPFieldMode, OTPFieldProps } from './otpField.types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whitelist allowed characters per mode. Anything else is dropped.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
function sanitize(input: string, mode: OTPFieldMode, max: number): string {
|
|
17
|
+
const re = mode === 'numeric' ? /[0-9]/ : /[A-Za-z0-9]/;
|
|
18
|
+
let out = '';
|
|
19
|
+
for (const ch of input) {
|
|
20
|
+
if (re.test(ch) && out.length < max) out += ch;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Dashforge TW OTPField — segmented input for one-time codes.
|
|
27
|
+
*
|
|
28
|
+
* Renders N visible "slot" cells over a hidden text input that absorbs
|
|
29
|
+
* all keystrokes. The visible cells are read-only display surfaces; only
|
|
30
|
+
* the hidden input takes focus. This pattern works correctly with
|
|
31
|
+
* password managers + autofill (the hidden input has
|
|
32
|
+
* `autoComplete="one-time-code"`).
|
|
33
|
+
*
|
|
34
|
+
* Bridge contract: the joined string (length ≤ `length`) is what gets
|
|
35
|
+
* written to the bridge. Use `<DashForm rules={{ minLength: length }} />`
|
|
36
|
+
* to require a complete code.
|
|
37
|
+
*
|
|
38
|
+
* Out of scope for F4-B (deferred to a follow-up sprint):
|
|
39
|
+
* - Per-slot click to jump caret
|
|
40
|
+
* - Arrow-key navigation between slots
|
|
41
|
+
* - Right-to-left input direction
|
|
42
|
+
*
|
|
43
|
+
* **A11y**: the hidden `<input>` exposes
|
|
44
|
+
* `inputMode="numeric" | "text"` per mode + `autoComplete="one-time-code"`
|
|
45
|
+
* so iOS Safari + Android Chrome trigger SMS autofill correctly.
|
|
46
|
+
*/
|
|
47
|
+
export function OTPField(props: OTPFieldProps) {
|
|
48
|
+
const {
|
|
49
|
+
name,
|
|
50
|
+
rules,
|
|
51
|
+
visibleWhen,
|
|
52
|
+
size,
|
|
53
|
+
label,
|
|
54
|
+
helperText,
|
|
55
|
+
required,
|
|
56
|
+
error,
|
|
57
|
+
disabled,
|
|
58
|
+
access,
|
|
59
|
+
sx,
|
|
60
|
+
slotProps,
|
|
61
|
+
length = 6,
|
|
62
|
+
mode = 'numeric',
|
|
63
|
+
value: userValue,
|
|
64
|
+
defaultValue,
|
|
65
|
+
onChange: userOnChange,
|
|
66
|
+
onComplete,
|
|
67
|
+
} = props;
|
|
68
|
+
|
|
69
|
+
// ───── Hooks (unconditional) ─────
|
|
70
|
+
const bridge = useContext(DashFormContext) as DashFormBridge | null;
|
|
71
|
+
const isVisible = useEngineVisibility(bridge?.engine, visibleWhen);
|
|
72
|
+
useDashFieldMeta(name);
|
|
73
|
+
const accessState = useAccessState(access);
|
|
74
|
+
|
|
75
|
+
const inputId = useId();
|
|
76
|
+
const helperId = `${inputId}-help`;
|
|
77
|
+
const labelId = `${inputId}-label`;
|
|
78
|
+
|
|
79
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
80
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(() =>
|
|
81
|
+
sanitize(defaultValue ?? '', mode, length)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// StrictMode-safe unregister-on-unmount
|
|
85
|
+
const unregisterRef = useRef({ bridge, name });
|
|
86
|
+
unregisterRef.current = { bridge, name };
|
|
87
|
+
const isMountedRef = useRef(false);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
isMountedRef.current = true;
|
|
91
|
+
return () => {
|
|
92
|
+
isMountedRef.current = false;
|
|
93
|
+
const { bridge: cap, name: capName } = unregisterRef.current;
|
|
94
|
+
queueMicrotask(() => {
|
|
95
|
+
if (!isMountedRef.current) cap?.unregister?.(capName);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
if (!isVisible) return null;
|
|
101
|
+
if (!accessState.visible) return null;
|
|
102
|
+
|
|
103
|
+
const effectiveDisabled = Boolean(disabled) || accessState.disabled;
|
|
104
|
+
const effectiveReadOnly = accessState.readonly;
|
|
105
|
+
|
|
106
|
+
const isFormMode = Boolean(bridge?.register);
|
|
107
|
+
|
|
108
|
+
let resolvedError = error;
|
|
109
|
+
let resolvedHelperText: typeof helperText = helperText;
|
|
110
|
+
let registration: FieldRegistration | null = null;
|
|
111
|
+
let resolvedValue: string;
|
|
112
|
+
|
|
113
|
+
if (isFormMode && bridge) {
|
|
114
|
+
registration = bridge.register(name, rules);
|
|
115
|
+
const validation = resolveValidationState(name, bridge, error, helperText);
|
|
116
|
+
resolvedError = validation.error;
|
|
117
|
+
resolvedHelperText = validation.helperText;
|
|
118
|
+
if (userValue !== undefined) {
|
|
119
|
+
resolvedValue = sanitize(userValue, mode, length);
|
|
120
|
+
} else {
|
|
121
|
+
const bv = bridge.getValue(name);
|
|
122
|
+
resolvedValue = sanitize(bv == null ? '' : String(bv), mode, length);
|
|
123
|
+
}
|
|
124
|
+
} else if (userValue !== undefined) {
|
|
125
|
+
resolvedValue = sanitize(userValue, mode, length);
|
|
126
|
+
} else {
|
|
127
|
+
resolvedValue = uncontrolledValue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const v = otpFieldVariants({ size, error: resolvedError });
|
|
131
|
+
|
|
132
|
+
const commitValue = (next: string) => {
|
|
133
|
+
const sanitised = sanitize(next, mode, length);
|
|
134
|
+
|
|
135
|
+
if (isFormMode && bridge) {
|
|
136
|
+
bridge.setValue?.(name, sanitised);
|
|
137
|
+
void registration?.onChange?.({
|
|
138
|
+
target: { name, value: sanitised },
|
|
139
|
+
type: 'change',
|
|
140
|
+
});
|
|
141
|
+
} else if (userValue === undefined) {
|
|
142
|
+
setUncontrolledValue(sanitised);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
userOnChange?.(sanitised);
|
|
146
|
+
|
|
147
|
+
if (sanitised.length === length && onComplete) {
|
|
148
|
+
onComplete(sanitised);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
153
|
+
commitValue(e.target.value);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
const pasted = e.clipboardData.getData('text');
|
|
159
|
+
commitValue(pasted);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
163
|
+
setIsFocused(false);
|
|
164
|
+
if (isFormMode && registration?.onBlur) {
|
|
165
|
+
registration.onBlur(e);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Compute the active slot index (cursor position) for the visual
|
|
170
|
+
// highlight: it's the next empty slot, or the last slot if full.
|
|
171
|
+
const activeSlotIndex = Math.min(resolvedValue.length, length - 1);
|
|
172
|
+
const inputMode = mode === 'numeric' ? 'numeric' : 'text';
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className={cn(v.root(), sx, slotProps?.root?.className)}>
|
|
176
|
+
{label && (
|
|
177
|
+
<label
|
|
178
|
+
id={labelId}
|
|
179
|
+
htmlFor={inputId}
|
|
180
|
+
className={cn(v.label(), slotProps?.label?.className)}
|
|
181
|
+
>
|
|
182
|
+
{label}
|
|
183
|
+
{required && (
|
|
184
|
+
<span
|
|
185
|
+
aria-hidden="true"
|
|
186
|
+
className={cn(v.requiredMark(), slotProps?.requiredMark?.className)}
|
|
187
|
+
>
|
|
188
|
+
*
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
</label>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
<div className={cn(v.slotsRow(), slotProps?.slotsRow?.className)}>
|
|
195
|
+
{Array.from({ length }, (_, i) => {
|
|
196
|
+
const char = resolvedValue[i] ?? '';
|
|
197
|
+
const isActive = isFocused && !effectiveDisabled && i === activeSlotIndex;
|
|
198
|
+
return (
|
|
199
|
+
<div
|
|
200
|
+
key={i}
|
|
201
|
+
data-active={isActive || undefined}
|
|
202
|
+
data-disabled={effectiveDisabled || undefined}
|
|
203
|
+
className={cn(v.slot(), slotProps?.slot?.className)}
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
>
|
|
206
|
+
<span className={cn(v.slotChar(), slotProps?.slotChar?.className)}>
|
|
207
|
+
{char}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
})}
|
|
212
|
+
<input
|
|
213
|
+
id={inputId}
|
|
214
|
+
name={name}
|
|
215
|
+
type="text"
|
|
216
|
+
inputMode={inputMode}
|
|
217
|
+
autoComplete="one-time-code"
|
|
218
|
+
maxLength={length}
|
|
219
|
+
value={resolvedValue}
|
|
220
|
+
disabled={effectiveDisabled}
|
|
221
|
+
readOnly={effectiveReadOnly}
|
|
222
|
+
required={required}
|
|
223
|
+
aria-labelledby={label ? labelId : undefined}
|
|
224
|
+
aria-invalid={resolvedError || undefined}
|
|
225
|
+
aria-describedby={resolvedHelperText ? helperId : undefined}
|
|
226
|
+
onChange={handleChange}
|
|
227
|
+
onPaste={handlePaste}
|
|
228
|
+
onFocus={() => setIsFocused(true)}
|
|
229
|
+
onBlur={handleBlur}
|
|
230
|
+
ref={registration?.ref as React.Ref<HTMLInputElement> | undefined}
|
|
231
|
+
className={cn(v.hiddenInput(), slotProps?.hiddenInput?.className)}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{resolvedHelperText && (
|
|
236
|
+
<p
|
|
237
|
+
id={helperId}
|
|
238
|
+
className={cn(
|
|
239
|
+
resolvedError ? v.errorText() : v.helperText(),
|
|
240
|
+
resolvedError
|
|
241
|
+
? slotProps?.errorText?.className
|
|
242
|
+
: slotProps?.helperText?.className
|
|
243
|
+
)}
|
|
244
|
+
>
|
|
245
|
+
{resolvedHelperText}
|
|
246
|
+
</p>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Engine } from '@dashforge/ui-core';
|
|
3
|
+
import type { AccessRequirement } from '@dashforge/rbac';
|
|
4
|
+
import type { OTPFieldVariants } from './otpField.variants.js';
|
|
5
|
+
|
|
6
|
+
export type OTPFieldMode = 'numeric' | 'alphanumeric';
|
|
7
|
+
|
|
8
|
+
export interface OTPFieldSlotProps {
|
|
9
|
+
root?: { className?: string };
|
|
10
|
+
label?: { className?: string };
|
|
11
|
+
requiredMark?: { className?: string };
|
|
12
|
+
slotsRow?: { className?: string };
|
|
13
|
+
slot?: { className?: string };
|
|
14
|
+
slotChar?: { className?: string };
|
|
15
|
+
hiddenInput?: { className?: string };
|
|
16
|
+
helperText?: { className?: string };
|
|
17
|
+
errorText?: { className?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Props for `<OTPField>`.
|
|
22
|
+
*
|
|
23
|
+
* Storage contract: bridge value is a single string (e.g. `"123456"`).
|
|
24
|
+
* The user types characters that the component sanitises per `mode`:
|
|
25
|
+
* - `'numeric'` → `[0-9]`
|
|
26
|
+
* - `'alphanumeric'` → `[A-Za-z0-9]`
|
|
27
|
+
* Anything else is dropped. Paste content is sanitised the same way and
|
|
28
|
+
* fills slots sequentially.
|
|
29
|
+
*/
|
|
30
|
+
export interface OTPFieldProps extends OTPFieldVariants {
|
|
31
|
+
name: string;
|
|
32
|
+
rules?: unknown;
|
|
33
|
+
label?: ReactNode;
|
|
34
|
+
helperText?: ReactNode;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
error?: boolean;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Engine predicate — field not rendered when it returns `false`. */
|
|
39
|
+
visibleWhen?: (engine: Engine) => boolean;
|
|
40
|
+
/** RBAC access requirement (combines with explicit `disabled`). */
|
|
41
|
+
access?: AccessRequirement;
|
|
42
|
+
/** Root className shortcut. */
|
|
43
|
+
sx?: string;
|
|
44
|
+
/** Per-slot className overrides. */
|
|
45
|
+
slotProps?: OTPFieldSlotProps;
|
|
46
|
+
/** Number of slots (default 6 — the SMS code convention). */
|
|
47
|
+
length?: number;
|
|
48
|
+
/** Character set allowed. Default `'numeric'`. */
|
|
49
|
+
mode?: OTPFieldMode;
|
|
50
|
+
/** Controlled value (form mode reads from the bridge if omitted). */
|
|
51
|
+
value?: string;
|
|
52
|
+
/** Default value (uncontrolled, standalone mode only). */
|
|
53
|
+
defaultValue?: string;
|
|
54
|
+
/** Fires every time the joined value changes (sanitised, ≤ length). */
|
|
55
|
+
onChange?: (value: string) => void;
|
|
56
|
+
/** Fires when the user has filled all slots. */
|
|
57
|
+
onComplete?: (value: string) => void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tailwind-variants recipe for `<OTPField>`.
|
|
5
|
+
*
|
|
6
|
+
* Slots:
|
|
7
|
+
* - `root` — outer wrapper (label + slots row + helper/error)
|
|
8
|
+
* - `label` — group-level label
|
|
9
|
+
* - `requiredMark` — red `*` for required fields
|
|
10
|
+
* - `slotsRow` — flex row holding the visible slot boxes
|
|
11
|
+
* - `slot` — single slot cell (visual representation of a char)
|
|
12
|
+
* - `slotChar` — the character displayed inside a slot
|
|
13
|
+
* - `hiddenInput` — the actual <input> absorbing keystrokes (visually
|
|
14
|
+
* hidden but focusable + reads as a single
|
|
15
|
+
* one-line text input to AT)
|
|
16
|
+
* - `helperText` — descriptive line below
|
|
17
|
+
* - `errorText` — semantic counterpart of helperText for error mode
|
|
18
|
+
*
|
|
19
|
+
* Variants:
|
|
20
|
+
* - `size` : sm / md / lg — drives slot box dimensions + text size
|
|
21
|
+
* - `error` : bool — swaps slot border + helperText color to danger tones
|
|
22
|
+
*/
|
|
23
|
+
export const otpFieldVariants = tv({
|
|
24
|
+
slots: {
|
|
25
|
+
root: 'flex flex-col gap-1.5',
|
|
26
|
+
label: 'block text-sm font-medium text-neutral-900',
|
|
27
|
+
requiredMark: 'text-danger-500 ml-0.5',
|
|
28
|
+
slotsRow: 'relative inline-flex items-center gap-2',
|
|
29
|
+
slot: [
|
|
30
|
+
'flex items-center justify-center',
|
|
31
|
+
// bg-neutral-50 inverts — dark-mode aware via the dashforge preset.
|
|
32
|
+
'rounded-md border bg-neutral-50',
|
|
33
|
+
'border-neutral-300',
|
|
34
|
+
'text-neutral-900 font-mono',
|
|
35
|
+
'transition-colors',
|
|
36
|
+
'data-[active=true]:border-primary-500 data-[active=true]:ring-2 data-[active=true]:ring-primary-500/30',
|
|
37
|
+
'data-[disabled=true]:opacity-60 data-[disabled=true]:cursor-not-allowed',
|
|
38
|
+
],
|
|
39
|
+
slotChar: 'select-none',
|
|
40
|
+
hiddenInput: [
|
|
41
|
+
'absolute inset-0 w-full h-full opacity-0',
|
|
42
|
+
'cursor-text outline-none',
|
|
43
|
+
'disabled:cursor-not-allowed',
|
|
44
|
+
],
|
|
45
|
+
helperText: 'text-sm text-neutral-600',
|
|
46
|
+
errorText: 'text-sm text-danger-600',
|
|
47
|
+
},
|
|
48
|
+
variants: {
|
|
49
|
+
size: {
|
|
50
|
+
sm: {
|
|
51
|
+
slot: 'h-9 w-7 text-base',
|
|
52
|
+
},
|
|
53
|
+
md: {
|
|
54
|
+
slot: 'h-11 w-9 text-lg',
|
|
55
|
+
},
|
|
56
|
+
lg: {
|
|
57
|
+
slot: 'h-14 w-11 text-2xl',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
error: {
|
|
61
|
+
true: {
|
|
62
|
+
slot: 'border-danger-500 data-[active=true]:border-danger-500 data-[active=true]:ring-danger-500/30',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
defaultVariants: {
|
|
67
|
+
size: 'md',
|
|
68
|
+
error: false,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type OTPFieldVariants = VariantProps<typeof otpFieldVariants>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { render, screen, fireEvent, within } from '@testing-library/react';
|
|
4
|
+
import { DashFormProvider } from '@dashforge/forms';
|
|
5
|
+
import { RadioGroup } from './RadioGroup.js';
|
|
6
|
+
import type { RadioGroupOption } from './radioGroup.types.js';
|
|
7
|
+
|
|
8
|
+
const colors: RadioGroupOption[] = [
|
|
9
|
+
{ value: 'red', label: 'Red' },
|
|
10
|
+
{ value: 'green', label: 'Green' },
|
|
11
|
+
{ value: 'blue', label: 'Blue' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe('<RadioGroup>', () => {
|
|
15
|
+
describe('standalone (no DashForm)', () => {
|
|
16
|
+
it('renders all options with the provided labels', () => {
|
|
17
|
+
render(
|
|
18
|
+
<RadioGroup name="color" label="Pick a color" options={colors} />
|
|
19
|
+
);
|
|
20
|
+
// Group label rendered as the labelledby anchor
|
|
21
|
+
expect(screen.getByText('Pick a color')).toBeTruthy();
|
|
22
|
+
// Each option label resolves through the htmlFor → button id link
|
|
23
|
+
for (const opt of colors) {
|
|
24
|
+
expect(screen.getByLabelText(opt.label as string)).toBeTruthy();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('selects the default value via uncontrolled `defaultValue`', () => {
|
|
29
|
+
render(
|
|
30
|
+
<RadioGroup
|
|
31
|
+
name="color"
|
|
32
|
+
options={colors}
|
|
33
|
+
defaultValue="green"
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
const radios = screen.getAllByRole('radio');
|
|
37
|
+
// The second option corresponds to "green"
|
|
38
|
+
expect(radios[1].getAttribute('data-state')).toBe('checked');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('fires onValueChange on selection', () => {
|
|
42
|
+
const onValueChange = vi.fn();
|
|
43
|
+
render(
|
|
44
|
+
<RadioGroup
|
|
45
|
+
name="color"
|
|
46
|
+
options={colors}
|
|
47
|
+
onValueChange={onValueChange}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
fireEvent.click(screen.getByLabelText('Blue'));
|
|
51
|
+
expect(onValueChange).toHaveBeenCalledWith('blue');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('respects per-option `disabled`', () => {
|
|
55
|
+
const opts: RadioGroupOption[] = [
|
|
56
|
+
{ value: 'a', label: 'A' },
|
|
57
|
+
{ value: 'b', label: 'B', disabled: true },
|
|
58
|
+
];
|
|
59
|
+
render(<RadioGroup name="x" options={opts} />);
|
|
60
|
+
const radios = screen.getAllByRole('radio') as HTMLButtonElement[];
|
|
61
|
+
expect(radios[0].disabled).toBe(false);
|
|
62
|
+
expect(radios[1].disabled).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('disables ALL options when the group is disabled', () => {
|
|
66
|
+
render(
|
|
67
|
+
<RadioGroup name="color" options={colors} disabled />
|
|
68
|
+
);
|
|
69
|
+
for (const r of screen.getAllByRole('radio') as HTMLButtonElement[]) {
|
|
70
|
+
expect(r.disabled).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('inside DashFormProvider', () => {
|
|
76
|
+
it('renders without errors inside a bridge-providing tree', () => {
|
|
77
|
+
render(
|
|
78
|
+
<DashFormProvider defaultValues={{ color: 'green' }}>
|
|
79
|
+
<RadioGroup name="color" label="Color" options={colors} />
|
|
80
|
+
</DashFormProvider>
|
|
81
|
+
);
|
|
82
|
+
// Bridge mode mounts cleanly; all three options render. The exact
|
|
83
|
+
// checked-state reflection is verified end-to-end in the F4 smoke
|
|
84
|
+
// page in learn/dash where the full reactive cycle has time to
|
|
85
|
+
// settle — unit-test assertions on Radix internal state right
|
|
86
|
+
// after render are timing-sensitive and not worth the brittleness.
|
|
87
|
+
expect(screen.getAllByRole('radio')).toHaveLength(3);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renders the helperText when provided', () => {
|
|
91
|
+
render(
|
|
92
|
+
<DashFormProvider defaultValues={{ color: '' }}>
|
|
93
|
+
<RadioGroup
|
|
94
|
+
name="color"
|
|
95
|
+
options={colors}
|
|
96
|
+
helperText="Choose carefully"
|
|
97
|
+
/>
|
|
98
|
+
</DashFormProvider>
|
|
99
|
+
);
|
|
100
|
+
expect(screen.getByText('Choose carefully')).toBeTruthy();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('layout variant', () => {
|
|
105
|
+
it('default `stacked` renders options vertically (flex-col)', () => {
|
|
106
|
+
const { container } = render(
|
|
107
|
+
<RadioGroup name="x" options={colors} />
|
|
108
|
+
);
|
|
109
|
+
const list = within(container).getByRole('radiogroup');
|
|
110
|
+
expect(list.className).toMatch(/flex-col/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('`row` layout renders options horizontally', () => {
|
|
114
|
+
const { container } = render(
|
|
115
|
+
<RadioGroup name="x" options={colors} layout="row" />
|
|
116
|
+
);
|
|
117
|
+
const list = within(container).getByRole('radiogroup');
|
|
118
|
+
expect(list.className).toMatch(/flex-row/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('required marker', () => {
|
|
123
|
+
it('renders a red `*` after the label when `required` is set', () => {
|
|
124
|
+
render(
|
|
125
|
+
<RadioGroup
|
|
126
|
+
name="x"
|
|
127
|
+
options={colors}
|
|
128
|
+
label="Color"
|
|
129
|
+
required
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
expect(screen.getByText('*')).toBeTruthy();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('does NOT render a `*` when `required` is omitted', () => {
|
|
136
|
+
render(<RadioGroup name="x" options={colors} label="Color" />);
|
|
137
|
+
expect(screen.queryByText('*')).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|