@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,216 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useId, useRef } 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 { textFieldVariants } from './textField.variants.js';
|
|
9
|
+
import type { TextFieldProps } from './textField.types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dashforge TW TextField — bridge-integrated text input.
|
|
13
|
+
*
|
|
14
|
+
* Implementation mirrors `@dashforge/ui/TextField` (MUI) at the bridge
|
|
15
|
+
* level — same `name` / `rules` / `visibleWhen` / `access` semantics,
|
|
16
|
+
* same validation gating, same StrictMode-safe unregister pattern.
|
|
17
|
+
* Renderer differences:
|
|
18
|
+
*
|
|
19
|
+
* - Native `<input>` element wrapped in a TV-styled
|
|
20
|
+
* `<div role-less wrapper>` so the focus ring extends to the entire
|
|
21
|
+
* field (consistent with shadcn-style inputs).
|
|
22
|
+
* - Layout modes: `stacked` (default — label above) and `inline`
|
|
23
|
+
* (label left of input). MUI's `floating` mode is deferred to a
|
|
24
|
+
* follow-up sprint; consumers needing the visual can layer it via
|
|
25
|
+
* `slotProps`.
|
|
26
|
+
* - No Select-integration hook (Select lands in F4 as its own component).
|
|
27
|
+
*
|
|
28
|
+
* **A11y**:
|
|
29
|
+
* - `aria-invalid` is set from the resolved validation state.
|
|
30
|
+
* - `aria-describedby` links the input to the helper/error text node.
|
|
31
|
+
* - Required fields get the native `required` attribute + a visual `*`.
|
|
32
|
+
*/
|
|
33
|
+
export function TextField(props: TextFieldProps) {
|
|
34
|
+
const {
|
|
35
|
+
name,
|
|
36
|
+
rules,
|
|
37
|
+
visibleWhen,
|
|
38
|
+
layout = 'stacked',
|
|
39
|
+
size,
|
|
40
|
+
label,
|
|
41
|
+
helperText,
|
|
42
|
+
required,
|
|
43
|
+
error,
|
|
44
|
+
disabled,
|
|
45
|
+
fullWidth,
|
|
46
|
+
access,
|
|
47
|
+
sx,
|
|
48
|
+
slotProps,
|
|
49
|
+
placeholder,
|
|
50
|
+
type,
|
|
51
|
+
onChange: userOnChange,
|
|
52
|
+
onBlur: userOnBlur,
|
|
53
|
+
value: userValue,
|
|
54
|
+
defaultValue,
|
|
55
|
+
...rest
|
|
56
|
+
} = props;
|
|
57
|
+
|
|
58
|
+
// ───── Hooks (unconditional) ─────
|
|
59
|
+
const bridge = useContext(DashFormContext) as DashFormBridge | null;
|
|
60
|
+
const isVisible = useEngineVisibility(bridge?.engine, visibleWhen);
|
|
61
|
+
// Granular per-field subscription. We READ from `fieldMeta.value`
|
|
62
|
+
// (reactive snapshot) rather than `bridge.getValue(name)` (eager
|
|
63
|
+
// read with no subscription) so React re-renders this field on its
|
|
64
|
+
// own state mutations — the canonical pattern, mirrors the MUI side.
|
|
65
|
+
const fieldMeta = useDashFieldMeta(name);
|
|
66
|
+
const accessState = useAccessState(access);
|
|
67
|
+
|
|
68
|
+
const inputId = useId();
|
|
69
|
+
const helperId = `${inputId}-help`;
|
|
70
|
+
|
|
71
|
+
// StrictMode-safe unregister-on-unmount
|
|
72
|
+
const unregisterRef = useRef({ bridge, name });
|
|
73
|
+
unregisterRef.current = { bridge, name };
|
|
74
|
+
const isMountedRef = useRef(false);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
isMountedRef.current = true;
|
|
78
|
+
return () => {
|
|
79
|
+
isMountedRef.current = false;
|
|
80
|
+
const { bridge: cap, name: capName } = unregisterRef.current;
|
|
81
|
+
queueMicrotask(() => {
|
|
82
|
+
if (!isMountedRef.current) cap?.unregister?.(capName);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
// ───── Derived (every hook MUST be called above the early returns) ─────
|
|
88
|
+
const effectiveDisabled =
|
|
89
|
+
Boolean(disabled) || accessState.disabled;
|
|
90
|
+
const effectiveReadOnly = accessState.readonly;
|
|
91
|
+
|
|
92
|
+
const isFormMode = Boolean(bridge?.register);
|
|
93
|
+
|
|
94
|
+
let resolvedError = error;
|
|
95
|
+
let resolvedHelperText: typeof helperText = helperText;
|
|
96
|
+
let registration: FieldRegistration | null = null;
|
|
97
|
+
let resolvedValue: string | number | readonly string[] | undefined = userValue as
|
|
98
|
+
| string
|
|
99
|
+
| number
|
|
100
|
+
| readonly string[]
|
|
101
|
+
| undefined;
|
|
102
|
+
|
|
103
|
+
if (isFormMode && bridge) {
|
|
104
|
+
registration = bridge.register(name, rules);
|
|
105
|
+
const validation = resolveValidationState(name, bridge, error, helperText);
|
|
106
|
+
resolvedError = validation.error;
|
|
107
|
+
resolvedHelperText = validation.helperText;
|
|
108
|
+
if (userValue === undefined) {
|
|
109
|
+
// Read the reactive snapshot from `useDashFieldMeta` — NOT
|
|
110
|
+
// `bridge.getValue` — so the component re-renders on every
|
|
111
|
+
// per-field state change without the unsubscribe trick the
|
|
112
|
+
// legacy bridge used.
|
|
113
|
+
const bv = fieldMeta.value;
|
|
114
|
+
resolvedValue =
|
|
115
|
+
bv == null ? '' : (bv as string | number);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
120
|
+
if (isFormMode && bridge) {
|
|
121
|
+
bridge.setValue?.(name, e.target.value);
|
|
122
|
+
void registration?.onChange?.(e);
|
|
123
|
+
}
|
|
124
|
+
userOnChange?.(e);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
128
|
+
if (isFormMode && bridge) {
|
|
129
|
+
registration?.onBlur?.(e);
|
|
130
|
+
}
|
|
131
|
+
userOnBlur?.(e);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// In form mode, RHF supplies its own callback ref via registration.
|
|
135
|
+
// Wrap it in a stable callback so React 19 unambiguously treats it as
|
|
136
|
+
// a callback ref (and to give us a single place to extend the ref
|
|
137
|
+
// chain in the future — e.g. for a user-supplied `ref` prop via
|
|
138
|
+
// `forwardRef`).
|
|
139
|
+
const registrationRefFn = registration?.ref;
|
|
140
|
+
const inputRef = useCallback(
|
|
141
|
+
(instance: HTMLInputElement | null) => {
|
|
142
|
+
if (typeof registrationRefFn === 'function') {
|
|
143
|
+
registrationRefFn(instance);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[registrationRefFn]
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// ───── Render-time guards (after all hooks) ─────
|
|
150
|
+
if (!isVisible) return null;
|
|
151
|
+
if (!accessState.visible) return null;
|
|
152
|
+
|
|
153
|
+
const v = textFieldVariants({
|
|
154
|
+
size,
|
|
155
|
+
layout,
|
|
156
|
+
error: resolvedError,
|
|
157
|
+
fullWidth,
|
|
158
|
+
disabled: effectiveDisabled,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className={cn(v.root(), sx, slotProps?.root?.className)}>
|
|
163
|
+
{label && (
|
|
164
|
+
<label
|
|
165
|
+
htmlFor={inputId}
|
|
166
|
+
className={cn(v.label(), slotProps?.label?.className)}
|
|
167
|
+
>
|
|
168
|
+
{label}
|
|
169
|
+
{required && (
|
|
170
|
+
<span
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
className={cn(v.requiredMark(), slotProps?.requiredMark?.className)}
|
|
173
|
+
>
|
|
174
|
+
*
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</label>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<div className={cn(v.inputWrapper(), slotProps?.inputWrapper?.className)}>
|
|
181
|
+
<input
|
|
182
|
+
{...rest}
|
|
183
|
+
id={inputId}
|
|
184
|
+
name={name}
|
|
185
|
+
type={type ?? 'text'}
|
|
186
|
+
placeholder={placeholder}
|
|
187
|
+
value={isFormMode ? (resolvedValue as string | number | readonly string[]) ?? '' : userValue}
|
|
188
|
+
defaultValue={!isFormMode && userValue === undefined ? defaultValue : undefined}
|
|
189
|
+
disabled={effectiveDisabled}
|
|
190
|
+
readOnly={effectiveReadOnly}
|
|
191
|
+
required={required}
|
|
192
|
+
aria-invalid={resolvedError || undefined}
|
|
193
|
+
aria-describedby={resolvedHelperText ? helperId : undefined}
|
|
194
|
+
onChange={handleChange}
|
|
195
|
+
onBlur={handleBlur}
|
|
196
|
+
ref={inputRef}
|
|
197
|
+
className={cn(v.input(), slotProps?.input?.className)}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{resolvedHelperText && (
|
|
202
|
+
<p
|
|
203
|
+
id={helperId}
|
|
204
|
+
className={cn(
|
|
205
|
+
resolvedError ? v.errorText() : v.helperText(),
|
|
206
|
+
resolvedError
|
|
207
|
+
? slotProps?.errorText?.className
|
|
208
|
+
: slotProps?.helperText?.className
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
{resolvedHelperText}
|
|
212
|
+
</p>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { AccessRequirement } from '@dashforge/rbac';
|
|
3
|
+
import type { Engine } from '@dashforge/ui-core';
|
|
4
|
+
import type { TextFieldVariants } from './textField.variants.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-slot overrides for `<TextField>`.
|
|
8
|
+
*
|
|
9
|
+
* Each slot accepts `{ className: string }` so the override path is
|
|
10
|
+
* extensible (style / aria-* / data-* can be added without a breaking
|
|
11
|
+
* change). Mirrors the MUI-side `slotProps` shape.
|
|
12
|
+
*/
|
|
13
|
+
export interface TextFieldSlotProps {
|
|
14
|
+
root?: { className?: string };
|
|
15
|
+
label?: { className?: string };
|
|
16
|
+
requiredMark?: { className?: string };
|
|
17
|
+
inputWrapper?: { className?: string };
|
|
18
|
+
input?: { className?: string };
|
|
19
|
+
helperText?: { className?: string };
|
|
20
|
+
errorText?: { className?: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Props for the Dashforge TW `<TextField>`.
|
|
25
|
+
*
|
|
26
|
+
* Extends the native `<input>` attributes (so `placeholder`, `type`,
|
|
27
|
+
* `autoComplete`, `aria-*` etc. all flow through), narrowed in two
|
|
28
|
+
* places:
|
|
29
|
+
*
|
|
30
|
+
* - `size` is omitted because HTML's legacy `size` attribute (number)
|
|
31
|
+
* collides with our variant axis (`'sm' | 'md' | 'lg'`).
|
|
32
|
+
* - `className` is omitted — use `sx` instead (the canonical override
|
|
33
|
+
* path that wins over variant classes via `tailwind-merge`).
|
|
34
|
+
*
|
|
35
|
+
* Plus the variant axes and Dashforge form-bridge wiring (`name`,
|
|
36
|
+
* `rules`, `visibleWhen`, `access`).
|
|
37
|
+
*/
|
|
38
|
+
export interface TextFieldProps
|
|
39
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'className'>,
|
|
40
|
+
Pick<TextFieldVariants, 'size' | 'layout' | 'fullWidth'> {
|
|
41
|
+
/** Bridge field name (required when used inside `DashFormProvider`). */
|
|
42
|
+
name: string;
|
|
43
|
+
|
|
44
|
+
/** Visible label. */
|
|
45
|
+
label?: ReactNode;
|
|
46
|
+
|
|
47
|
+
/** Forwarded to the bridge as RHF rules — opaque to this component. */
|
|
48
|
+
rules?: unknown;
|
|
49
|
+
|
|
50
|
+
/** Engine predicate. Component returns null when false. */
|
|
51
|
+
visibleWhen?: (engine: Engine) => boolean;
|
|
52
|
+
|
|
53
|
+
/** Helper line below the input. */
|
|
54
|
+
helperText?: ReactNode;
|
|
55
|
+
|
|
56
|
+
/** Explicit error semaphore. Overrides bridge auto-error. */
|
|
57
|
+
error?: boolean;
|
|
58
|
+
|
|
59
|
+
/** Render the asterisk + set the native `required` attribute. */
|
|
60
|
+
required?: boolean;
|
|
61
|
+
|
|
62
|
+
/** RBAC requirement. */
|
|
63
|
+
access?: AccessRequirement;
|
|
64
|
+
|
|
65
|
+
/** Root-level Tailwind override — wins over variant classes via `cn()`. */
|
|
66
|
+
sx?: string;
|
|
67
|
+
|
|
68
|
+
/** Per-slot overrides — see `TextFieldSlotProps`. */
|
|
69
|
+
slotProps?: TextFieldSlotProps;
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tailwind-variants recipe for `<TextField>` with 7 slots.
|
|
5
|
+
*
|
|
6
|
+
* The MUI-side `TextField` supports three layout modes (floating /
|
|
7
|
+
* stacked / inline) plus a Select-integration hook. The TW-side
|
|
8
|
+
* port covers **stacked** + **inline** in F3 (label outside the
|
|
9
|
+
* input wrapper) and ships a CSS-only floating label using
|
|
10
|
+
* Tailwind's `peer` modifier.
|
|
11
|
+
*
|
|
12
|
+
* The Select integration (`__selectAvailableValues` etc.) is
|
|
13
|
+
* deferred to F4 when the dedicated `<Select>` component arrives.
|
|
14
|
+
*/
|
|
15
|
+
export const textFieldVariants = tv({
|
|
16
|
+
slots: {
|
|
17
|
+
root: 'flex',
|
|
18
|
+
label: 'block text-sm font-medium text-neutral-900 mb-1',
|
|
19
|
+
requiredMark: 'text-danger-500 ml-0.5',
|
|
20
|
+
inputWrapper: [
|
|
21
|
+
'relative flex items-center',
|
|
22
|
+
// bg-neutral-50 inverts via @dashforge/tw-tokens: #fafafa in light,
|
|
23
|
+
// #0a0a0a in dark. Same class, dark-mode aware automatically.
|
|
24
|
+
'rounded-md border bg-neutral-50',
|
|
25
|
+
'border-neutral-300',
|
|
26
|
+
'transition-colors',
|
|
27
|
+
'focus-within:border-primary-500 focus-within:ring-2 focus-within:ring-primary-500/30',
|
|
28
|
+
],
|
|
29
|
+
input: [
|
|
30
|
+
'block w-full bg-transparent outline-none',
|
|
31
|
+
'text-neutral-900 placeholder:text-neutral-400',
|
|
32
|
+
'disabled:cursor-not-allowed disabled:opacity-60',
|
|
33
|
+
'read-only:cursor-default',
|
|
34
|
+
],
|
|
35
|
+
helperText: 'mt-1 text-sm text-neutral-600',
|
|
36
|
+
errorText: 'mt-1 text-sm text-danger-600',
|
|
37
|
+
},
|
|
38
|
+
variants: {
|
|
39
|
+
size: {
|
|
40
|
+
sm: {
|
|
41
|
+
inputWrapper: 'h-8 px-2',
|
|
42
|
+
input: 'text-sm',
|
|
43
|
+
},
|
|
44
|
+
md: {
|
|
45
|
+
inputWrapper: 'h-10 px-3',
|
|
46
|
+
input: 'text-base',
|
|
47
|
+
},
|
|
48
|
+
lg: {
|
|
49
|
+
inputWrapper: 'h-12 px-4',
|
|
50
|
+
input: 'text-lg',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
layout: {
|
|
54
|
+
stacked: { root: 'flex-col' },
|
|
55
|
+
inline: { root: 'flex-row items-start gap-3', label: 'mb-0 pt-2 whitespace-nowrap shrink-0' },
|
|
56
|
+
},
|
|
57
|
+
error: {
|
|
58
|
+
true: {
|
|
59
|
+
inputWrapper:
|
|
60
|
+
'border-danger-500 focus-within:border-danger-500 focus-within:ring-danger-500/30',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
fullWidth: {
|
|
64
|
+
true: {
|
|
65
|
+
root: 'w-full',
|
|
66
|
+
inputWrapper: 'w-full',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
disabled: {
|
|
70
|
+
true: {
|
|
71
|
+
// neutral-200 is a darker shade of the default neutral-50 surface
|
|
72
|
+
// — visually distinguishes the disabled input from the active one
|
|
73
|
+
// in both light and dark modes.
|
|
74
|
+
inputWrapper: 'opacity-60 cursor-not-allowed bg-neutral-200',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
defaultVariants: {
|
|
79
|
+
size: 'md',
|
|
80
|
+
layout: 'stacked',
|
|
81
|
+
error: false,
|
|
82
|
+
fullWidth: false,
|
|
83
|
+
disabled: false,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export type TextFieldVariants = VariantProps<typeof textFieldVariants>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import { DashFormProvider } from '@dashforge/forms';
|
|
5
|
+
import { Textarea } from './Textarea.js';
|
|
6
|
+
|
|
7
|
+
describe('<Textarea>', () => {
|
|
8
|
+
describe('standalone (no DashForm)', () => {
|
|
9
|
+
it('renders a <textarea> element with the label', () => {
|
|
10
|
+
render(<Textarea name="bio" label="Bio" />);
|
|
11
|
+
const el = screen.getByLabelText('Bio');
|
|
12
|
+
expect(el).toBeInstanceOf(HTMLTextAreaElement);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('respects the `rows` attribute (default 3, overridable)', () => {
|
|
16
|
+
const { rerender } = render(<Textarea name="bio" label="Bio" />);
|
|
17
|
+
expect(screen.getByLabelText('Bio').getAttribute('rows')).toBe('3');
|
|
18
|
+
rerender(<Textarea name="bio" label="Bio" rows={6} />);
|
|
19
|
+
expect(screen.getByLabelText('Bio').getAttribute('rows')).toBe('6');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders helperText when provided', () => {
|
|
23
|
+
render(
|
|
24
|
+
<Textarea name="bio" label="Bio" helperText="Tell us about yourself" />
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText('Tell us about yourself')).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders a `*` for required fields', () => {
|
|
30
|
+
render(<Textarea name="bio" label="Bio" required />);
|
|
31
|
+
expect(screen.getByText('*')).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('fires onChange when typed into', () => {
|
|
35
|
+
const onChange = vi.fn();
|
|
36
|
+
render(<Textarea name="bio" label="Bio" onChange={onChange} />);
|
|
37
|
+
fireEvent.change(screen.getByLabelText('Bio'), {
|
|
38
|
+
target: { value: 'hello' },
|
|
39
|
+
});
|
|
40
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('applies the `disabled` attribute on the textarea', () => {
|
|
44
|
+
render(<Textarea name="bio" label="Bio" disabled />);
|
|
45
|
+
const el = screen.getByLabelText('Bio') as HTMLTextAreaElement;
|
|
46
|
+
expect(el.disabled).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('inside DashFormProvider', () => {
|
|
51
|
+
it('reflects the typed value on the textarea element', () => {
|
|
52
|
+
render(
|
|
53
|
+
<DashFormProvider defaultValues={{ bio: '' }}>
|
|
54
|
+
<Textarea name="bio" label="Bio" />
|
|
55
|
+
</DashFormProvider>
|
|
56
|
+
);
|
|
57
|
+
const el = screen.getByLabelText('Bio') as HTMLTextAreaElement;
|
|
58
|
+
fireEvent.change(el, { target: { value: 'A short bio.' } });
|
|
59
|
+
// The bridge updates synchronously; the rendered value mirrors it.
|
|
60
|
+
expect(el.value).toBe('A short bio.');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('resize variant', () => {
|
|
65
|
+
it('default `vertical` adds resize-y', () => {
|
|
66
|
+
render(<Textarea name="bio" label="Bio" />);
|
|
67
|
+
expect(screen.getByLabelText('Bio').className).toMatch(/resize-y/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('`none` adds resize-none', () => {
|
|
71
|
+
render(<Textarea name="bio" label="Bio" resize="none" />);
|
|
72
|
+
expect(screen.getByLabelText('Bio').className).toMatch(/resize-none/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useId, useRef } 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 { textareaVariants } from './textarea.variants.js';
|
|
9
|
+
import type { TextareaProps } from './textarea.types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dashforge TW Textarea — bridge-integrated multi-line input.
|
|
13
|
+
*
|
|
14
|
+
* Sibling of `<TextField>` (same slot taxonomy, same bridge wiring) but
|
|
15
|
+
* renders a `<textarea>` element instead of a single-line `<input>`.
|
|
16
|
+
*
|
|
17
|
+
* Defaults to `rows={3}` and `resize="vertical"` — both overridable via
|
|
18
|
+
* variant prop or per-instance `rows` attribute.
|
|
19
|
+
*
|
|
20
|
+
* **A11y**: `aria-invalid` from the resolved validation state,
|
|
21
|
+
* `aria-describedby` links to the helper/error text node, required
|
|
22
|
+
* fields get the native `required` attribute + a visual `*`.
|
|
23
|
+
*/
|
|
24
|
+
export function Textarea(props: TextareaProps) {
|
|
25
|
+
const {
|
|
26
|
+
name,
|
|
27
|
+
rules,
|
|
28
|
+
visibleWhen,
|
|
29
|
+
layout = 'stacked',
|
|
30
|
+
size,
|
|
31
|
+
resize,
|
|
32
|
+
label,
|
|
33
|
+
helperText,
|
|
34
|
+
required,
|
|
35
|
+
error,
|
|
36
|
+
disabled,
|
|
37
|
+
fullWidth,
|
|
38
|
+
access,
|
|
39
|
+
sx,
|
|
40
|
+
slotProps,
|
|
41
|
+
placeholder,
|
|
42
|
+
rows = 3,
|
|
43
|
+
onChange: userOnChange,
|
|
44
|
+
onBlur: userOnBlur,
|
|
45
|
+
value: userValue,
|
|
46
|
+
defaultValue,
|
|
47
|
+
...rest
|
|
48
|
+
} = props;
|
|
49
|
+
|
|
50
|
+
// ───── Hooks (unconditional) ─────
|
|
51
|
+
const bridge = useContext(DashFormContext) as DashFormBridge | null;
|
|
52
|
+
const isVisible = useEngineVisibility(bridge?.engine, visibleWhen);
|
|
53
|
+
const fieldMeta = useDashFieldMeta(name);
|
|
54
|
+
const accessState = useAccessState(access);
|
|
55
|
+
|
|
56
|
+
const inputId = useId();
|
|
57
|
+
const helperId = `${inputId}-help`;
|
|
58
|
+
|
|
59
|
+
// StrictMode-safe unregister-on-unmount
|
|
60
|
+
const unregisterRef = useRef({ bridge, name });
|
|
61
|
+
unregisterRef.current = { bridge, name };
|
|
62
|
+
const isMountedRef = useRef(false);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
isMountedRef.current = true;
|
|
66
|
+
return () => {
|
|
67
|
+
isMountedRef.current = false;
|
|
68
|
+
const { bridge: cap, name: capName } = unregisterRef.current;
|
|
69
|
+
queueMicrotask(() => {
|
|
70
|
+
if (!isMountedRef.current) cap?.unregister?.(capName);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
// ───── Derived (every hook MUST be called above the early returns) ─────
|
|
76
|
+
const effectiveDisabled = Boolean(disabled) || accessState.disabled;
|
|
77
|
+
const effectiveReadOnly = accessState.readonly;
|
|
78
|
+
|
|
79
|
+
const isFormMode = Boolean(bridge?.register);
|
|
80
|
+
|
|
81
|
+
let resolvedError = error;
|
|
82
|
+
let resolvedHelperText: typeof helperText = helperText;
|
|
83
|
+
let registration: FieldRegistration | null = null;
|
|
84
|
+
let resolvedValue: string | undefined = userValue;
|
|
85
|
+
|
|
86
|
+
if (isFormMode && bridge) {
|
|
87
|
+
registration = bridge.register(name, rules);
|
|
88
|
+
const validation = resolveValidationState(name, bridge, error, helperText);
|
|
89
|
+
resolvedError = validation.error;
|
|
90
|
+
resolvedHelperText = validation.helperText;
|
|
91
|
+
if (userValue === undefined) {
|
|
92
|
+
const bv = fieldMeta.value;
|
|
93
|
+
resolvedValue = bv == null ? '' : String(bv);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
98
|
+
if (isFormMode && bridge) {
|
|
99
|
+
bridge.setValue?.(name, e.target.value);
|
|
100
|
+
void registration?.onChange?.(e);
|
|
101
|
+
}
|
|
102
|
+
userOnChange?.(e);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
106
|
+
if (isFormMode && bridge) {
|
|
107
|
+
registration?.onBlur?.(e);
|
|
108
|
+
}
|
|
109
|
+
userOnBlur?.(e);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const registrationRefFn = registration?.ref;
|
|
113
|
+
const inputRef = useCallback(
|
|
114
|
+
(instance: HTMLTextAreaElement | null) => {
|
|
115
|
+
if (typeof registrationRefFn === 'function') {
|
|
116
|
+
registrationRefFn(instance);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[registrationRefFn]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// ───── Render-time guards (after all hooks) ─────
|
|
123
|
+
if (!isVisible) return null;
|
|
124
|
+
if (!accessState.visible) return null;
|
|
125
|
+
|
|
126
|
+
const v = textareaVariants({
|
|
127
|
+
size,
|
|
128
|
+
layout,
|
|
129
|
+
resize,
|
|
130
|
+
error: resolvedError,
|
|
131
|
+
fullWidth,
|
|
132
|
+
disabled: effectiveDisabled,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={cn(v.root(), sx, slotProps?.root?.className)}>
|
|
137
|
+
{label && (
|
|
138
|
+
<label
|
|
139
|
+
htmlFor={inputId}
|
|
140
|
+
className={cn(v.label(), slotProps?.label?.className)}
|
|
141
|
+
>
|
|
142
|
+
{label}
|
|
143
|
+
{required && (
|
|
144
|
+
<span
|
|
145
|
+
aria-hidden="true"
|
|
146
|
+
className={cn(v.requiredMark(), slotProps?.requiredMark?.className)}
|
|
147
|
+
>
|
|
148
|
+
*
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</label>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<div className={cn(v.inputWrapper(), slotProps?.inputWrapper?.className)}>
|
|
155
|
+
<textarea
|
|
156
|
+
{...rest}
|
|
157
|
+
id={inputId}
|
|
158
|
+
name={name}
|
|
159
|
+
rows={rows}
|
|
160
|
+
placeholder={placeholder}
|
|
161
|
+
value={isFormMode ? resolvedValue ?? '' : userValue}
|
|
162
|
+
defaultValue={!isFormMode && userValue === undefined ? defaultValue : undefined}
|
|
163
|
+
disabled={effectiveDisabled}
|
|
164
|
+
readOnly={effectiveReadOnly}
|
|
165
|
+
required={required}
|
|
166
|
+
aria-invalid={resolvedError || undefined}
|
|
167
|
+
aria-describedby={resolvedHelperText ? helperId : undefined}
|
|
168
|
+
onChange={handleChange}
|
|
169
|
+
onBlur={handleBlur}
|
|
170
|
+
ref={inputRef}
|
|
171
|
+
className={cn(v.input(), slotProps?.input?.className)}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{resolvedHelperText && (
|
|
176
|
+
<p
|
|
177
|
+
id={helperId}
|
|
178
|
+
className={cn(
|
|
179
|
+
resolvedError ? v.errorText() : v.helperText(),
|
|
180
|
+
resolvedError
|
|
181
|
+
? slotProps?.errorText?.className
|
|
182
|
+
: slotProps?.helperText?.className
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{resolvedHelperText}
|
|
186
|
+
</p>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChangeEventHandler,
|
|
3
|
+
FocusEventHandler,
|
|
4
|
+
ReactNode,
|
|
5
|
+
TextareaHTMLAttributes,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import type { Engine } from '@dashforge/ui-core';
|
|
8
|
+
import type { AccessRequirement } from '@dashforge/rbac';
|
|
9
|
+
import type { TextareaVariants } from './textarea.variants.js';
|
|
10
|
+
|
|
11
|
+
export interface TextareaSlotProps {
|
|
12
|
+
root?: { className?: string };
|
|
13
|
+
label?: { className?: string };
|
|
14
|
+
requiredMark?: { className?: string };
|
|
15
|
+
inputWrapper?: { className?: string };
|
|
16
|
+
input?: { className?: string };
|
|
17
|
+
helperText?: { className?: string };
|
|
18
|
+
errorText?: { className?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for `<Textarea>`.
|
|
23
|
+
*
|
|
24
|
+
* Mirrors `<TextField>` semantics (bridge integration, RBAC, gating)
|
|
25
|
+
* but renders a multi-line `<textarea>` and exposes a `resize` variant
|
|
26
|
+
* (default `'vertical'`). Inherits standard HTMLTextarea attributes
|
|
27
|
+
* (`rows`, `cols`, `maxLength`, etc.) via `TextareaHTMLAttributes`.
|
|
28
|
+
*/
|
|
29
|
+
export interface TextareaProps
|
|
30
|
+
extends Omit<
|
|
31
|
+
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
32
|
+
'name' | 'size' | 'onChange' | 'onBlur' | 'value' | 'defaultValue'
|
|
33
|
+
>,
|
|
34
|
+
TextareaVariants {
|
|
35
|
+
name: string;
|
|
36
|
+
rules?: unknown;
|
|
37
|
+
label?: ReactNode;
|
|
38
|
+
helperText?: ReactNode;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
error?: boolean;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Engine predicate — textarea not rendered when it returns `false`. */
|
|
43
|
+
visibleWhen?: (engine: Engine) => boolean;
|
|
44
|
+
/** RBAC access requirement (combines with explicit `disabled`). */
|
|
45
|
+
access?: AccessRequirement;
|
|
46
|
+
/** Root className shortcut (`cn`'d with the variant root class). */
|
|
47
|
+
sx?: string;
|
|
48
|
+
/** Per-slot className overrides. */
|
|
49
|
+
slotProps?: TextareaSlotProps;
|
|
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
|
+
/** User-supplied change handler — fires *after* bridge update. */
|
|
55
|
+
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
|
56
|
+
/** User-supplied blur handler — fires *after* bridge `onBlur`. */
|
|
57
|
+
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
|
58
|
+
/** Minimum visible row count for the textarea. Default: 3. */
|
|
59
|
+
rows?: number;
|
|
60
|
+
}
|