@dillingerstaffing/strand-vue 0.4.0
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/dist/components/Alert/Alert.vue.d.ts +31 -0
- package/dist/components/Alert/Alert.vue.d.ts.map +1 -0
- package/dist/components/Alert/index.d.ts +3 -0
- package/dist/components/Alert/index.d.ts.map +1 -0
- package/dist/components/Avatar/Avatar.vue.d.ts +20 -0
- package/dist/components/Avatar/Avatar.vue.d.ts.map +1 -0
- package/dist/components/Avatar/index.d.ts +2 -0
- package/dist/components/Avatar/index.d.ts.map +1 -0
- package/dist/components/Badge/Badge.vue.d.ts +35 -0
- package/dist/components/Badge/Badge.vue.d.ts.map +1 -0
- package/dist/components/Badge/index.d.ts +2 -0
- package/dist/components/Badge/index.d.ts.map +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.vue.d.ts +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.vue.d.ts.map +1 -0
- package/dist/components/Breadcrumb/index.d.ts +3 -0
- package/dist/components/Breadcrumb/index.d.ts.map +1 -0
- package/dist/components/Button/Button.vue.d.ts +46 -0
- package/dist/components/Button/Button.vue.d.ts.map +1 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Card/Card.vue.d.ts +30 -0
- package/dist/components/Card/Card.vue.d.ts.map +1 -0
- package/dist/components/Card/index.d.ts +2 -0
- package/dist/components/Card/index.d.ts.map +1 -0
- package/dist/components/Checkbox/Checkbox.vue.d.ts +23 -0
- package/dist/components/Checkbox/Checkbox.vue.d.ts.map +1 -0
- package/dist/components/Checkbox/index.d.ts +3 -0
- package/dist/components/Checkbox/index.d.ts.map +1 -0
- package/dist/components/Container/Container.vue.d.ts +27 -0
- package/dist/components/Container/Container.vue.d.ts.map +1 -0
- package/dist/components/Container/index.d.ts +2 -0
- package/dist/components/Container/index.d.ts.map +1 -0
- package/dist/components/DataReadout/DataReadout.vue.d.ts +15 -0
- package/dist/components/DataReadout/DataReadout.vue.d.ts.map +1 -0
- package/dist/components/DataReadout/index.d.ts +2 -0
- package/dist/components/DataReadout/index.d.ts.map +1 -0
- package/dist/components/Dialog/Dialog.vue.d.ts +39 -0
- package/dist/components/Dialog/Dialog.vue.d.ts.map +1 -0
- package/dist/components/Dialog/index.d.ts +3 -0
- package/dist/components/Dialog/index.d.ts.map +1 -0
- package/dist/components/Divider/Divider.vue.d.ts +14 -0
- package/dist/components/Divider/Divider.vue.d.ts.map +1 -0
- package/dist/components/Divider/index.d.ts +2 -0
- package/dist/components/Divider/index.d.ts.map +1 -0
- package/dist/components/FormField/FormField.vue.d.ts +32 -0
- package/dist/components/FormField/FormField.vue.d.ts.map +1 -0
- package/dist/components/FormField/index.d.ts +3 -0
- package/dist/components/FormField/index.d.ts.map +1 -0
- package/dist/components/Grid/Grid.vue.d.ts +30 -0
- package/dist/components/Grid/Grid.vue.d.ts.map +1 -0
- package/dist/components/Grid/index.d.ts +2 -0
- package/dist/components/Grid/index.d.ts.map +1 -0
- package/dist/components/Input/Input.vue.d.ts +37 -0
- package/dist/components/Input/Input.vue.d.ts.map +1 -0
- package/dist/components/Input/index.d.ts +3 -0
- package/dist/components/Input/index.d.ts.map +1 -0
- package/dist/components/Link/Link.vue.d.ts +29 -0
- package/dist/components/Link/Link.vue.d.ts.map +1 -0
- package/dist/components/Link/index.d.ts +2 -0
- package/dist/components/Link/index.d.ts.map +1 -0
- package/dist/components/Nav/Nav.vue.d.ts +30 -0
- package/dist/components/Nav/Nav.vue.d.ts.map +1 -0
- package/dist/components/Nav/index.d.ts +3 -0
- package/dist/components/Nav/index.d.ts.map +1 -0
- package/dist/components/Progress/Progress.vue.d.ts +17 -0
- package/dist/components/Progress/Progress.vue.d.ts.map +1 -0
- package/dist/components/Progress/index.d.ts +2 -0
- package/dist/components/Progress/index.d.ts.map +1 -0
- package/dist/components/Radio/Radio.vue.d.ts +22 -0
- package/dist/components/Radio/Radio.vue.d.ts.map +1 -0
- package/dist/components/Radio/index.d.ts +3 -0
- package/dist/components/Radio/index.d.ts.map +1 -0
- package/dist/components/Section/Section.vue.d.ts +30 -0
- package/dist/components/Section/Section.vue.d.ts.map +1 -0
- package/dist/components/Section/index.d.ts +2 -0
- package/dist/components/Section/index.d.ts.map +1 -0
- package/dist/components/Select/Select.vue.d.ts +26 -0
- package/dist/components/Select/Select.vue.d.ts.map +1 -0
- package/dist/components/Select/index.d.ts +3 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Skeleton/Skeleton.vue.d.ts +16 -0
- package/dist/components/Skeleton/Skeleton.vue.d.ts.map +1 -0
- package/dist/components/Skeleton/index.d.ts +2 -0
- package/dist/components/Skeleton/index.d.ts.map +1 -0
- package/dist/components/Slider/Slider.vue.d.ts +24 -0
- package/dist/components/Slider/Slider.vue.d.ts.map +1 -0
- package/dist/components/Slider/index.d.ts +3 -0
- package/dist/components/Slider/index.d.ts.map +1 -0
- package/dist/components/Spinner/Spinner.vue.d.ts +12 -0
- package/dist/components/Spinner/Spinner.vue.d.ts.map +1 -0
- package/dist/components/Spinner/index.d.ts +2 -0
- package/dist/components/Spinner/index.d.ts.map +1 -0
- package/dist/components/Stack/Stack.vue.d.ts +38 -0
- package/dist/components/Stack/Stack.vue.d.ts.map +1 -0
- package/dist/components/Stack/index.d.ts +2 -0
- package/dist/components/Stack/index.d.ts.map +1 -0
- package/dist/components/Switch/Switch.vue.d.ts +18 -0
- package/dist/components/Switch/Switch.vue.d.ts.map +1 -0
- package/dist/components/Switch/index.d.ts +3 -0
- package/dist/components/Switch/index.d.ts.map +1 -0
- package/dist/components/Table/Table.vue.d.ts +23 -0
- package/dist/components/Table/Table.vue.d.ts.map +1 -0
- package/dist/components/Table/index.d.ts +3 -0
- package/dist/components/Table/index.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.vue.d.ts +34 -0
- package/dist/components/Tabs/Tabs.vue.d.ts.map +1 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tag/Tag.vue.d.ts +37 -0
- package/dist/components/Tag/Tag.vue.d.ts.map +1 -0
- package/dist/components/Tag/index.d.ts +2 -0
- package/dist/components/Tag/index.d.ts.map +1 -0
- package/dist/components/Textarea/Textarea.vue.d.ts +29 -0
- package/dist/components/Textarea/Textarea.vue.d.ts.map +1 -0
- package/dist/components/Textarea/index.d.ts +3 -0
- package/dist/components/Textarea/index.d.ts.map +1 -0
- package/dist/components/Toast/Toast.vue.d.ts +16 -0
- package/dist/components/Toast/Toast.vue.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.vue.d.ts +18 -0
- package/dist/components/Toast/ToastProvider.vue.d.ts.map +1 -0
- package/dist/components/Toast/index.d.ts +6 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toast/useToast.d.ts +13 -0
- package/dist/components/Toast/useToast.d.ts.map +1 -0
- package/dist/components/Tooltip/Tooltip.vue.d.ts +29 -0
- package/dist/components/Tooltip/Tooltip.vue.d.ts.map +1 -0
- package/dist/components/Tooltip/index.d.ts +3 -0
- package/dist/components/Tooltip/index.d.ts.map +1 -0
- package/dist/css/strand-ui.css +2534 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1413 -0
- package/dist/index.js.map +1 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/components/Alert/Alert.test.ts +100 -0
- package/src/components/Alert/Alert.vue +54 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/Avatar/Avatar.test.ts +105 -0
- package/src/components/Avatar/Avatar.vue +56 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.test.ts +114 -0
- package/src/components/Badge/Badge.vue +66 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.test.ts +119 -0
- package/src/components/Breadcrumb/Breadcrumb.vue +58 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.test.ts +148 -0
- package/src/components/Button/Button.vue +75 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Card/Card.test.ts +93 -0
- package/src/components/Card/Card.vue +36 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.test.ts +118 -0
- package/src/components/Checkbox/Checkbox.vue +117 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Container/Container.test.ts +70 -0
- package/src/components/Container/Container.vue +32 -0
- package/src/components/Container/index.ts +1 -0
- package/src/components/DataReadout/DataReadout.test.ts +99 -0
- package/src/components/DataReadout/DataReadout.vue +36 -0
- package/src/components/DataReadout/index.ts +1 -0
- package/src/components/Dialog/Dialog.test.ts +224 -0
- package/src/components/Dialog/Dialog.vue +146 -0
- package/src/components/Dialog/index.ts +2 -0
- package/src/components/Divider/Divider.test.ts +95 -0
- package/src/components/Divider/Divider.vue +63 -0
- package/src/components/Divider/index.ts +1 -0
- package/src/components/FormField/FormField.test.ts +98 -0
- package/src/components/FormField/FormField.vue +59 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/Grid/Grid.test.ts +73 -0
- package/src/components/Grid/Grid.vue +34 -0
- package/src/components/Grid/index.ts +1 -0
- package/src/components/Input/Input.test.ts +102 -0
- package/src/components/Input/Input.vue +63 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Link/Link.test.ts +92 -0
- package/src/components/Link/Link.vue +35 -0
- package/src/components/Link/index.ts +1 -0
- package/src/components/Nav/Nav.test.ts +171 -0
- package/src/components/Nav/Nav.vue +81 -0
- package/src/components/Nav/index.ts +2 -0
- package/src/components/Progress/Progress.test.ts +103 -0
- package/src/components/Progress/Progress.vue +96 -0
- package/src/components/Progress/index.ts +1 -0
- package/src/components/Radio/Radio.test.ts +92 -0
- package/src/components/Radio/Radio.vue +60 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Section/Section.test.ts +77 -0
- package/src/components/Section/Section.vue +36 -0
- package/src/components/Section/index.ts +1 -0
- package/src/components/Select/Select.test.ts +102 -0
- package/src/components/Select/Select.vue +70 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Skeleton/Skeleton.test.ts +77 -0
- package/src/components/Skeleton/Skeleton.vue +48 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Slider/Slider.test.ts +73 -0
- package/src/components/Slider/Slider.vue +60 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Spinner/Spinner.test.ts +66 -0
- package/src/components/Spinner/Spinner.vue +33 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/Stack/Stack.test.ts +140 -0
- package/src/components/Stack/Stack.vue +50 -0
- package/src/components/Stack/index.ts +1 -0
- package/src/components/Switch/Switch.test.ts +116 -0
- package/src/components/Switch/Switch.vue +62 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/Table.test.ts +152 -0
- package/src/components/Table/Table.vue +98 -0
- package/src/components/Table/index.ts +2 -0
- package/src/components/Tabs/Tabs.test.ts +138 -0
- package/src/components/Tabs/Tabs.vue +96 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Tag/Tag.test.ts +128 -0
- package/src/components/Tag/Tag.vue +65 -0
- package/src/components/Tag/index.ts +1 -0
- package/src/components/Textarea/Textarea.test.ts +107 -0
- package/src/components/Textarea/Textarea.vue +90 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Toast/Toast.test.ts +204 -0
- package/src/components/Toast/Toast.vue +48 -0
- package/src/components/Toast/ToastProvider.vue +80 -0
- package/src/components/Toast/index.ts +5 -0
- package/src/components/Toast/useToast.ts +26 -0
- package/src/components/Tooltip/Tooltip.test.ts +145 -0
- package/src/components/Tooltip/Tooltip.vue +79 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/index.ts +44 -0
- package/src/test-setup.ts +7 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!--! Strand Vue | MIT License | dillingerstaffing.com -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
export interface SwitchProps {
|
|
6
|
+
/** Controlled checked state */
|
|
7
|
+
checked?: boolean
|
|
8
|
+
/** Disabled state */
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
/** Inline label text */
|
|
11
|
+
label?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<SwitchProps>(), {
|
|
15
|
+
checked: false,
|
|
16
|
+
disabled: false,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'change', checked: boolean): void
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const classes = computed(() =>
|
|
24
|
+
[
|
|
25
|
+
'strand-switch',
|
|
26
|
+
props.checked && 'strand-switch--checked',
|
|
27
|
+
props.disabled && 'strand-switch--disabled',
|
|
28
|
+
]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(' '),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
function handleClick() {
|
|
34
|
+
if (!props.disabled) {
|
|
35
|
+
emit('change', !props.checked)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
40
|
+
if ((event.key === ' ' || event.key === 'Enter') && !props.disabled) {
|
|
41
|
+
event.preventDefault()
|
|
42
|
+
emit('change', !props.checked)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<label :class="classes">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
role="switch"
|
|
52
|
+
class="strand-switch__track"
|
|
53
|
+
:aria-checked="checked ? 'true' : 'false'"
|
|
54
|
+
:disabled="disabled"
|
|
55
|
+
@click="handleClick"
|
|
56
|
+
@keydown="handleKeyDown"
|
|
57
|
+
>
|
|
58
|
+
<span class="strand-switch__thumb" aria-hidden="true" />
|
|
59
|
+
</button>
|
|
60
|
+
<span v-if="label" class="strand-switch__label">{{ label }}</span>
|
|
61
|
+
</label>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*! Strand Vue | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
import { render, fireEvent } from '@testing-library/vue'
|
|
5
|
+
import Table from './Table.vue'
|
|
6
|
+
|
|
7
|
+
const columns = [
|
|
8
|
+
{ key: 'name', header: 'Name', sortable: true },
|
|
9
|
+
{ key: 'role', header: 'Role' },
|
|
10
|
+
{ key: 'status', header: 'Status', sortable: true },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const data = [
|
|
14
|
+
{ name: 'Alice', role: 'Engineer', status: 'Active' },
|
|
15
|
+
{ name: 'Bob', role: 'Designer', status: 'Away' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
describe('Table', () => {
|
|
19
|
+
// -- Rendering --
|
|
20
|
+
|
|
21
|
+
it('renders a table element', () => {
|
|
22
|
+
const { container } = render(Table, {
|
|
23
|
+
props: { columns, data },
|
|
24
|
+
})
|
|
25
|
+
expect(container.querySelector('table')).toBeTruthy()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders column headers', () => {
|
|
29
|
+
const { getByText } = render(Table, {
|
|
30
|
+
props: { columns, data },
|
|
31
|
+
})
|
|
32
|
+
expect(getByText('Name')).toBeTruthy()
|
|
33
|
+
expect(getByText('Role')).toBeTruthy()
|
|
34
|
+
expect(getByText('Status')).toBeTruthy()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders data rows', () => {
|
|
38
|
+
const { getByText } = render(Table, {
|
|
39
|
+
props: { columns, data },
|
|
40
|
+
})
|
|
41
|
+
expect(getByText('Alice')).toBeTruthy()
|
|
42
|
+
expect(getByText('Bob')).toBeTruthy()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders correct number of cells', () => {
|
|
46
|
+
const { container } = render(Table, {
|
|
47
|
+
props: { columns, data },
|
|
48
|
+
})
|
|
49
|
+
const cells = container.querySelectorAll('.strand-table__td')
|
|
50
|
+
// 2 rows x 3 columns = 6 cells
|
|
51
|
+
expect(cells.length).toBe(6)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('renders correct number of header cells', () => {
|
|
55
|
+
const { container } = render(Table, {
|
|
56
|
+
props: { columns, data },
|
|
57
|
+
})
|
|
58
|
+
const headers = container.querySelectorAll('.strand-table__th')
|
|
59
|
+
expect(headers.length).toBe(3)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// -- Sorting --
|
|
63
|
+
|
|
64
|
+
it('renders sort button for sortable columns', () => {
|
|
65
|
+
const { container } = render(Table, {
|
|
66
|
+
props: { columns, data },
|
|
67
|
+
})
|
|
68
|
+
const sortButtons = container.querySelectorAll('.strand-table__sort-btn')
|
|
69
|
+
// "Name" and "Status" are sortable
|
|
70
|
+
expect(sortButtons.length).toBe(2)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('does not render sort button for non-sortable columns', () => {
|
|
74
|
+
const nonSortable = [{ key: 'role', header: 'Role' }]
|
|
75
|
+
const { container } = render(Table, {
|
|
76
|
+
props: { columns: nonSortable, data },
|
|
77
|
+
})
|
|
78
|
+
const sortButtons = container.querySelectorAll('.strand-table__sort-btn')
|
|
79
|
+
expect(sortButtons.length).toBe(0)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('emits sort with key and asc direction on first click', async () => {
|
|
83
|
+
const { container, emitted } = render(Table, {
|
|
84
|
+
props: { columns, data },
|
|
85
|
+
})
|
|
86
|
+
const sortButtons = container.querySelectorAll('.strand-table__sort-btn')
|
|
87
|
+
await fireEvent.click(sortButtons[0])
|
|
88
|
+
expect(emitted().sort[0]).toEqual(['name', 'asc'])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('toggles sort direction on second click of same column', async () => {
|
|
92
|
+
const { container, emitted } = render(Table, {
|
|
93
|
+
props: { columns, data },
|
|
94
|
+
})
|
|
95
|
+
const sortButtons = container.querySelectorAll('.strand-table__sort-btn')
|
|
96
|
+
await fireEvent.click(sortButtons[0]) // asc
|
|
97
|
+
await fireEvent.click(sortButtons[0]) // desc
|
|
98
|
+
expect(emitted().sort[1]).toEqual(['name', 'desc'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// -- Responsive --
|
|
102
|
+
|
|
103
|
+
it('wraps table in overflow-x scroll container', () => {
|
|
104
|
+
const { container } = render(Table, {
|
|
105
|
+
props: { columns, data },
|
|
106
|
+
})
|
|
107
|
+
const wrapper = container.querySelector('.strand-table-wrapper')
|
|
108
|
+
expect(wrapper).toBeTruthy()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// -- Empty state --
|
|
112
|
+
|
|
113
|
+
it('renders empty tbody when data is empty', () => {
|
|
114
|
+
const { container } = render(Table, {
|
|
115
|
+
props: { columns, data: [] },
|
|
116
|
+
})
|
|
117
|
+
const rows = container.querySelectorAll('.strand-table__row')
|
|
118
|
+
expect(rows.length).toBe(0)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// -- Column width --
|
|
122
|
+
|
|
123
|
+
it('applies width style to column headers', () => {
|
|
124
|
+
const cols = [{ key: 'name', header: 'Name', width: '200px' }]
|
|
125
|
+
const { container } = render(Table, {
|
|
126
|
+
props: { columns: cols, data: [] },
|
|
127
|
+
})
|
|
128
|
+
const th = container.querySelector('.strand-table__th') as HTMLElement
|
|
129
|
+
expect(th?.style.width).toBe('200px')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// -- Sort indicator --
|
|
133
|
+
|
|
134
|
+
it('shows sort indicator on sortable columns', () => {
|
|
135
|
+
const { container } = render(Table, {
|
|
136
|
+
props: { columns, data },
|
|
137
|
+
})
|
|
138
|
+
const indicators = container.querySelectorAll('.strand-table__sort-indicator')
|
|
139
|
+
expect(indicators.length).toBe(2)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// -- Sort button aria-label --
|
|
143
|
+
|
|
144
|
+
it('sort button has aria-label with column name', () => {
|
|
145
|
+
const { container } = render(Table, {
|
|
146
|
+
props: { columns, data },
|
|
147
|
+
})
|
|
148
|
+
const sortButtons = container.querySelectorAll('.strand-table__sort-btn')
|
|
149
|
+
expect(sortButtons[0]).toHaveAttribute('aria-label', 'Sort by Name')
|
|
150
|
+
expect(sortButtons[1]).toHaveAttribute('aria-label', 'Sort by Status')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--! Strand Vue | MIT License | dillingerstaffing.com -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
export interface TableColumn {
|
|
6
|
+
/** Unique key matching the data field */
|
|
7
|
+
key: string
|
|
8
|
+
/** Display header text */
|
|
9
|
+
header: string
|
|
10
|
+
/** Whether the column is sortable */
|
|
11
|
+
sortable?: boolean
|
|
12
|
+
/** Optional fixed width */
|
|
13
|
+
width?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TableProps {
|
|
17
|
+
/** Column definitions */
|
|
18
|
+
columns: TableColumn[]
|
|
19
|
+
/** Row data */
|
|
20
|
+
data: Array<Record<string, unknown>>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = defineProps<TableProps>()
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
(e: 'sort', key: string, direction: 'asc' | 'desc'): void
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const sortKey = ref<string | null>(null)
|
|
30
|
+
const sortDirection = ref<'asc' | 'desc'>('asc')
|
|
31
|
+
|
|
32
|
+
const wrapperClasses = computed(() =>
|
|
33
|
+
['strand-table-wrapper'].filter(Boolean).join(' '),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
function handleSort(key: string) {
|
|
37
|
+
const nextDirection =
|
|
38
|
+
sortKey.value === key && sortDirection.value === 'asc' ? 'desc' : 'asc'
|
|
39
|
+
sortKey.value = key
|
|
40
|
+
sortDirection.value = nextDirection
|
|
41
|
+
emit('sort', key, nextDirection)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sortIndicator(key: string): string {
|
|
45
|
+
if (sortKey.value === key) {
|
|
46
|
+
return sortDirection.value === 'asc' ? '\u2191' : '\u2193'
|
|
47
|
+
}
|
|
48
|
+
return '\u2195'
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div :class="wrapperClasses">
|
|
54
|
+
<table class="strand-table">
|
|
55
|
+
<thead class="strand-table__head">
|
|
56
|
+
<tr>
|
|
57
|
+
<th
|
|
58
|
+
v-for="col in columns"
|
|
59
|
+
:key="col.key"
|
|
60
|
+
class="strand-table__th"
|
|
61
|
+
:style="col.width ? { width: col.width } : undefined"
|
|
62
|
+
>
|
|
63
|
+
<button
|
|
64
|
+
v-if="col.sortable"
|
|
65
|
+
type="button"
|
|
66
|
+
class="strand-table__sort-btn"
|
|
67
|
+
:aria-label="`Sort by ${col.header}`"
|
|
68
|
+
@click="handleSort(col.key)"
|
|
69
|
+
>
|
|
70
|
+
{{ col.header }}
|
|
71
|
+
<span class="strand-table__sort-indicator" aria-hidden="true">
|
|
72
|
+
{{ sortIndicator(col.key) }}
|
|
73
|
+
</span>
|
|
74
|
+
</button>
|
|
75
|
+
<template v-else>
|
|
76
|
+
{{ col.header }}
|
|
77
|
+
</template>
|
|
78
|
+
</th>
|
|
79
|
+
</tr>
|
|
80
|
+
</thead>
|
|
81
|
+
<tbody class="strand-table__body">
|
|
82
|
+
<tr
|
|
83
|
+
v-for="(row, rowIndex) in data"
|
|
84
|
+
:key="rowIndex"
|
|
85
|
+
class="strand-table__row"
|
|
86
|
+
>
|
|
87
|
+
<td
|
|
88
|
+
v-for="col in columns"
|
|
89
|
+
:key="col.key"
|
|
90
|
+
class="strand-table__td"
|
|
91
|
+
>
|
|
92
|
+
{{ row[col.key] }}
|
|
93
|
+
</td>
|
|
94
|
+
</tr>
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*! Strand Vue | MIT License | dillingerstaffing.com */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
import { render, fireEvent } from '@testing-library/vue'
|
|
5
|
+
import Tabs from './Tabs.vue'
|
|
6
|
+
|
|
7
|
+
const sampleTabs = [
|
|
8
|
+
{ id: 'one', label: 'Tab One' },
|
|
9
|
+
{ id: 'two', label: 'Tab Two' },
|
|
10
|
+
{ id: 'three', label: 'Tab Three' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function renderTabs(props: Record<string, unknown> = {}, slots: Record<string, string> = {}) {
|
|
14
|
+
return render(Tabs, {
|
|
15
|
+
props: {
|
|
16
|
+
tabs: sampleTabs,
|
|
17
|
+
activeTab: 'one',
|
|
18
|
+
...props,
|
|
19
|
+
},
|
|
20
|
+
slots: {
|
|
21
|
+
'panel-one': '<div>Content One</div>',
|
|
22
|
+
'panel-two': '<div>Content Two</div>',
|
|
23
|
+
'panel-three': '<div>Content Three</div>',
|
|
24
|
+
...slots,
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Tabs', () => {
|
|
30
|
+
// -- Structure --
|
|
31
|
+
|
|
32
|
+
it('renders tablist role', () => {
|
|
33
|
+
const { getByRole } = renderTabs()
|
|
34
|
+
expect(getByRole('tablist')).toBeTruthy()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('renders tab buttons for each tab', () => {
|
|
38
|
+
const { getAllByRole } = renderTabs()
|
|
39
|
+
expect(getAllByRole('tab')).toHaveLength(3)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// -- Active state --
|
|
43
|
+
|
|
44
|
+
it('active tab has aria-selected true', () => {
|
|
45
|
+
const { getAllByRole } = renderTabs()
|
|
46
|
+
const tabs = getAllByRole('tab')
|
|
47
|
+
expect(tabs[0]).toHaveAttribute('aria-selected', 'true')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('inactive tabs have aria-selected false', () => {
|
|
51
|
+
const { getAllByRole } = renderTabs()
|
|
52
|
+
const tabs = getAllByRole('tab')
|
|
53
|
+
expect(tabs[1]).toHaveAttribute('aria-selected', 'false')
|
|
54
|
+
expect(tabs[2]).toHaveAttribute('aria-selected', 'false')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('active tab has tabindex 0, inactive tabs have tabindex -1', () => {
|
|
58
|
+
const { getAllByRole } = renderTabs({ activeTab: 'two' })
|
|
59
|
+
const tabs = getAllByRole('tab')
|
|
60
|
+
expect(tabs[0]).toHaveAttribute('tabindex', '-1')
|
|
61
|
+
expect(tabs[1]).toHaveAttribute('tabindex', '0')
|
|
62
|
+
expect(tabs[2]).toHaveAttribute('tabindex', '-1')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// -- Interaction --
|
|
66
|
+
|
|
67
|
+
it('clicking tab emits change with the tab id', async () => {
|
|
68
|
+
const { getAllByRole, emitted } = renderTabs()
|
|
69
|
+
await fireEvent.click(getAllByRole('tab')[1])
|
|
70
|
+
expect(emitted().change[0]).toEqual(['two'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// -- Panels --
|
|
74
|
+
|
|
75
|
+
it('active panel is visible', () => {
|
|
76
|
+
const { getByText } = renderTabs()
|
|
77
|
+
expect(getByText('Content One').closest('[role="tabpanel"]')).not.toHaveAttribute('hidden')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('inactive panels are hidden', () => {
|
|
81
|
+
const { getByText } = renderTabs()
|
|
82
|
+
expect(getByText('Content Two').closest('[role="tabpanel"]')).toHaveAttribute('hidden')
|
|
83
|
+
expect(getByText('Content Three').closest('[role="tabpanel"]')).toHaveAttribute('hidden')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('tabpanel has aria-labelledby pointing to its tab', () => {
|
|
87
|
+
const { getAllByRole } = renderTabs()
|
|
88
|
+
const panels = getAllByRole('tabpanel', { hidden: true })
|
|
89
|
+
expect(panels[0]).toHaveAttribute('aria-labelledby', 'tab-one')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// -- Keyboard navigation --
|
|
93
|
+
|
|
94
|
+
it('ArrowRight moves to next tab', async () => {
|
|
95
|
+
const { getAllByRole, emitted } = renderTabs()
|
|
96
|
+
await fireEvent.keyDown(getAllByRole('tab')[0], { key: 'ArrowRight' })
|
|
97
|
+
expect(emitted().change[0]).toEqual(['two'])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('ArrowLeft moves to previous tab', async () => {
|
|
101
|
+
const { getAllByRole, emitted } = renderTabs({ activeTab: 'two' })
|
|
102
|
+
await fireEvent.keyDown(getAllByRole('tab')[1], { key: 'ArrowLeft' })
|
|
103
|
+
expect(emitted().change[0]).toEqual(['one'])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('ArrowRight wraps to first tab from last', async () => {
|
|
107
|
+
const { getAllByRole, emitted } = renderTabs({ activeTab: 'three' })
|
|
108
|
+
await fireEvent.keyDown(getAllByRole('tab')[2], { key: 'ArrowRight' })
|
|
109
|
+
expect(emitted().change[0]).toEqual(['one'])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('ArrowLeft wraps to last tab from first', async () => {
|
|
113
|
+
const { getAllByRole, emitted } = renderTabs({ activeTab: 'one' })
|
|
114
|
+
await fireEvent.keyDown(getAllByRole('tab')[0], { key: 'ArrowLeft' })
|
|
115
|
+
expect(emitted().change[0]).toEqual(['three'])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('Home moves focus to first tab', async () => {
|
|
119
|
+
const { getAllByRole, emitted } = renderTabs({ activeTab: 'three' })
|
|
120
|
+
await fireEvent.keyDown(getAllByRole('tab')[2], { key: 'Home' })
|
|
121
|
+
expect(emitted().change[0]).toEqual(['one'])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('End moves focus to last tab', async () => {
|
|
125
|
+
const { getAllByRole, emitted } = renderTabs({ activeTab: 'one' })
|
|
126
|
+
await fireEvent.keyDown(getAllByRole('tab')[0], { key: 'End' })
|
|
127
|
+
expect(emitted().change[0]).toEqual(['three'])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// -- Tab aria-controls --
|
|
131
|
+
|
|
132
|
+
it('tab has aria-controls pointing to panel', () => {
|
|
133
|
+
const { getAllByRole } = renderTabs()
|
|
134
|
+
const tabs = getAllByRole('tab')
|
|
135
|
+
expect(tabs[0]).toHaveAttribute('aria-controls', 'panel-one')
|
|
136
|
+
expect(tabs[1]).toHaveAttribute('aria-controls', 'panel-two')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!--! Strand Vue | MIT License | dillingerstaffing.com -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
export interface TabItem {
|
|
6
|
+
id: string
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TabsProps {
|
|
11
|
+
/** Tab definitions (id + label only; content is provided via slots) */
|
|
12
|
+
tabs: TabItem[]
|
|
13
|
+
/** Currently active tab id (controlled) */
|
|
14
|
+
activeTab: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = defineProps<TabsProps>()
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'change', id: string): void
|
|
21
|
+
}>()
|
|
22
|
+
|
|
23
|
+
const tablistRef = ref<HTMLDivElement | null>(null)
|
|
24
|
+
|
|
25
|
+
const classes = computed(() => ['strand-tabs'].filter(Boolean).join(' '))
|
|
26
|
+
|
|
27
|
+
function focusAndSelect(index: number) {
|
|
28
|
+
const tab = props.tabs[index]
|
|
29
|
+
if (tab) {
|
|
30
|
+
emit('change', tab.id)
|
|
31
|
+
const buttons = tablistRef.value?.querySelectorAll<HTMLButtonElement>('[role="tab"]')
|
|
32
|
+
buttons?.[index]?.focus()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
37
|
+
const currentIndex = props.tabs.findIndex((t) => t.id === props.activeTab)
|
|
38
|
+
let nextIndex: number | null = null
|
|
39
|
+
|
|
40
|
+
switch (event.key) {
|
|
41
|
+
case 'ArrowRight':
|
|
42
|
+
nextIndex = (currentIndex + 1) % props.tabs.length
|
|
43
|
+
break
|
|
44
|
+
case 'ArrowLeft':
|
|
45
|
+
nextIndex = (currentIndex - 1 + props.tabs.length) % props.tabs.length
|
|
46
|
+
break
|
|
47
|
+
case 'Home':
|
|
48
|
+
nextIndex = 0
|
|
49
|
+
break
|
|
50
|
+
case 'End':
|
|
51
|
+
nextIndex = props.tabs.length - 1
|
|
52
|
+
break
|
|
53
|
+
default:
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
focusAndSelect(nextIndex)
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div :class="classes">
|
|
64
|
+
<div ref="tablistRef" role="tablist" @keydown="handleKeyDown">
|
|
65
|
+
<button
|
|
66
|
+
v-for="tab in tabs"
|
|
67
|
+
:key="tab.id"
|
|
68
|
+
:id="`tab-${tab.id}`"
|
|
69
|
+
role="tab"
|
|
70
|
+
type="button"
|
|
71
|
+
:class="[
|
|
72
|
+
'strand-tabs__tab',
|
|
73
|
+
tab.id === activeTab && 'strand-tabs__tab--active',
|
|
74
|
+
].filter(Boolean).join(' ')"
|
|
75
|
+
:aria-selected="tab.id === activeTab ? 'true' : 'false'"
|
|
76
|
+
:aria-controls="`panel-${tab.id}`"
|
|
77
|
+
:tabindex="tab.id === activeTab ? 0 : -1"
|
|
78
|
+
@click="emit('change', tab.id)"
|
|
79
|
+
>
|
|
80
|
+
{{ tab.label }}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div
|
|
85
|
+
v-for="tab in tabs"
|
|
86
|
+
:key="tab.id"
|
|
87
|
+
:id="`panel-${tab.id}`"
|
|
88
|
+
role="tabpanel"
|
|
89
|
+
:aria-labelledby="`tab-${tab.id}`"
|
|
90
|
+
:hidden="tab.id !== activeTab || undefined"
|
|
91
|
+
:tabindex="0"
|
|
92
|
+
>
|
|
93
|
+
<slot :name="`panel-${tab.id}`" />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, fireEvent } from '@testing-library/vue'
|
|
3
|
+
import Tag from './Tag.vue'
|
|
4
|
+
|
|
5
|
+
describe('Tag', () => {
|
|
6
|
+
// ── Rendering ──
|
|
7
|
+
|
|
8
|
+
it('renders a span element', () => {
|
|
9
|
+
const { container } = render(Tag, { slots: { default: 'Label' } })
|
|
10
|
+
expect(container.firstElementChild?.tagName).toBe('SPAN')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders slot content inside text span', () => {
|
|
14
|
+
const { container } = render(Tag, { slots: { default: 'Active' } })
|
|
15
|
+
const textSpan = container.querySelector('.strand-tag__text')
|
|
16
|
+
expect(textSpan?.textContent).toBe('Active')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// ── Variants ──
|
|
20
|
+
|
|
21
|
+
it('applies solid variant class by default', () => {
|
|
22
|
+
const { container } = render(Tag, { slots: { default: 'Test' } })
|
|
23
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--solid')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('applies outlined variant class', () => {
|
|
27
|
+
const { container } = render(Tag, {
|
|
28
|
+
props: { variant: 'outlined' },
|
|
29
|
+
slots: { default: 'Test' },
|
|
30
|
+
})
|
|
31
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--outlined')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// ── Status colors ──
|
|
35
|
+
|
|
36
|
+
it('applies default status class by default', () => {
|
|
37
|
+
const { container } = render(Tag, { slots: { default: 'Test' } })
|
|
38
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--default')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('applies teal status class', () => {
|
|
42
|
+
const { container } = render(Tag, {
|
|
43
|
+
props: { status: 'teal' },
|
|
44
|
+
slots: { default: 'Test' },
|
|
45
|
+
})
|
|
46
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--teal')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('applies red status class', () => {
|
|
50
|
+
const { container } = render(Tag, {
|
|
51
|
+
props: { status: 'red' },
|
|
52
|
+
slots: { default: 'Test' },
|
|
53
|
+
})
|
|
54
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--red')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('applies amber status class', () => {
|
|
58
|
+
const { container } = render(Tag, {
|
|
59
|
+
props: { status: 'amber' },
|
|
60
|
+
slots: { default: 'Test' },
|
|
61
|
+
})
|
|
62
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--amber')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('applies blue status class', () => {
|
|
66
|
+
const { container } = render(Tag, {
|
|
67
|
+
props: { status: 'blue' },
|
|
68
|
+
slots: { default: 'Test' },
|
|
69
|
+
})
|
|
70
|
+
expect(container.firstElementChild?.className).toContain('strand-tag--blue')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// ── Removable ──
|
|
74
|
+
|
|
75
|
+
it('does not show remove button by default', () => {
|
|
76
|
+
const { container } = render(Tag, { slots: { default: 'Test' } })
|
|
77
|
+
expect(container.querySelector('.strand-tag__remove')).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('shows remove button when removable', () => {
|
|
81
|
+
const { container } = render(Tag, {
|
|
82
|
+
props: { removable: true },
|
|
83
|
+
slots: { default: 'Test' },
|
|
84
|
+
})
|
|
85
|
+
expect(container.querySelector('.strand-tag__remove')).toBeTruthy()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('remove button has aria-label', () => {
|
|
89
|
+
const { container } = render(Tag, {
|
|
90
|
+
props: { removable: true },
|
|
91
|
+
slots: { default: 'Test' },
|
|
92
|
+
})
|
|
93
|
+
const btn = container.querySelector('.strand-tag__remove')
|
|
94
|
+
expect(btn?.getAttribute('aria-label')).toBe('Remove')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('emits remove event on button click', async () => {
|
|
98
|
+
const { container, emitted } = render(Tag, {
|
|
99
|
+
props: { removable: true },
|
|
100
|
+
slots: { default: 'Test' },
|
|
101
|
+
})
|
|
102
|
+
const btn = container.querySelector('.strand-tag__remove') as HTMLButtonElement
|
|
103
|
+
await fireEvent.click(btn)
|
|
104
|
+
expect(emitted().remove).toHaveLength(1)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('remove button contains SVG with aria-hidden', () => {
|
|
108
|
+
const { container } = render(Tag, {
|
|
109
|
+
props: { removable: true },
|
|
110
|
+
slots: { default: 'Test' },
|
|
111
|
+
})
|
|
112
|
+
const svg = container.querySelector('.strand-tag__remove svg')
|
|
113
|
+
expect(svg).toBeTruthy()
|
|
114
|
+
expect(svg?.getAttribute('aria-hidden')).toBe('true')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ── Custom className ──
|
|
118
|
+
|
|
119
|
+
it('merges custom className', () => {
|
|
120
|
+
const { container } = render(Tag, {
|
|
121
|
+
props: { className: 'custom' },
|
|
122
|
+
slots: { default: 'Test' },
|
|
123
|
+
})
|
|
124
|
+
const el = container.firstElementChild
|
|
125
|
+
expect(el?.className).toContain('strand-tag')
|
|
126
|
+
expect(el?.className).toContain('custom')
|
|
127
|
+
})
|
|
128
|
+
})
|