@sabrenski/spire-ui 0.0.1
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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.d.ts +4981 -0
- package/dist/spire-ui.css +1 -0
- package/dist/spire-ui.es.js +18403 -0
- package/dist/spire-ui.umd.js +45 -0
- package/package.json +83 -0
- package/src/components/Accordion/Accordion.test.ts +218 -0
- package/src/components/Accordion/AccordionContent.vue +112 -0
- package/src/components/Accordion/AccordionItem.vue +87 -0
- package/src/components/Accordion/AccordionRoot.vue +111 -0
- package/src/components/Accordion/AccordionTrigger.vue +125 -0
- package/src/components/Accordion/index.ts +11 -0
- package/src/components/Accordion/keys.ts +23 -0
- package/src/components/Avatar/Avatar.test.ts +181 -0
- package/src/components/Avatar/Avatar.vue +150 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/Badge/Badge.test.ts +141 -0
- package/src/components/Badge/Badge.vue +133 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
- package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
- package/src/components/BadgeContainer/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
- package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
- package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
- package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
- package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
- package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
- package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
- package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
- package/src/components/Breadcrumb/index.ts +13 -0
- package/src/components/Breadcrumb/keys.ts +7 -0
- package/src/components/Button/Button.test.ts +231 -0
- package/src/components/Button/Button.vue +349 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Callout/Callout.test.ts +260 -0
- package/src/components/Callout/Callout.vue +341 -0
- package/src/components/Callout/index.ts +2 -0
- package/src/components/Card/Card.test.ts +565 -0
- package/src/components/Card/Card.vue +209 -0
- package/src/components/Card/CardContent.vue +57 -0
- package/src/components/Card/CardFooter.vue +72 -0
- package/src/components/Card/CardHeader.vue +111 -0
- package/src/components/Card/CardImage.vue +124 -0
- package/src/components/Card/index.ts +14 -0
- package/src/components/Chart/BarChart.vue +208 -0
- package/src/components/Chart/BaseChart.vue +444 -0
- package/src/components/Chart/Chart.test.ts +359 -0
- package/src/components/Chart/DonutChart.vue +283 -0
- package/src/components/Chart/LineChart.vue +211 -0
- package/src/components/Chart/index.ts +20 -0
- package/src/components/Chart/useChartTheme.ts +192 -0
- package/src/components/Checkbox/Checkbox.test.ts +209 -0
- package/src/components/Checkbox/Checkbox.vue +285 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
- package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
- package/src/components/ChoiceChip/index.ts +2 -0
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
- package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
- package/src/components/ChoiceChipGroup/index.ts +2 -0
- package/src/components/ColorPicker/ColorArea.vue +159 -0
- package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
- package/src/components/ColorPicker/ColorPicker.vue +339 -0
- package/src/components/ColorPicker/ColorSlider.vue +191 -0
- package/src/components/ColorPicker/index.ts +7 -0
- package/src/components/Combobox/Combobox.test.ts +891 -0
- package/src/components/Combobox/Combobox.vue +934 -0
- package/src/components/Combobox/index.ts +2 -0
- package/src/components/DataTable/DataTable.test.ts +1221 -0
- package/src/components/DataTable/DataTable.vue +1415 -0
- package/src/components/DataTable/index.ts +10 -0
- package/src/components/DatePicker/DatePicker.test.ts +625 -0
- package/src/components/DatePicker/DatePicker.vue +1586 -0
- package/src/components/DatePicker/index.ts +2 -0
- package/src/components/Drawer/Drawer.test.ts +336 -0
- package/src/components/Drawer/Drawer.vue +466 -0
- package/src/components/Drawer/index.ts +2 -0
- package/src/components/Dropdown/Dropdown.test.ts +607 -0
- package/src/components/Dropdown/Dropdown.vue +807 -0
- package/src/components/Dropdown/DropdownItem.vue +227 -0
- package/src/components/Dropdown/DropdownSeparator.vue +14 -0
- package/src/components/Dropdown/DropdownSub.vue +104 -0
- package/src/components/Dropdown/DropdownSubContent.vue +187 -0
- package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
- package/src/components/Dropdown/index.ts +14 -0
- package/src/components/EmptyState/EmptyState.test.ts +180 -0
- package/src/components/EmptyState/EmptyState.vue +137 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/FileUpload/FileUpload.test.ts +1151 -0
- package/src/components/FileUpload/FileUpload.vue +1042 -0
- package/src/components/FileUpload/index.ts +2 -0
- package/src/components/Heading/Heading.test.ts +107 -0
- package/src/components/Heading/Heading.vue +67 -0
- package/src/components/Heading/index.ts +2 -0
- package/src/components/Icon/Icon.test.ts +157 -0
- package/src/components/Icon/Icon.vue +86 -0
- package/src/components/Icon/index.ts +2 -0
- package/src/components/Input/Input.test.ts +273 -0
- package/src/components/Input/Input.vue +388 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Layout/Container.vue +67 -0
- package/src/components/Layout/Grid.vue +159 -0
- package/src/components/Layout/GridItem.vue +154 -0
- package/src/components/Layout/Layout.test.ts +202 -0
- package/src/components/Layout/Stack.vue +128 -0
- package/src/components/Layout/index.ts +9 -0
- package/src/components/Layout/keys.ts +7 -0
- package/src/components/Modal/Modal.test.ts +311 -0
- package/src/components/Modal/Modal.vue +336 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Pagination/Pagination.test.ts +303 -0
- package/src/components/Pagination/Pagination.vue +212 -0
- package/src/components/Pagination/index.ts +3 -0
- package/src/components/Pagination/utils.ts +86 -0
- package/src/components/Popover/Popover.test.ts +285 -0
- package/src/components/Popover/Popover.vue +441 -0
- package/src/components/Popover/index.ts +2 -0
- package/src/components/Progress/Progress.test.ts +361 -0
- package/src/components/Progress/Progress.vue +363 -0
- package/src/components/Progress/index.ts +7 -0
- package/src/components/Radio/Radio.test.ts +216 -0
- package/src/components/Radio/Radio.vue +214 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Rating/Rating.test.ts +319 -0
- package/src/components/Rating/Rating.vue +247 -0
- package/src/components/Rating/index.ts +2 -0
- package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
- package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
- package/src/components/SegmentedControl/index.ts +2 -0
- package/src/components/Select/Select.test.ts +589 -0
- package/src/components/Select/Select.vue +666 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.test.ts +301 -0
- package/src/components/Sidebar/SidebarGroup.vue +103 -0
- package/src/components/Sidebar/SidebarItem.vue +196 -0
- package/src/components/Sidebar/SidebarLayout.vue +42 -0
- package/src/components/Sidebar/SidebarRoot.vue +122 -0
- package/src/components/Sidebar/index.ts +11 -0
- package/src/components/Sidebar/keys.ts +14 -0
- package/src/components/Skeleton/Skeleton.test.ts +130 -0
- package/src/components/Skeleton/Skeleton.vue +104 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.test.ts +416 -0
- package/src/components/Slider/Slider.vue +435 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Slider/utils.ts +91 -0
- package/src/components/Spinner/Spinner.test.ts +79 -0
- package/src/components/Spinner/Spinner.vue +159 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/SpireProvider/SpireProvider.vue +71 -0
- package/src/components/SpireProvider/index.ts +11 -0
- package/src/components/Stepper/Stepper.test.ts +221 -0
- package/src/components/Stepper/StepperContent.vue +51 -0
- package/src/components/Stepper/StepperItem.vue +89 -0
- package/src/components/Stepper/StepperRoot.vue +101 -0
- package/src/components/Stepper/StepperSeparator.vue +52 -0
- package/src/components/Stepper/StepperTrigger.vue +144 -0
- package/src/components/Stepper/index.ts +11 -0
- package/src/components/Stepper/keys.ts +27 -0
- package/src/components/Switch/Switch.test.ts +214 -0
- package/src/components/Switch/Switch.vue +235 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Tabs/Tabs.test.ts +363 -0
- package/src/components/Tabs/Tabs.vue +318 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Text/Text.test.ts +154 -0
- package/src/components/Text/Text.vue +100 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.test.ts +432 -0
- package/src/components/Textarea/Textarea.vue +411 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/TimePicker/TimePicker.test.ts +352 -0
- package/src/components/TimePicker/TimePicker.vue +569 -0
- package/src/components/TimePicker/index.ts +2 -0
- package/src/components/Timeline/Timeline.test.ts +193 -0
- package/src/components/Timeline/Timeline.vue +111 -0
- package/src/components/Timeline/TimelineItem.vue +167 -0
- package/src/components/Timeline/index.ts +13 -0
- package/src/components/Timeline/keys.ts +21 -0
- package/src/components/Toast/ToastItem.test.ts +289 -0
- package/src/components/Toast/ToastItem.vue +370 -0
- package/src/components/Toast/ToastProvider.test.ts +158 -0
- package/src/components/Toast/ToastProvider.vue +181 -0
- package/src/components/Toast/index.ts +83 -0
- package/src/components/Toast/toastState.test.ts +165 -0
- package/src/components/Toast/toastState.ts +161 -0
- package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
- package/src/components/ToggleButton/ToggleButton.vue +197 -0
- package/src/components/ToggleButton/index.ts +2 -0
- package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
- package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.test.ts +238 -0
- package/src/components/Tooltip/Tooltip.vue +217 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/TreeView/TreeView.test.ts +357 -0
- package/src/components/TreeView/TreeView.vue +251 -0
- package/src/components/TreeView/TreeViewItem.vue +288 -0
- package/src/components/TreeView/index.ts +11 -0
- package/src/components/TreeView/keys.ts +35 -0
- package/src/composables/index.ts +12 -0
- package/src/composables/useClickOutside.ts +36 -0
- package/src/composables/useClipboard.ts +35 -0
- package/src/composables/useEventListener.ts +48 -0
- package/src/composables/useFocusTrap.ts +58 -0
- package/src/composables/useHoverReveal.ts +98 -0
- package/src/composables/useId.ts +10 -0
- package/src/composables/useMagnetic.ts +171 -0
- package/src/composables/useRelativePosition.ts +127 -0
- package/src/composables/useRipple.ts +146 -0
- package/src/composables/useScrollLock.ts +25 -0
- package/src/composables/useSpireConfig.ts +27 -0
- package/src/composables/useStagger.ts +224 -0
- package/src/config/icons.test.ts +115 -0
- package/src/config/icons.ts +170 -0
- package/src/index.ts +361 -0
- package/src/styles/depth.css +129 -0
- package/src/styles/effects.css +169 -0
- package/src/styles/fallback.css +152 -0
- package/src/styles/main.css +25 -0
- package/src/styles/mood.css +211 -0
- package/src/styles/motion.css +159 -0
- package/src/styles/reset.css +97 -0
- package/src/styles/theme.css +708 -0
- package/src/styles/tokens.css +183 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/color.ts +277 -0
- package/src/utils/date.test.ts +522 -0
- package/src/utils/date.ts +380 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/object.test.ts +80 -0
- package/src/utils/object.ts +25 -0
- package/src/utils/string.test.ts +64 -0
- package/src/utils/string.ts +32 -0
- package/src/utils/time.ts +156 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Text from './Text.vue'
|
|
4
|
+
|
|
5
|
+
describe('Text', () => {
|
|
6
|
+
describe('Rendering', () => {
|
|
7
|
+
it('renders as p by default', () => {
|
|
8
|
+
const wrapper = mount(Text, {
|
|
9
|
+
slots: { default: 'Hello' }
|
|
10
|
+
})
|
|
11
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe('p')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('renders slot content', () => {
|
|
15
|
+
const wrapper = mount(Text, {
|
|
16
|
+
slots: { default: 'Body text' }
|
|
17
|
+
})
|
|
18
|
+
expect(wrapper.text()).toBe('Body text')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('renders as custom tag via "as" prop', () => {
|
|
22
|
+
const wrapper = mount(Text, {
|
|
23
|
+
props: { as: 'span' },
|
|
24
|
+
slots: { default: 'Inline text' }
|
|
25
|
+
})
|
|
26
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe('span')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('can render as label', () => {
|
|
30
|
+
const wrapper = mount(Text, {
|
|
31
|
+
props: { as: 'label' },
|
|
32
|
+
slots: { default: 'Field label' }
|
|
33
|
+
})
|
|
34
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe('label')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('Sizes', () => {
|
|
39
|
+
const sizes = ['xs', 'sm', 'base', 'md', 'lg', 'xl'] as const
|
|
40
|
+
|
|
41
|
+
sizes.forEach(size => {
|
|
42
|
+
it(`applies ${size} size class`, () => {
|
|
43
|
+
const wrapper = mount(Text, {
|
|
44
|
+
props: { size },
|
|
45
|
+
slots: { default: 'Text' }
|
|
46
|
+
})
|
|
47
|
+
expect(wrapper.classes()).toContain(`ui-text--${size}`)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('defaults to base size', () => {
|
|
52
|
+
const wrapper = mount(Text, {
|
|
53
|
+
slots: { default: 'Text' }
|
|
54
|
+
})
|
|
55
|
+
expect(wrapper.classes()).toContain('ui-text--base')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('Weights', () => {
|
|
60
|
+
const weights = ['regular', 'medium', 'semibold', 'bold'] as const
|
|
61
|
+
|
|
62
|
+
weights.forEach(weight => {
|
|
63
|
+
it(`applies ${weight} weight class`, () => {
|
|
64
|
+
const wrapper = mount(Text, {
|
|
65
|
+
props: { weight },
|
|
66
|
+
slots: { default: 'Text' }
|
|
67
|
+
})
|
|
68
|
+
expect(wrapper.classes()).toContain(`ui-text--${weight}`)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('defaults to regular weight', () => {
|
|
73
|
+
const wrapper = mount(Text, {
|
|
74
|
+
slots: { default: 'Text' }
|
|
75
|
+
})
|
|
76
|
+
expect(wrapper.classes()).toContain('ui-text--regular')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('Alignment', () => {
|
|
81
|
+
const alignments = ['left', 'center', 'right', 'justify'] as const
|
|
82
|
+
|
|
83
|
+
alignments.forEach(align => {
|
|
84
|
+
it(`applies ${align} alignment class`, () => {
|
|
85
|
+
const wrapper = mount(Text, {
|
|
86
|
+
props: { align },
|
|
87
|
+
slots: { default: 'Text' }
|
|
88
|
+
})
|
|
89
|
+
expect(wrapper.classes()).toContain(`ui-text--${align}`)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('defaults to left alignment', () => {
|
|
94
|
+
const wrapper = mount(Text, {
|
|
95
|
+
slots: { default: 'Text' }
|
|
96
|
+
})
|
|
97
|
+
expect(wrapper.classes()).toContain('ui-text--left')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('Muted variant', () => {
|
|
102
|
+
it('applies muted class when muted prop is true', () => {
|
|
103
|
+
const wrapper = mount(Text, {
|
|
104
|
+
props: { muted: true },
|
|
105
|
+
slots: { default: 'Secondary text' }
|
|
106
|
+
})
|
|
107
|
+
expect(wrapper.classes()).toContain('ui-text--muted')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('does not apply muted class by default', () => {
|
|
111
|
+
const wrapper = mount(Text, {
|
|
112
|
+
slots: { default: 'Text' }
|
|
113
|
+
})
|
|
114
|
+
expect(wrapper.classes()).not.toContain('ui-text--muted')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('Truncation', () => {
|
|
119
|
+
it('applies truncate class when truncate prop is true', () => {
|
|
120
|
+
const wrapper = mount(Text, {
|
|
121
|
+
props: { truncate: true },
|
|
122
|
+
slots: { default: 'Very long text' }
|
|
123
|
+
})
|
|
124
|
+
expect(wrapper.classes()).toContain('ui-text--truncate')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('does not apply truncate when clamp is set', () => {
|
|
128
|
+
const wrapper = mount(Text, {
|
|
129
|
+
props: { truncate: true, clamp: 2 },
|
|
130
|
+
slots: { default: 'Long text' }
|
|
131
|
+
})
|
|
132
|
+
expect(wrapper.classes()).not.toContain('ui-text--truncate')
|
|
133
|
+
expect(wrapper.classes()).toContain('ui-text--clamp')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('Line clamping', () => {
|
|
138
|
+
it('applies clamp class when clamp prop is set', () => {
|
|
139
|
+
const wrapper = mount(Text, {
|
|
140
|
+
props: { clamp: 3 },
|
|
141
|
+
slots: { default: 'Multi-line text' }
|
|
142
|
+
})
|
|
143
|
+
expect(wrapper.classes()).toContain('ui-text--clamp')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('accepts different clamp values', () => {
|
|
147
|
+
const wrapper = mount(Text, {
|
|
148
|
+
props: { clamp: 5 },
|
|
149
|
+
slots: { default: 'Multi-line text' }
|
|
150
|
+
})
|
|
151
|
+
expect(wrapper.classes()).toContain('ui-text--clamp')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, type CSSProperties } from 'vue'
|
|
3
|
+
|
|
4
|
+
type TextTag = 'p' | 'span' | 'div' | 'label' | 'li'
|
|
5
|
+
type TextSize = 'xs' | 'sm' | 'base' | 'md' | 'lg' | 'xl'
|
|
6
|
+
type TextWeight = 'regular' | 'medium' | 'semibold' | 'bold'
|
|
7
|
+
|
|
8
|
+
export interface TextProps {
|
|
9
|
+
/** Semantic HTML tag */
|
|
10
|
+
as?: TextTag
|
|
11
|
+
/** Font size */
|
|
12
|
+
size?: TextSize
|
|
13
|
+
/** Font weight */
|
|
14
|
+
weight?: TextWeight
|
|
15
|
+
/** Text alignment */
|
|
16
|
+
align?: 'left' | 'center' | 'right' | 'justify'
|
|
17
|
+
/** Use secondary (muted) color */
|
|
18
|
+
muted?: boolean
|
|
19
|
+
/** Single-line truncation with ellipsis */
|
|
20
|
+
truncate?: boolean
|
|
21
|
+
/** Multi-line clamping (number of lines) */
|
|
22
|
+
clamp?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<TextProps>(), {
|
|
26
|
+
as: 'p',
|
|
27
|
+
size: 'base',
|
|
28
|
+
weight: 'regular',
|
|
29
|
+
align: 'left'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const classes = computed(() => [
|
|
33
|
+
'ui-text',
|
|
34
|
+
`ui-text--${props.size}`,
|
|
35
|
+
`ui-text--${props.weight}`,
|
|
36
|
+
`ui-text--${props.align}`,
|
|
37
|
+
{
|
|
38
|
+
'ui-text--muted': props.muted,
|
|
39
|
+
'ui-text--truncate': props.truncate && !props.clamp,
|
|
40
|
+
'ui-text--clamp': props.clamp
|
|
41
|
+
}
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
const clampStyle = computed(() =>
|
|
45
|
+
props.clamp ? { WebkitLineClamp: props.clamp } as CSSProperties : undefined
|
|
46
|
+
)
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<component :is="as" :class="classes" :style="clampStyle">
|
|
51
|
+
<slot />
|
|
52
|
+
</component>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.ui-text {
|
|
57
|
+
font-family: var(--font-sans);
|
|
58
|
+
color: var(--text-primary);
|
|
59
|
+
margin: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Sizes */
|
|
63
|
+
.ui-text--xs { font-size: var(--text-xs); line-height: 1.4; }
|
|
64
|
+
.ui-text--sm { font-size: var(--text-sm); line-height: 1.4; }
|
|
65
|
+
.ui-text--base { font-size: var(--text-base); line-height: 1.5; }
|
|
66
|
+
.ui-text--md { font-size: var(--text-md); line-height: 1.5; }
|
|
67
|
+
.ui-text--lg { font-size: var(--text-lg); line-height: 1.5; }
|
|
68
|
+
.ui-text--xl { font-size: var(--text-xl); line-height: 1.4; }
|
|
69
|
+
|
|
70
|
+
/* Weights */
|
|
71
|
+
.ui-text--regular { font-weight: 400; }
|
|
72
|
+
.ui-text--medium { font-weight: 500; }
|
|
73
|
+
.ui-text--semibold { font-weight: 600; }
|
|
74
|
+
.ui-text--bold { font-weight: 700; }
|
|
75
|
+
|
|
76
|
+
/* Alignment */
|
|
77
|
+
.ui-text--left { text-align: left; }
|
|
78
|
+
.ui-text--center { text-align: center; }
|
|
79
|
+
.ui-text--right { text-align: right; }
|
|
80
|
+
.ui-text--justify { text-align: justify; }
|
|
81
|
+
|
|
82
|
+
/* Muted variant */
|
|
83
|
+
.ui-text--muted {
|
|
84
|
+
color: var(--text-secondary);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Single-line truncation */
|
|
88
|
+
.ui-text--truncate {
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
text-overflow: ellipsis;
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Multi-line clamping */
|
|
95
|
+
.ui-text--clamp {
|
|
96
|
+
display: -webkit-box;
|
|
97
|
+
-webkit-box-orient: vertical;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import Textarea from './Textarea.vue'
|
|
5
|
+
|
|
6
|
+
describe('Textarea', () => {
|
|
7
|
+
describe('Rendering', () => {
|
|
8
|
+
it('renders a textarea element', () => {
|
|
9
|
+
const wrapper = mount(Textarea)
|
|
10
|
+
expect(wrapper.find('textarea').exists()).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders label when provided', () => {
|
|
14
|
+
const wrapper = mount(Textarea, {
|
|
15
|
+
props: { label: 'Description' }
|
|
16
|
+
})
|
|
17
|
+
expect(wrapper.find('label').text()).toContain('Description')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renders required indicator when required', () => {
|
|
21
|
+
const wrapper = mount(Textarea, {
|
|
22
|
+
props: { label: 'Description', required: true }
|
|
23
|
+
})
|
|
24
|
+
expect(wrapper.find('.ui-textarea-field__required').exists()).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders hint text', () => {
|
|
28
|
+
const wrapper = mount(Textarea, {
|
|
29
|
+
props: { hint: 'Enter a detailed description' }
|
|
30
|
+
})
|
|
31
|
+
expect(wrapper.find('.ui-textarea-field__message--hint').text()).toBe('Enter a detailed description')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders error message', () => {
|
|
35
|
+
const wrapper = mount(Textarea, {
|
|
36
|
+
props: { error: 'This field is required' }
|
|
37
|
+
})
|
|
38
|
+
expect(wrapper.find('.ui-textarea-field__message--error').text()).toBe('This field is required')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders placeholder', () => {
|
|
42
|
+
const wrapper = mount(Textarea, {
|
|
43
|
+
props: { placeholder: 'Enter text...' }
|
|
44
|
+
})
|
|
45
|
+
expect(wrapper.find('textarea').attributes('placeholder')).toBe('Enter text...')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('sets rows attribute', () => {
|
|
49
|
+
const wrapper = mount(Textarea, {
|
|
50
|
+
props: { rows: 5 }
|
|
51
|
+
})
|
|
52
|
+
expect(wrapper.find('textarea').attributes('rows')).toBe('5')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('defaults to 3 rows', () => {
|
|
56
|
+
const wrapper = mount(Textarea)
|
|
57
|
+
expect(wrapper.find('textarea').attributes('rows')).toBe('3')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('v-model', () => {
|
|
62
|
+
it('binds value correctly', () => {
|
|
63
|
+
const wrapper = mount(Textarea, {
|
|
64
|
+
props: { modelValue: 'Hello world' }
|
|
65
|
+
})
|
|
66
|
+
expect((wrapper.find('textarea').element as HTMLTextAreaElement).value).toBe('Hello world')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('emits update:modelValue on input', async () => {
|
|
70
|
+
const wrapper = mount(Textarea)
|
|
71
|
+
const textarea = wrapper.find('textarea')
|
|
72
|
+
|
|
73
|
+
await textarea.setValue('New content')
|
|
74
|
+
|
|
75
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
76
|
+
expect(wrapper.emitted('update:modelValue')![0][0]).toBe('New content')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('Focus events', () => {
|
|
81
|
+
it('emits focus event', async () => {
|
|
82
|
+
const wrapper = mount(Textarea)
|
|
83
|
+
await wrapper.find('textarea').trigger('focus')
|
|
84
|
+
expect(wrapper.emitted('focus')).toBeTruthy()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('emits blur event', async () => {
|
|
88
|
+
const wrapper = mount(Textarea)
|
|
89
|
+
await wrapper.find('textarea').trigger('blur')
|
|
90
|
+
expect(wrapper.emitted('blur')).toBeTruthy()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('States', () => {
|
|
95
|
+
it('applies disabled state', () => {
|
|
96
|
+
const wrapper = mount(Textarea, {
|
|
97
|
+
props: { disabled: true }
|
|
98
|
+
})
|
|
99
|
+
expect(wrapper.find('textarea').attributes('disabled')).toBeDefined()
|
|
100
|
+
expect(wrapper.find('.ui-textarea-field--disabled').exists()).toBe(true)
|
|
101
|
+
expect(wrapper.find('.ui-textarea-wrapper--disabled').exists()).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('applies readonly state', () => {
|
|
105
|
+
const wrapper = mount(Textarea, {
|
|
106
|
+
props: { readonly: true }
|
|
107
|
+
})
|
|
108
|
+
expect(wrapper.find('textarea').attributes('readonly')).toBeDefined()
|
|
109
|
+
expect(wrapper.find('.ui-textarea-field--readonly').exists()).toBe(true)
|
|
110
|
+
expect(wrapper.find('.ui-textarea-wrapper--readonly').exists()).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('applies error state', () => {
|
|
114
|
+
const wrapper = mount(Textarea, {
|
|
115
|
+
props: { error: 'Error message' }
|
|
116
|
+
})
|
|
117
|
+
expect(wrapper.find('.ui-textarea-field--error').exists()).toBe(true)
|
|
118
|
+
expect(wrapper.find('.ui-textarea-wrapper--error').exists()).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('applies block modifier', () => {
|
|
122
|
+
const wrapper = mount(Textarea, {
|
|
123
|
+
props: { block: true }
|
|
124
|
+
})
|
|
125
|
+
expect(wrapper.find('.ui-textarea-field--block').exists()).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('Accessibility', () => {
|
|
130
|
+
it('generates unique id for textarea', () => {
|
|
131
|
+
const wrapper = mount(Textarea)
|
|
132
|
+
const id = wrapper.find('textarea').attributes('id')
|
|
133
|
+
expect(id).toMatch(/^textarea-/)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('uses provided id', () => {
|
|
137
|
+
const wrapper = mount(Textarea, {
|
|
138
|
+
props: { id: 'custom-textarea' }
|
|
139
|
+
})
|
|
140
|
+
expect(wrapper.find('textarea').attributes('id')).toBe('custom-textarea')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('links label to textarea', () => {
|
|
144
|
+
const wrapper = mount(Textarea, {
|
|
145
|
+
props: { label: 'Bio', id: 'bio-field' }
|
|
146
|
+
})
|
|
147
|
+
expect(wrapper.find('label').attributes('for')).toBe('bio-field')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('sets aria-invalid when error', () => {
|
|
151
|
+
const wrapper = mount(Textarea, {
|
|
152
|
+
props: { error: 'Required' }
|
|
153
|
+
})
|
|
154
|
+
expect(wrapper.find('textarea').attributes('aria-invalid')).toBe('true')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('links aria-describedby to hint', () => {
|
|
158
|
+
const wrapper = mount(Textarea, {
|
|
159
|
+
props: { hint: 'Help text', id: 'test-id' }
|
|
160
|
+
})
|
|
161
|
+
expect(wrapper.find('textarea').attributes('aria-describedby')).toBe('test-id-hint')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('links aria-describedby to error over hint', () => {
|
|
165
|
+
const wrapper = mount(Textarea, {
|
|
166
|
+
props: { hint: 'Help text', error: 'Error', id: 'test-id' }
|
|
167
|
+
})
|
|
168
|
+
expect(wrapper.find('textarea').attributes('aria-describedby')).toBe('test-id-error')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('links aria-describedby to counter when showCount and maxLength', () => {
|
|
172
|
+
const wrapper = mount(Textarea, {
|
|
173
|
+
props: { showCount: true, maxLength: 100, id: 'test-id' }
|
|
174
|
+
})
|
|
175
|
+
expect(wrapper.find('textarea').attributes('aria-describedby')).toContain('test-id-counter')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('Character count', () => {
|
|
180
|
+
it('shows character counter when showCount is true', () => {
|
|
181
|
+
const wrapper = mount(Textarea, {
|
|
182
|
+
props: { showCount: true, modelValue: 'Hello' }
|
|
183
|
+
})
|
|
184
|
+
expect(wrapper.find('.ui-textarea-field__counter').exists()).toBe(true)
|
|
185
|
+
expect(wrapper.find('.ui-textarea-field__counter').text()).toBe('5')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('shows count with max length', () => {
|
|
189
|
+
const wrapper = mount(Textarea, {
|
|
190
|
+
props: { showCount: true, maxLength: 100, modelValue: 'Hello' }
|
|
191
|
+
})
|
|
192
|
+
expect(wrapper.find('.ui-textarea-field__counter').text()).toBe('5 / 100')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('applies warning state at 90%', () => {
|
|
196
|
+
const wrapper = mount(Textarea, {
|
|
197
|
+
props: { showCount: true, maxLength: 10, modelValue: '123456789' }
|
|
198
|
+
})
|
|
199
|
+
expect(wrapper.find('.ui-textarea-field__counter--warning').exists()).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('applies error state over limit', () => {
|
|
203
|
+
const wrapper = mount(Textarea, {
|
|
204
|
+
props: { showCount: true, maxLength: 5, modelValue: '123456' }
|
|
205
|
+
})
|
|
206
|
+
expect(wrapper.find('.ui-textarea-field__counter--error').exists()).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('applies error state to wrapper when over limit', () => {
|
|
210
|
+
const wrapper = mount(Textarea, {
|
|
211
|
+
props: { maxLength: 5, modelValue: '123456' }
|
|
212
|
+
})
|
|
213
|
+
expect(wrapper.find('.ui-textarea-field--error').exists()).toBe(true)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('hides counter when showCount is false', () => {
|
|
217
|
+
const wrapper = mount(Textarea, {
|
|
218
|
+
props: { showCount: false, maxLength: 100 }
|
|
219
|
+
})
|
|
220
|
+
expect(wrapper.find('.ui-textarea-field__counter').exists()).toBe(false)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('sets maxlength attribute on textarea', () => {
|
|
224
|
+
const wrapper = mount(Textarea, {
|
|
225
|
+
props: { maxLength: 140 }
|
|
226
|
+
})
|
|
227
|
+
expect(wrapper.find('textarea').attributes('maxlength')).toBe('140')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('Auto-grow', () => {
|
|
232
|
+
it('applies autosize class when enabled', () => {
|
|
233
|
+
const wrapper = mount(Textarea, {
|
|
234
|
+
props: { autosize: true }
|
|
235
|
+
})
|
|
236
|
+
expect(wrapper.find('.ui-textarea-wrapper--autosize').exists()).toBe(true)
|
|
237
|
+
expect(wrapper.find('.ui-textarea-wrapper__textarea--autosize').exists()).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('does not apply autosize class when disabled', () => {
|
|
241
|
+
const wrapper = mount(Textarea, {
|
|
242
|
+
props: { autosize: false }
|
|
243
|
+
})
|
|
244
|
+
expect(wrapper.find('.ui-textarea-wrapper--autosize').exists()).toBe(false)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('adjusts height on input when autosize is true', async () => {
|
|
248
|
+
const wrapper = mount(Textarea, {
|
|
249
|
+
props: { autosize: true },
|
|
250
|
+
attachTo: document.body
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
|
254
|
+
|
|
255
|
+
// Mock scrollHeight
|
|
256
|
+
Object.defineProperty(textarea, 'scrollHeight', {
|
|
257
|
+
value: 100,
|
|
258
|
+
configurable: true
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
await wrapper.find('textarea').setValue('Line 1\nLine 2\nLine 3')
|
|
262
|
+
await nextTick()
|
|
263
|
+
|
|
264
|
+
// Height should be set to scrollHeight
|
|
265
|
+
expect(textarea.style.height).toBe('100px')
|
|
266
|
+
|
|
267
|
+
wrapper.unmount()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('respects maxHeight constraint', async () => {
|
|
271
|
+
const wrapper = mount(Textarea, {
|
|
272
|
+
props: { autosize: true, maxHeight: 150 },
|
|
273
|
+
attachTo: document.body
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
|
277
|
+
|
|
278
|
+
// Mock scrollHeight larger than maxHeight
|
|
279
|
+
Object.defineProperty(textarea, 'scrollHeight', {
|
|
280
|
+
value: 200,
|
|
281
|
+
configurable: true
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
await wrapper.find('textarea').setValue('Very long content...')
|
|
285
|
+
await nextTick()
|
|
286
|
+
|
|
287
|
+
// Height should be capped at maxHeight
|
|
288
|
+
expect(textarea.style.height).toBe('150px')
|
|
289
|
+
expect(textarea.style.overflowY).toBe('auto')
|
|
290
|
+
|
|
291
|
+
wrapper.unmount()
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('Resize behavior', () => {
|
|
296
|
+
it('has vertical resize by default', () => {
|
|
297
|
+
const wrapper = mount(Textarea)
|
|
298
|
+
const textarea = wrapper.find('textarea')
|
|
299
|
+
expect(textarea.classes()).not.toContain('ui-textarea-wrapper__textarea--autosize')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('disables resize when autosize is true', () => {
|
|
303
|
+
const wrapper = mount(Textarea, {
|
|
304
|
+
props: { autosize: true }
|
|
305
|
+
})
|
|
306
|
+
expect(wrapper.find('.ui-textarea-wrapper__textarea--autosize').exists()).toBe(true)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('disables resize when disabled', async () => {
|
|
310
|
+
const wrapper = mount(Textarea, {
|
|
311
|
+
props: { disabled: true },
|
|
312
|
+
attachTo: document.body
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
|
316
|
+
const styles = window.getComputedStyle(textarea)
|
|
317
|
+
|
|
318
|
+
// The CSS sets resize: none for disabled textareas
|
|
319
|
+
// In JSDOM, we can check the class is applied
|
|
320
|
+
expect(wrapper.find('textarea').attributes('disabled')).toBeDefined()
|
|
321
|
+
|
|
322
|
+
wrapper.unmount()
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('Icons', () => {
|
|
327
|
+
it('renders left icon', () => {
|
|
328
|
+
const mockIcon = { template: '<svg class="mock-icon"></svg>' }
|
|
329
|
+
const wrapper = mount(Textarea, {
|
|
330
|
+
props: { iconLeft: mockIcon as any }
|
|
331
|
+
})
|
|
332
|
+
expect(wrapper.find('.ui-textarea-wrapper__addon--left').exists()).toBe(true)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('renders left slot content', () => {
|
|
336
|
+
const wrapper = mount(Textarea, {
|
|
337
|
+
slots: {
|
|
338
|
+
left: '<span class="custom-icon">📝</span>'
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
expect(wrapper.find('.ui-textarea-wrapper__addon--left').exists()).toBe(true)
|
|
342
|
+
expect(wrapper.find('.custom-icon').exists()).toBe(true)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('applies has-left class when icon present', () => {
|
|
346
|
+
const mockIcon = { template: '<svg class="mock-icon"></svg>' }
|
|
347
|
+
const wrapper = mount(Textarea, {
|
|
348
|
+
props: { iconLeft: mockIcon as any }
|
|
349
|
+
})
|
|
350
|
+
expect(wrapper.find('.ui-textarea-wrapper--has-left').exists()).toBe(true)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('Exposed methods', () => {
|
|
355
|
+
it('exposes focus method', async () => {
|
|
356
|
+
const wrapper = mount(Textarea, {
|
|
357
|
+
attachTo: document.body
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const focusSpy = vi.spyOn(wrapper.find('textarea').element, 'focus')
|
|
361
|
+
|
|
362
|
+
;(wrapper.vm as any).focus()
|
|
363
|
+
|
|
364
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
365
|
+
|
|
366
|
+
wrapper.unmount()
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('exposes blur method', async () => {
|
|
370
|
+
const wrapper = mount(Textarea, {
|
|
371
|
+
attachTo: document.body
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const blurSpy = vi.spyOn(wrapper.find('textarea').element, 'blur')
|
|
375
|
+
|
|
376
|
+
;(wrapper.vm as any).blur()
|
|
377
|
+
|
|
378
|
+
expect(blurSpy).toHaveBeenCalled()
|
|
379
|
+
|
|
380
|
+
wrapper.unmount()
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('exposes select method', async () => {
|
|
384
|
+
const wrapper = mount(Textarea, {
|
|
385
|
+
props: { modelValue: 'Select me' },
|
|
386
|
+
attachTo: document.body
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const selectSpy = vi.spyOn(wrapper.find('textarea').element, 'select')
|
|
390
|
+
|
|
391
|
+
;(wrapper.vm as any).select()
|
|
392
|
+
|
|
393
|
+
expect(selectSpy).toHaveBeenCalled()
|
|
394
|
+
|
|
395
|
+
wrapper.unmount()
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('Name attribute', () => {
|
|
400
|
+
it('sets name attribute', () => {
|
|
401
|
+
const wrapper = mount(Textarea, {
|
|
402
|
+
props: { name: 'bio' }
|
|
403
|
+
})
|
|
404
|
+
expect(wrapper.find('textarea').attributes('name')).toBe('bio')
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
describe('Visual parity with Input', () => {
|
|
409
|
+
it('uses same wrapper pattern class structure', () => {
|
|
410
|
+
const wrapper = mount(Textarea)
|
|
411
|
+
// Should have field and wrapper classes similar to Input
|
|
412
|
+
expect(wrapper.find('.ui-textarea-field').exists()).toBe(true)
|
|
413
|
+
expect(wrapper.find('.ui-textarea-wrapper').exists()).toBe(true)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('applies error state classes consistently', () => {
|
|
417
|
+
const wrapper = mount(Textarea, {
|
|
418
|
+
props: { error: 'Error' }
|
|
419
|
+
})
|
|
420
|
+
expect(wrapper.find('.ui-textarea-field--error').exists()).toBe(true)
|
|
421
|
+
expect(wrapper.find('.ui-textarea-wrapper--error').exists()).toBe(true)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('applies disabled state classes consistently', () => {
|
|
425
|
+
const wrapper = mount(Textarea, {
|
|
426
|
+
props: { disabled: true }
|
|
427
|
+
})
|
|
428
|
+
expect(wrapper.find('.ui-textarea-field--disabled').exists()).toBe(true)
|
|
429
|
+
expect(wrapper.find('.ui-textarea-wrapper--disabled').exists()).toBe(true)
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
})
|