diff --git a/apps/storybook/src/stories/components/inputs/FieldTextarea.stories.js b/apps/storybook/src/stories/components/inputs/FieldTextarea.stories.js
index 54959203d..b92678d3e 100644
--- a/apps/storybook/src/stories/components/inputs/FieldTextarea.stories.js
+++ b/apps/storybook/src/stories/components/inputs/FieldTextarea.stories.js
@@ -1,27 +1,6 @@
-import { ref } from 'vue'
-
+import Button from '@aziontech/webkit/button'
import FieldTextarea from '@aziontech/webkit/field-textarea'
-
-const CORE_IMPORT = "import FieldTextarea from '@aziontech/webkit/field-textarea'"
-
-const basicSource = ({ initial = "''", bind = '' } = {}) =>
- [
- '',
- '',
- '',
- ' ` ${line}`) : []),
- ' />',
- ''
- ].join('\n')
+import { ref } from 'vue'
/** @type {import('@storybook/vue3').Meta} */
const meta = {
@@ -29,38 +8,40 @@ const meta = {
component: FieldTextarea,
tags: ['autodocs'],
parameters: {
- layout: 'padded',
- backgrounds: { default: 'dark' },
+ layout: 'centered',
+ backgrounds: {
+ default: 'dark'
+ },
a11y: {
config: {
rules: [
{ id: 'color-contrast', enabled: true },
- { id: 'label', enabled: true }
+ { id: 'focus-order-semantics', enabled: true }
]
}
},
docs: {
description: {
component: [
- 'Labeled multi-line text field — composes Label, Textarea, and HelperText into a single form-ready control. Sibling of FieldText; hosts Textarea (fixed large size, vertical resize, 80px min-height).',
+ 'Form field for multi-line text input that composes `Label`, `Textarea`, and `HelperText` into a single vertical stack with consistent spacing. Use it whenever a textarea needs a visible label or helper/error message — i.e. virtually every long-form field outside of inline editing. Acts as the canonical wrapper for `Textarea` in form contexts.',
'',
'## Usage',
'',
'```vue',
'',
'',
'',
' ',
'',
'```'
@@ -78,59 +59,75 @@ const meta = {
argTypes: {
modelValue: {
control: 'text',
- description: 'Two-way bound value of the underlying Textarea.',
- table: { type: { summary: 'string' }, category: 'props' }
- },
- name: {
- control: 'text',
- description: 'HTML name for the underlying textarea (form integration).',
- table: { type: { summary: 'string' }, category: 'props' }
+ description: 'Two-way bound value of the underlying `Textarea`.',
+ table: { category: 'props', type: { summary: 'string' } }
},
label: {
control: 'text',
- description: 'Text rendered inside the Label.',
- table: { type: { summary: 'string' }, category: 'props' }
+ description: 'Text rendered inside the `Label`. When empty, the label row is omitted.',
+ table: { category: 'props', type: { summary: 'string' } }
},
placeholder: {
control: 'text',
- description: 'Placeholder forwarded to the Textarea.',
- table: { type: { summary: 'string' }, category: 'props' }
+ description: 'Placeholder forwarded to the `Textarea`.',
+ table: { category: 'props', type: { summary: 'string' } }
},
helperText: {
control: 'text',
- description: 'Auxiliary helper text shown below the field.',
- table: { type: { summary: 'string' }, category: 'props' }
+ description:
+ 'Auxiliary text rendered inside `HelperText`. When empty, the helper row is omitted.',
+ table: { category: 'props', type: { summary: 'string' } }
},
disabled: {
control: 'boolean',
- description: 'Disables interaction and switches helper to kind=disabled.',
- table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' }
+ description:
+ 'Disables the textarea and switches the helper to `kind="disabled"` (lock icon).',
+ table: { category: 'props', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
},
readonly: {
control: 'boolean',
- description: 'Marks the field as read-only.',
- table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' }
+ description:
+ 'Marks the textarea read-only; value is visible but not editable. Native pass-through.',
+ table: { category: 'props', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
},
required: {
control: 'boolean',
- description: 'Adds the Required tag and sets aria-required.',
- table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' }
+ description:
+ 'Adds the `Required` tag to the `Label` and sets native `required` + `aria-required` on the textarea.',
+ table: { category: 'props', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
},
invalid: {
control: 'boolean',
- description: 'Switches helper to kind=invalid and applies invalid border on the textarea.',
- table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' }
+ description:
+ 'Switches the helper to `kind="invalid"` and applies invalid border/ring tokens on the textarea.',
+ table: { category: 'props', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } }
},
inputId: {
control: 'text',
- description: 'id for the native textarea.',
- table: { type: { summary: 'string' }, category: 'props' }
+ description:
+ 'id for the native textarea; consumed by `Label` via `for` and by `aria-describedby` wiring.',
+ table: { category: 'props', type: { summary: 'string' } }
+ },
+ name: {
+ control: 'text',
+ description: 'HTML name for the underlying textarea (form + vee-validate integration).',
+ table: { category: 'props', type: { summary: 'string' } }
},
'onUpdate:modelValue': {
action: 'update:modelValue',
- description: 'Emitted when the bound value changes.',
- table: { type: { summary: 'string' }, category: 'events' }
+ description: 'Re-emitted from the underlying `Textarea` on every input event.',
+ table: { category: 'events', type: { summary: 'string' } }
}
+ },
+ args: {
+ modelValue: '',
+ label: 'Message',
+ placeholder: 'Write your message',
+ helperText: 'Up to 500 characters.',
+ disabled: false,
+ readonly: false,
+ required: false,
+ invalid: false
}
}
@@ -139,52 +136,103 @@ export default meta
const Template = (args) => ({
components: { FieldTextarea },
setup() {
- const value = ref(args.modelValue ?? '')
- return { args, value }
+ return { args }
},
- template: `
-
-
-
- `
+ template: ''
})
+/** @type {import('@storybook/vue3').StoryObj} */
+export const Default = {
+ render: Template,
+ parameters: {
+ docs: { description: { story: 'Default field with label, textarea, and helper text.' } }
+ }
+}
+
+/** @type {import('@storybook/vue3').StoryObj} */
export const Required = {
+ render: () => ({
+ components: { FieldTextarea, Button },
+ setup() {
+ const value = ref('')
+ const missing = ref(false)
+ const loading = ref(false)
+
+ const onSubmit = () => {
+ if (loading.value) return
+ missing.value = false
+ loading.value = true
+ setTimeout(() => {
+ loading.value = false
+ missing.value = !value.value
+ }, 1200)
+ }
+
+ const onUpdate = (next) => {
+ value.value = next
+ if (next) missing.value = false
+ }
+
+ return { value, missing, loading, onSubmit, onUpdate }
+ },
+ template: `
+
+
+
+
+ `
+ }),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Click **Submit** with the field empty: the button shows the loading spinner for ~1.2s (simulating an async call) and the textarea is locked. When loading finishes, the required check fires — the `Label` gets the Required tag, the textarea border turns warning, and the helper switches to the warning-tone "This field is required." copy. Typing any value clears the error.'
+ }
+ }
+ }
+}
+
+/** @type {import('@storybook/vue3').StoryObj} */
+export const Invalid = {
args: {
- name: 'message',
+ invalid: true,
label: 'Message',
- placeholder: 'Write your message',
- helperText: 'This field is required.',
- required: true
+ modelValue: 'Text filled',
+ helperText: 'This value is not valid.'
},
render: Template,
parameters: {
docs: {
- source: {
- code: basicSource({
- bind: 'placeholder="Write your message"\nhelperText="This field is required."\nrequired'
- })
+ description: {
+ story:
+ 'Invalid state — the helper switches to `kind="invalid"` (danger tokens) and the textarea gets the invalid border/ring plus `aria-invalid`.'
}
}
}
}
-export const Invalid = {
+/** @type {import('@storybook/vue3').StoryObj} */
+export const Disabled = {
args: {
- name: 'message',
+ disabled: true,
label: 'Message',
- modelValue: 'Text Filled',
- helperText: 'This value is not valid.',
- invalid: true
+ modelValue: 'This message is locked.',
+ helperText: 'This field is locked.'
},
render: Template,
parameters: {
docs: {
- source: {
- code: basicSource({
- initial: "'Text Filled'",
- bind: 'helperText="This value is not valid."\ninvalid'
- })
+ description: {
+ story:
+ 'Disabled state — the textarea is disabled, the helper switches to `kind="disabled"` (lock icon), and the label dims via inherited muted text token.'
}
}
}
diff --git a/packages/webkit/src/components/inputs/textarea/textarea.vue b/packages/webkit/src/components/inputs/textarea/textarea.vue
index c7ab0b5aa..00e0e6f6f 100644
--- a/packages/webkit/src/components/inputs/textarea/textarea.vue
+++ b/packages/webkit/src/components/inputs/textarea/textarea.vue
@@ -44,31 +44,54 @@
const testId = computed(() => (attrs['data-testid'] as string | undefined) ?? 'input-textarea')
const isFilled = computed(() => props.modelValue.length > 0)
- const hasIconLeft = computed(() => !!slots['iconLeft'])
- const hasIconRight = computed(() => !!slots['iconRight'] || props.disabled)
+ const hasIconLeft = computed(() => Boolean(slots['iconLeft']))
+ const hasIconRight = computed(() => Boolean(slots['iconRight']) || props.disabled)
- // eslint-disable-next-line no-undef
- const handleInput = (event: InputEvent) => {
- const target = event.target as HTMLElement & { value: string }
+ const passthroughAttrs = computed(() => {
+ const rest: Record = { ...attrs }
+ delete rest['class']
+ delete rest['data-testid']
+ return rest
+ })
+
+ const handleInput = (event: globalThis.Event) => {
+ const target = event.target as globalThis.HTMLTextAreaElement
emit('update:modelValue', target.value)
}
-
+
+
+
+
-
-
-
+
+
-
+