@davidbirchall/core 1.0.6 → 1.0.8
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/.storybook/main.ts +18 -0
- package/.storybook/preview.ts +14 -0
- package/package.json +1 -4
- package/src/components/Badge/Badge.stories.ts +147 -0
- package/src/components/Badge/Badge.test.ts +57 -0
- package/src/components/Badge/Badge.vue +79 -0
- package/src/components/Button/Button.stories.ts +80 -0
- package/src/components/Button/Button.test.ts +145 -0
- package/src/components/Button/Button.vue +108 -0
- package/src/components/Button/types.ts +4 -0
- package/src/components/Calendar/Calendar.stories.ts +261 -0
- package/src/components/Calendar/Calendar.test.ts +119 -0
- package/src/components/Calendar/Calendar.vue +528 -0
- package/src/components/Calendar/types.ts +20 -0
- package/src/components/Card/Card.stories.ts +88 -0
- package/src/components/Card/Card.test.ts +173 -0
- package/src/components/Card/Card.vue +59 -0
- package/{dist/Card/types.d.ts → src/components/Card/types.ts} +1 -1
- package/src/components/Checkbox/Checkbox.stories.ts +126 -0
- package/src/components/Checkbox/Checkbox.test.ts +155 -0
- package/src/components/Checkbox/Checkbox.vue +121 -0
- package/src/components/Checkbox/types.ts +7 -0
- package/src/components/DataTable/DataTable.stories.ts +156 -0
- package/src/components/DataTable/DataTable.test.ts +185 -0
- package/src/components/DataTable/DataTable.vue +177 -0
- package/src/components/DataTable/types.ts +12 -0
- package/src/components/DatePicker/DatePicker.stories.ts +172 -0
- package/src/components/DatePicker/DatePicker.test.ts +87 -0
- package/src/components/DatePicker/DatePicker.vue +302 -0
- package/src/components/Dropdown/Dropdown.stories.ts +231 -0
- package/src/components/Dropdown/Dropdown.vue +314 -0
- package/src/components/Dropdown/types.ts +14 -0
- package/src/components/EmptyState/EmptyState.stories.ts +189 -0
- package/src/components/EmptyState/EmptyState.vue +215 -0
- package/src/components/EmptyState/types.ts +8 -0
- package/src/components/ErrorSummary/ErrorSummary.vue +78 -0
- package/src/components/ErrorSummary/types.ts +4 -0
- package/src/components/FormGroup/FormGroup.stories.ts +264 -0
- package/src/components/FormGroup/FormGroup.test.ts +63 -0
- package/src/components/FormGroup/FormGroup.vue +58 -0
- package/src/components/Heading/Heading.stories.ts +121 -0
- package/src/components/Heading/Heading.test.ts +184 -0
- package/src/components/Heading/Heading.vue +95 -0
- package/src/components/Heading/types.ts +6 -0
- package/src/components/Input/Input.stories.ts +172 -0
- package/src/components/Input/Input.test.ts +213 -0
- package/src/components/Input/Input.vue +121 -0
- package/src/components/Input/types.ts +11 -0
- package/src/components/Modal/Modal.stories.ts +341 -0
- package/src/components/Modal/Modal.test.ts +99 -0
- package/src/components/Modal/Modal.vue +278 -0
- package/src/components/ProgressBar/ProgressBar.stories.ts +313 -0
- package/src/components/ProgressBar/ProgressBar.test.ts +98 -0
- package/src/components/ProgressBar/ProgressBar.vue +117 -0
- package/src/components/Select/Select.stories.ts +177 -0
- package/src/components/Select/Select.test.ts +225 -0
- package/src/components/Select/Select.vue +147 -0
- package/src/components/Select/types.ts +16 -0
- package/src/components/StatCard/StatCard.stories.ts +274 -0
- package/src/components/StatCard/StatCard.vue +226 -0
- package/src/components/StatCard/types.ts +12 -0
- package/src/components/Tag/Tag.stories.ts +78 -0
- package/src/components/Tag/Tag.test.ts +50 -0
- package/src/components/Tag/Tag.vue +71 -0
- package/src/components/Tag/types.ts +4 -0
- package/src/components/TextArea/TextArea.stories.ts +171 -0
- package/src/components/TextArea/TextArea.test.ts +202 -0
- package/src/components/TextArea/TextArea.vue +122 -0
- package/src/components/TextArea/types.ts +11 -0
- package/src/components/index.ts +5 -0
- package/src/test/setup.ts +1 -0
- package/src/vite-env.d.ts +6 -0
- package/tsconfig.json +29 -0
- package/vite.config.ts +33 -0
- package/vitest.config.ts +28 -0
- package/dist/Button/types.d.ts +0 -4
- package/dist/Calendar/types.d.ts +0 -22
- package/dist/Checkbox/types.d.ts +0 -7
- package/dist/DataTable/types.d.ts +0 -11
- package/dist/Dropdown/types.d.ts +0 -13
- package/dist/EmptyState/types.d.ts +0 -8
- package/dist/ErrorSummary/types.d.ts +0 -4
- package/dist/Heading/types.d.ts +0 -6
- package/dist/Input/types.d.ts +0 -11
- package/dist/Select/types.d.ts +0 -15
- package/dist/StatCard/types.d.ts +0 -12
- package/dist/Tag/types.d.ts +0 -4
- package/dist/TextArea/types.d.ts +0 -11
- package/dist/core.css +0 -1
- package/dist/core.js +0 -24
- package/dist/core.js.map +0 -1
- package/dist/core.umd.cjs +0 -2
- package/dist/core.umd.cjs.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/package.json +0 -27
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/vue'
|
|
3
|
+
import Card from './Card.vue'
|
|
4
|
+
|
|
5
|
+
describe('Card', () => {
|
|
6
|
+
describe('rendering', () => {
|
|
7
|
+
it('renders default slot content', () => {
|
|
8
|
+
render(Card, {
|
|
9
|
+
slots: {
|
|
10
|
+
default: '<p>Card content</p>'
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect(screen.getByText('Card content')).toBeInTheDocument()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders header slot when provided', () => {
|
|
18
|
+
const { container } = render(Card, {
|
|
19
|
+
slots: {
|
|
20
|
+
header: 'Card Header',
|
|
21
|
+
default: 'Card content'
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const header = container.querySelector('.card__header')
|
|
26
|
+
expect(header).toBeInTheDocument()
|
|
27
|
+
expect(header).toHaveTextContent('Card Header')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('does not render header section when slot is not provided', () => {
|
|
31
|
+
const { container } = render(Card, {
|
|
32
|
+
slots: {
|
|
33
|
+
default: 'Card content'
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const header = container.querySelector('.card__header')
|
|
38
|
+
expect(header).not.toBeInTheDocument()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders footer slot when provided', () => {
|
|
42
|
+
const { container } = render(Card, {
|
|
43
|
+
slots: {
|
|
44
|
+
default: 'Card content',
|
|
45
|
+
footer: 'Card Footer'
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const footer = container.querySelector('.card__footer')
|
|
50
|
+
expect(footer).toBeInTheDocument()
|
|
51
|
+
expect(footer).toHaveTextContent('Card Footer')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('does not render footer section when slot is not provided', () => {
|
|
55
|
+
const { container } = render(Card, {
|
|
56
|
+
slots: {
|
|
57
|
+
default: 'Card content'
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const footer = container.querySelector('.card__footer')
|
|
62
|
+
expect(footer).not.toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders all slots together', () => {
|
|
66
|
+
const { container } = render(Card, {
|
|
67
|
+
slots: {
|
|
68
|
+
header: 'Header',
|
|
69
|
+
default: 'Body',
|
|
70
|
+
footer: 'Footer'
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(container.querySelector('.card__header')).toBeInTheDocument()
|
|
75
|
+
expect(container.querySelector('.card__body')).toBeInTheDocument()
|
|
76
|
+
expect(container.querySelector('.card__footer')).toBeInTheDocument()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('hoverable prop', () => {
|
|
81
|
+
it('does not apply hoverable class by default', () => {
|
|
82
|
+
const { container } = render(Card, {
|
|
83
|
+
slots: {
|
|
84
|
+
default: 'Content'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const card = container.querySelector('.card')
|
|
89
|
+
expect(card).not.toHaveClass('card--hoverable')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('applies hoverable class when prop is true', () => {
|
|
93
|
+
const { container } = render(Card, {
|
|
94
|
+
props: {
|
|
95
|
+
hoverable: true
|
|
96
|
+
},
|
|
97
|
+
slots: {
|
|
98
|
+
default: 'Content'
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const card = container.querySelector('.card')
|
|
103
|
+
expect(card).toHaveClass('card--hoverable')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not apply hoverable class when prop is false', () => {
|
|
107
|
+
const { container } = render(Card, {
|
|
108
|
+
props: {
|
|
109
|
+
hoverable: false
|
|
110
|
+
},
|
|
111
|
+
slots: {
|
|
112
|
+
default: 'Content'
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const card = container.querySelector('.card')
|
|
117
|
+
expect(card).not.toHaveClass('card--hoverable')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('styling', () => {
|
|
122
|
+
it('always applies base card class', () => {
|
|
123
|
+
const { container } = render(Card, {
|
|
124
|
+
slots: {
|
|
125
|
+
default: 'Content'
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const card = container.querySelector('.card')
|
|
130
|
+
expect(card).toHaveClass('card')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('maintains card structure with body wrapper', () => {
|
|
134
|
+
const { container } = render(Card, {
|
|
135
|
+
slots: {
|
|
136
|
+
default: 'Test content'
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const body = container.querySelector('.card__body')
|
|
141
|
+
expect(body).toBeInTheDocument()
|
|
142
|
+
expect(body).toHaveTextContent('Test content')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('complex content', () => {
|
|
147
|
+
it('handles complex HTML in slots', () => {
|
|
148
|
+
const { container } = render(Card, {
|
|
149
|
+
slots: {
|
|
150
|
+
header: '<h2>Title</h2>',
|
|
151
|
+
default: '<div><p>Paragraph 1</p><p>Paragraph 2</p></div>',
|
|
152
|
+
footer: '<button>Action</button>'
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(container.querySelector('.card__header h2')).toBeInTheDocument()
|
|
157
|
+
expect(container.querySelectorAll('.card__body p')).toHaveLength(2)
|
|
158
|
+
expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('handles nested components in slots', () => {
|
|
162
|
+
const { container } = render(Card, {
|
|
163
|
+
slots: {
|
|
164
|
+
default: '<div class="custom-content">Nested</div>'
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const customContent = container.querySelector('.custom-content')
|
|
169
|
+
expect(customContent).toBeInTheDocument()
|
|
170
|
+
expect(customContent).toHaveTextContent('Nested')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="['card', { 'card--hoverable': hoverable }]">
|
|
3
|
+
<div v-if="$slots.header" class="card__header">
|
|
4
|
+
<slot name="header" />
|
|
5
|
+
</div>
|
|
6
|
+
<div class="card__body">
|
|
7
|
+
<slot />
|
|
8
|
+
</div>
|
|
9
|
+
<div v-if="$slots.footer" class="card__footer">
|
|
10
|
+
<slot name="footer" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
export interface CardProps {
|
|
17
|
+
hoverable?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
withDefaults(defineProps<CardProps>(), {
|
|
21
|
+
hoverable: false
|
|
22
|
+
})
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
.card {
|
|
27
|
+
background: white;
|
|
28
|
+
border: 1px solid #e5e7eb;
|
|
29
|
+
border-radius: 0.5rem;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.card--hoverable {
|
|
34
|
+
transition: all 0.2s ease-in-out;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.card--hoverable:hover {
|
|
39
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
40
|
+
transform: translateY(-2px);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.card__header {
|
|
44
|
+
padding: 1rem 1.5rem;
|
|
45
|
+
border-bottom: 1px solid #e5e7eb;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
font-size: 1.125rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.card__body {
|
|
51
|
+
padding: 1.5rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.card__footer {
|
|
55
|
+
padding: 1rem 1.5rem;
|
|
56
|
+
border-top: 1px solid #e5e7eb;
|
|
57
|
+
background-color: #f9fafb;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import Checkbox from './Checkbox.vue'
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Form Fields/Checkbox',
|
|
7
|
+
component: Checkbox,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
modelValue: {
|
|
11
|
+
control: 'boolean',
|
|
12
|
+
description: 'Checked state'
|
|
13
|
+
},
|
|
14
|
+
label: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
description: 'Label text'
|
|
17
|
+
},
|
|
18
|
+
disabled: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
description: 'Disabled state'
|
|
21
|
+
},
|
|
22
|
+
required: {
|
|
23
|
+
control: 'boolean',
|
|
24
|
+
description: 'Required field'
|
|
25
|
+
},
|
|
26
|
+
error: {
|
|
27
|
+
control: 'text',
|
|
28
|
+
description: 'Error message'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
modelValue: false,
|
|
33
|
+
disabled: false,
|
|
34
|
+
required: false
|
|
35
|
+
}
|
|
36
|
+
} satisfies Meta<typeof Checkbox>
|
|
37
|
+
|
|
38
|
+
export default meta
|
|
39
|
+
type Story = StoryObj<typeof meta>
|
|
40
|
+
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
label: 'Accept terms and conditions'
|
|
44
|
+
},
|
|
45
|
+
render: (args: any) => ({
|
|
46
|
+
components: { Checkbox },
|
|
47
|
+
setup() {
|
|
48
|
+
const checked = ref(args.modelValue ?? false)
|
|
49
|
+
return { args, checked }
|
|
50
|
+
},
|
|
51
|
+
template: '<Checkbox v-bind="args" v-model="checked" />'
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Checked: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
label: 'Subscribe to newsletter',
|
|
58
|
+
modelValue: true
|
|
59
|
+
},
|
|
60
|
+
render: (args: any) => ({
|
|
61
|
+
components: { Checkbox },
|
|
62
|
+
setup() {
|
|
63
|
+
const checked = ref(args.modelValue ?? false)
|
|
64
|
+
return { args, checked }
|
|
65
|
+
},
|
|
66
|
+
template: '<Checkbox v-bind="args" v-model="checked" />'
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const Disabled: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
label: 'Disabled option',
|
|
73
|
+
disabled: true
|
|
74
|
+
},
|
|
75
|
+
render: (args: any) => ({
|
|
76
|
+
components: { Checkbox },
|
|
77
|
+
setup() {
|
|
78
|
+
const checked = ref(false)
|
|
79
|
+
return { args, checked }
|
|
80
|
+
},
|
|
81
|
+
template: '<Checkbox v-bind="args" v-model="checked" />'
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const DisabledChecked: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
label: 'Disabled checked option',
|
|
88
|
+
disabled: true,
|
|
89
|
+
modelValue: true
|
|
90
|
+
},
|
|
91
|
+
render: (args: any) => ({
|
|
92
|
+
components: { Checkbox },
|
|
93
|
+
setup() {
|
|
94
|
+
const checked = ref(args.modelValue ?? false)
|
|
95
|
+
return { args, checked }
|
|
96
|
+
},
|
|
97
|
+
template: '<Checkbox v-bind="args" v-model="checked" />'
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const WithError: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
label: 'I agree to the terms',
|
|
104
|
+
error: 'You must accept the terms to continue'
|
|
105
|
+
},
|
|
106
|
+
render: (args: any) => ({
|
|
107
|
+
components: { Checkbox },
|
|
108
|
+
setup() {
|
|
109
|
+
const checked = ref(false)
|
|
110
|
+
return { args, checked }
|
|
111
|
+
},
|
|
112
|
+
template: '<Checkbox v-bind="args" v-model="checked" />'
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const WithoutLabel: Story = {
|
|
117
|
+
args: {},
|
|
118
|
+
render: (args: any) => ({
|
|
119
|
+
components: { Checkbox },
|
|
120
|
+
setup() {
|
|
121
|
+
const checked = ref(false)
|
|
122
|
+
return { args, checked }
|
|
123
|
+
},
|
|
124
|
+
template: '<Checkbox v-bind="args" v-model="checked" />'
|
|
125
|
+
})
|
|
126
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/vue'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import Checkbox from './Checkbox.vue'
|
|
5
|
+
|
|
6
|
+
describe('Checkbox', () => {
|
|
7
|
+
describe('rendering', () => {
|
|
8
|
+
it('renders with label', () => {
|
|
9
|
+
render(Checkbox, {
|
|
10
|
+
props: {
|
|
11
|
+
label: 'Accept terms'
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(screen.getByText('Accept terms')).toBeInTheDocument()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders without label', () => {
|
|
19
|
+
const { container } = render(Checkbox)
|
|
20
|
+
|
|
21
|
+
expect(container.querySelector('.checkbox-text')).not.toBeInTheDocument()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders error message', () => {
|
|
25
|
+
render(Checkbox, {
|
|
26
|
+
props: {
|
|
27
|
+
error: 'This field is required'
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(screen.getByText('This field is required')).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('applies disabled class when disabled', () => {
|
|
35
|
+
render(Checkbox, {
|
|
36
|
+
props: {
|
|
37
|
+
label: 'Disabled',
|
|
38
|
+
disabled: true
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const label = screen.getByText('Disabled').closest('.checkbox-label')
|
|
43
|
+
expect(label).toHaveClass('checkbox-label--disabled')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('checked state', () => {
|
|
48
|
+
it('is unchecked by default', () => {
|
|
49
|
+
render(Checkbox, {
|
|
50
|
+
props: {
|
|
51
|
+
label: 'Checkbox'
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const checkbox = screen.getByRole('checkbox')
|
|
56
|
+
expect(checkbox).not.toBeChecked()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('renders as checked when modelValue is true', () => {
|
|
60
|
+
render(Checkbox, {
|
|
61
|
+
props: {
|
|
62
|
+
label: 'Checkbox',
|
|
63
|
+
modelValue: true
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const checkbox = screen.getByRole('checkbox')
|
|
68
|
+
expect(checkbox).toBeChecked()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('can be toggled', async () => {
|
|
72
|
+
const user = userEvent.setup()
|
|
73
|
+
const { emitted } = render(Checkbox, {
|
|
74
|
+
props: {
|
|
75
|
+
label: 'Toggle me'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const checkbox = screen.getByRole('checkbox')
|
|
80
|
+
await user.click(checkbox)
|
|
81
|
+
|
|
82
|
+
expect(emitted()['update:modelValue']).toBeTruthy()
|
|
83
|
+
expect(emitted()['update:modelValue'][0]).toEqual([true])
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('disabled state', () => {
|
|
88
|
+
it('disables the checkbox when disabled prop is true', () => {
|
|
89
|
+
render(Checkbox, {
|
|
90
|
+
props: {
|
|
91
|
+
label: 'Disabled',
|
|
92
|
+
disabled: true
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const checkbox = screen.getByRole('checkbox')
|
|
97
|
+
expect(checkbox).toBeDisabled()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not emit events when disabled', async () => {
|
|
101
|
+
const user = userEvent.setup()
|
|
102
|
+
const { emitted } = render(Checkbox, {
|
|
103
|
+
props: {
|
|
104
|
+
label: 'Disabled',
|
|
105
|
+
disabled: true
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const checkbox = screen.getByRole('checkbox')
|
|
110
|
+
await user.click(checkbox)
|
|
111
|
+
|
|
112
|
+
expect(emitted()['update:modelValue']).toBeFalsy()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('required attribute', () => {
|
|
117
|
+
it('sets required attribute when required prop is true', () => {
|
|
118
|
+
render(Checkbox, {
|
|
119
|
+
props: {
|
|
120
|
+
label: 'Required',
|
|
121
|
+
required: true
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const checkbox = screen.getByRole('checkbox')
|
|
126
|
+
expect(checkbox).toBeRequired()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('does not set required attribute by default', () => {
|
|
130
|
+
render(Checkbox, {
|
|
131
|
+
props: {
|
|
132
|
+
label: 'Optional'
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const checkbox = screen.getByRole('checkbox')
|
|
137
|
+
expect(checkbox).not.toBeRequired()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('accessibility', () => {
|
|
142
|
+
it('associates label with checkbox', () => {
|
|
143
|
+
render(Checkbox, {
|
|
144
|
+
props: {
|
|
145
|
+
label: 'Accept terms'
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const checkbox = screen.getByRole('checkbox')
|
|
150
|
+
const label = screen.getByText('Accept terms')
|
|
151
|
+
|
|
152
|
+
expect(checkbox.closest('label')).toContainElement(label)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="checkbox-wrapper">
|
|
3
|
+
<label :class="['checkbox-label', { 'checkbox-label--disabled': disabled }]">
|
|
4
|
+
<input
|
|
5
|
+
type="checkbox"
|
|
6
|
+
class="checkbox-input"
|
|
7
|
+
:checked="modelValue"
|
|
8
|
+
:disabled="disabled"
|
|
9
|
+
:required="required"
|
|
10
|
+
@change="handleChange"
|
|
11
|
+
/>
|
|
12
|
+
<span class="checkbox-box"></span>
|
|
13
|
+
<span v-if="label" class="checkbox-text">{{ label }}</span>
|
|
14
|
+
</label>
|
|
15
|
+
<span v-if="error" class="checkbox-error">{{ error }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
|
|
21
|
+
export interface CheckboxProps {
|
|
22
|
+
modelValue?: boolean
|
|
23
|
+
label?: string
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
required?: boolean
|
|
26
|
+
error?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
withDefaults(defineProps<CheckboxProps>(), {
|
|
30
|
+
modelValue: false,
|
|
31
|
+
disabled: false,
|
|
32
|
+
required: false
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
'update:modelValue': [value: boolean]
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const handleChange = (event: Event) => {
|
|
40
|
+
const target = event.target as HTMLInputElement
|
|
41
|
+
emit('update:modelValue', target.checked)
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
.checkbox-wrapper {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
gap: 0.25rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.checkbox-label {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 0.5rem;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
position: relative;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.checkbox-label--disabled {
|
|
61
|
+
cursor: not-allowed;
|
|
62
|
+
opacity: 0.6;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.checkbox-input {
|
|
66
|
+
position: absolute;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
width: 0;
|
|
69
|
+
height: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.checkbox-box {
|
|
73
|
+
width: 1.25rem;
|
|
74
|
+
height: 1.25rem;
|
|
75
|
+
border: 2px solid #d1d5db;
|
|
76
|
+
border-radius: 0.25rem;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
transition: all 0.2s ease-in-out;
|
|
81
|
+
background: white;
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.checkbox-input:checked + .checkbox-box {
|
|
86
|
+
background: #3b82f6;
|
|
87
|
+
border-color: #3b82f6;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.checkbox-input:checked + .checkbox-box::after {
|
|
91
|
+
content: '';
|
|
92
|
+
width: 0.375rem;
|
|
93
|
+
height: 0.75rem;
|
|
94
|
+
border: solid white;
|
|
95
|
+
border-width: 0 2px 2px 0;
|
|
96
|
+
transform: rotate(45deg);
|
|
97
|
+
margin-bottom: 0.125rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.checkbox-input:focus + .checkbox-box {
|
|
101
|
+
outline: 2px solid #3b82f6;
|
|
102
|
+
outline-offset: 2px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.checkbox-input:disabled + .checkbox-box {
|
|
106
|
+
background: #f3f4f6;
|
|
107
|
+
cursor: not-allowed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.checkbox-text {
|
|
111
|
+
font-size: 0.875rem;
|
|
112
|
+
color: #374151;
|
|
113
|
+
user-select: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.checkbox-error {
|
|
117
|
+
font-size: 0.75rem;
|
|
118
|
+
color: #ef4444;
|
|
119
|
+
margin-left: 1.75rem;
|
|
120
|
+
}
|
|
121
|
+
</style>
|