picocrank 1.26.0 → 1.27.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.
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Section
3
3
  title="Horizontal Tabs"
4
- subtitle="A flexible tabbed control component with support for multiple tab panes"
4
+ subtitle="A flexible tabbed control component with support for multiple tab panes and disabled tabs"
5
5
  :padding="horizontalTabsSectionPadding"
6
6
  >
7
7
  <template #toolbar>
@@ -13,6 +13,10 @@
13
13
  <input v-model="horizontalTabsPadding" type="checkbox" />
14
14
  Tab padding
15
15
  </label>
16
+ <label class="tabs-padding-toggle">
17
+ <input v-model="detailsTabEnabled" type="checkbox" />
18
+ Enable Details tab
19
+ </label>
16
20
  </template>
17
21
 
18
22
  <Tabs
@@ -26,6 +30,8 @@
26
30
  <p>
27
31
  This is the Overview tab. It contains general information about the current topic.
28
32
  You can place any content here, including forms, tables, images, or other components.
33
+ The <strong>Details</strong> tab starts disabled — use the toolbar checkbox to
34
+ enable it when elevated permissions are granted.
29
35
  </p>
30
36
  <ul>
31
37
  <li>First item in the overview</li>
@@ -97,7 +103,7 @@
97
103
 
98
104
  <Section
99
105
  title="Vertical Tabs"
100
- subtitle="Side tab strip with arrow-key navigation up and down"
106
+ subtitle="Side tab strip with arrow-key navigation up and down; disabled tabs are skipped"
101
107
  :padding="verticalTabsSectionPadding"
102
108
  >
103
109
  <template #toolbar>
@@ -109,6 +115,10 @@
109
115
  <input v-model="verticalTabsPadding" type="checkbox" />
110
116
  Tab padding
111
117
  </label>
118
+ <label class="tabs-padding-toggle">
119
+ <input v-model="advancedTabEnabled" type="checkbox" />
120
+ Enable Advanced tab
121
+ </label>
112
122
  </template>
113
123
 
114
124
  <Tabs
@@ -123,6 +133,7 @@
123
133
  <p>
124
134
  Vertical tabs place the tab list beside the panel. Use them when labels are
125
135
  longer or when the layout mirrors a settings sidebar.
136
+ <strong>Advanced</strong> starts disabled — use the toolbar checkbox to enable it.
126
137
  </p>
127
138
  </div>
128
139
  </template>
@@ -186,24 +197,47 @@
186
197
  </template>
187
198
 
188
199
  <script setup>
189
- import { ref, reactive } from 'vue';
200
+ import { computed, ref, reactive } from 'vue';
190
201
  import { HomeIcon, Settings01Icon, UserIcon, ViewIcon } from '@hugeicons/core-free-icons';
191
202
  import Section from '../components/Section.vue';
192
203
  import Tabs from '../components/Tabs.vue';
193
204
  import FormLayout from '../components/FormLayout.vue';
194
205
  import FormField from '../components/FormField.vue';
195
206
 
196
- const basicTabs = ref([
207
+ const detailsTabEnabled = ref(false);
208
+ const advancedTabEnabled = ref(false);
209
+
210
+ const basicTabs = computed(() => [
197
211
  { id: 0, label: 'Overview', icon: HomeIcon },
198
- { id: 1, label: 'Details', icon: ViewIcon },
212
+ {
213
+ id: 1,
214
+ label: 'Details',
215
+ icon: ViewIcon,
216
+ ...(detailsTabEnabled.value
217
+ ? {}
218
+ : {
219
+ disabled: true,
220
+ disabledTitle: 'Details requires elevated permissions',
221
+ }),
222
+ },
199
223
  { id: 2, label: 'Settings', icon: Settings01Icon },
200
224
  ]);
201
225
 
202
- const verticalTabs = ref([
226
+ const verticalTabs = computed(() => [
203
227
  { id: 'general', label: 'General', icon: HomeIcon },
204
228
  { id: 'appearance', label: 'Appearance', icon: Settings01Icon },
205
229
  { id: 'notifications', label: 'Notifications', icon: UserIcon },
206
- { id: 'advanced', label: 'Advanced', icon: Settings01Icon },
230
+ {
231
+ id: 'advanced',
232
+ label: 'Advanced',
233
+ icon: Settings01Icon,
234
+ ...(advancedTabEnabled.value
235
+ ? {}
236
+ : {
237
+ disabled: true,
238
+ disabledTitle: 'Advanced settings are not available for this account',
239
+ }),
240
+ },
207
241
  ]);
208
242
 
209
243
  const horizontalTabsSectionPadding = ref(true);
@@ -0,0 +1,279 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { computed, defineComponent, ref } from 'vue'
3
+ import FormField from '../components/FormField.vue'
4
+ import FormLayout from '../components/FormLayout.vue'
5
+ import NotificationBlock from '../components/NotificationBlock.vue'
6
+
7
+ export const routerLinkStub = {
8
+ name: 'RouterLink',
9
+ template: '<a><slot /></a>',
10
+ }
11
+
12
+ const defaultMountOptions = {
13
+ attachTo: document.body,
14
+ }
15
+
16
+ export function mountFormField(options = {}) {
17
+ const {
18
+ props = {},
19
+ slot = '<input id="test-input" />',
20
+ global = {},
21
+ attachTo = document.body,
22
+ ...rest
23
+ } = options
24
+
25
+ return mount(FormField, {
26
+ ...defaultMountOptions,
27
+ attachTo,
28
+ props: {
29
+ label: 'Test Field',
30
+ for: 'test-input',
31
+ ...props,
32
+ },
33
+ slots: {
34
+ default: slot,
35
+ },
36
+ global: {
37
+ stubs: {
38
+ RouterLink: routerLinkStub,
39
+ ...global.stubs,
40
+ },
41
+ ...global,
42
+ },
43
+ ...rest,
44
+ })
45
+ }
46
+
47
+ export function mountFormFieldWithError(initialError = '') {
48
+ const error = ref(initialError)
49
+
50
+ const wrapper = mount(
51
+ defineComponent({
52
+ components: { FormField },
53
+ setup() {
54
+ return { error }
55
+ },
56
+ template: `
57
+ <FormField label="Email" for="email" v-model:error="error">
58
+ <input id="email" />
59
+ </FormField>
60
+ `,
61
+ }),
62
+ {
63
+ attachTo: document.body,
64
+ global: {
65
+ stubs: {
66
+ RouterLink: routerLinkStub,
67
+ },
68
+ },
69
+ },
70
+ )
71
+
72
+ return { wrapper, error }
73
+ }
74
+
75
+ export async function flushControlState() {
76
+ const { nextTick } = await import('vue')
77
+ await nextTick()
78
+ await nextTick()
79
+ }
80
+
81
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
82
+ export const MIN_COMMENTS_LENGTH = 50
83
+
84
+ export function mountValidationForm() {
85
+ const formData = ref({
86
+ email: '',
87
+ comments: '',
88
+ accessibility: [],
89
+ })
90
+
91
+ const emailError = computed(() => {
92
+ const email = formData.value.email.trim()
93
+ if (!email) {
94
+ return 'Email is required.'
95
+ }
96
+ if (!EMAIL_PATTERN.test(email)) {
97
+ return 'Enter a valid email address.'
98
+ }
99
+ return ''
100
+ })
101
+
102
+ const commentsError = computed(() => {
103
+ const comments = formData.value.comments.trim()
104
+ if (comments.length > 0 && comments.length < MIN_COMMENTS_LENGTH) {
105
+ return `Comments must be at least ${MIN_COMMENTS_LENGTH} characters.`
106
+ }
107
+ return ''
108
+ })
109
+
110
+ function validateAccessibilityRequirements() {
111
+ const selected = formData.value.accessibility
112
+ if (selected.includes('high-contrast') && selected.includes('low-contrast')) {
113
+ return 'High contrast and low contrast UI cannot both be selected.'
114
+ }
115
+ return ''
116
+ }
117
+
118
+ const submitted = ref(false)
119
+
120
+ const wrapper = mount(
121
+ defineComponent({
122
+ components: { FormField, FormLayout },
123
+ setup() {
124
+ function handleSubmit(event) {
125
+ if (!event.target.reportValidity()) {
126
+ return
127
+ }
128
+ if (validateAccessibilityRequirements()) {
129
+ return
130
+ }
131
+ submitted.value = true
132
+ }
133
+
134
+ return {
135
+ formData,
136
+ emailError,
137
+ commentsError,
138
+ validateAccessibilityRequirements,
139
+ submitted,
140
+ handleSubmit,
141
+ }
142
+ },
143
+ template: `
144
+ <FormLayout @submit.prevent="handleSubmit">
145
+ <FormField
146
+ label="Email"
147
+ for="email"
148
+ :error="emailError"
149
+ >
150
+ <input
151
+ id="email"
152
+ v-model="formData.email"
153
+ type="email"
154
+ required
155
+ />
156
+ </FormField>
157
+
158
+ <FormField
159
+ label="Comments"
160
+ for="comments"
161
+ :error="commentsError"
162
+ >
163
+ <textarea id="comments" v-model="formData.comments" />
164
+ </FormField>
165
+
166
+ <FormField
167
+ label="Accessibility requirements"
168
+ component-has-label
169
+ :validate="validateAccessibilityRequirements"
170
+ :validate-deps="[formData.accessibility]"
171
+ >
172
+ <div role="group" aria-label="Accessibility requirements">
173
+ <label>
174
+ <input
175
+ type="checkbox"
176
+ value="high-contrast"
177
+ v-model="formData.accessibility"
178
+ />
179
+ High contrast
180
+ </label>
181
+ <label>
182
+ <input
183
+ type="checkbox"
184
+ value="low-contrast"
185
+ v-model="formData.accessibility"
186
+ />
187
+ Low contrast
188
+ </label>
189
+ </div>
190
+ </FormField>
191
+
192
+ <template #actions>
193
+ <button type="submit">Submit</button>
194
+ </template>
195
+ </FormLayout>
196
+ `,
197
+ }),
198
+ {
199
+ global: {
200
+ stubs: {
201
+ RouterLink: routerLinkStub,
202
+ },
203
+ },
204
+ attachTo: document.body,
205
+ },
206
+ )
207
+
208
+ return { wrapper, formData, submitted }
209
+ }
210
+
211
+ export function mountLoginValidationForm() {
212
+ const usernameError = ref('')
213
+ const passwordError = ref('')
214
+ const formError = ref('')
215
+
216
+ const wrapper = mount(
217
+ defineComponent({
218
+ components: { FormField, NotificationBlock },
219
+ setup() {
220
+ const credentials = ref({ username: '', password: '' })
221
+
222
+ function applyServerErrors(errors) {
223
+ usernameError.value = errors.username || ''
224
+ passwordError.value = errors.password || ''
225
+ formError.value = errors.form || ''
226
+ }
227
+
228
+ function handleSubmit() {
229
+ usernameError.value = ''
230
+ passwordError.value = ''
231
+ formError.value = ''
232
+ }
233
+
234
+ return {
235
+ credentials,
236
+ usernameError,
237
+ passwordError,
238
+ formError,
239
+ applyServerErrors,
240
+ handleSubmit,
241
+ }
242
+ },
243
+ template: `
244
+ <form @submit.prevent="handleSubmit">
245
+ <FormField label="Username" for="username" label-skip v-model:error="usernameError">
246
+ <input id="username" v-model="credentials.username" required aria-label="Username" />
247
+ </FormField>
248
+ <FormField label="Password" for="password" label-skip v-model:error="passwordError">
249
+ <input id="password" v-model="credentials.password" type="password" required aria-label="Password" />
250
+ </FormField>
251
+ <button type="submit">Login</button>
252
+ <NotificationBlock
253
+ v-if="formError"
254
+ class="login-form-error"
255
+ type="bad"
256
+ label="Error"
257
+ :message="formError"
258
+ role="alert"
259
+ />
260
+ </form>
261
+ `,
262
+ }),
263
+ {
264
+ global: {
265
+ stubs: {
266
+ RouterLink: routerLinkStub,
267
+ },
268
+ },
269
+ attachTo: document.body,
270
+ },
271
+ )
272
+
273
+ return {
274
+ wrapper,
275
+ usernameError,
276
+ passwordError,
277
+ formError,
278
+ }
279
+ }
@@ -0,0 +1,5 @@
1
+ import { afterEach } from 'vitest'
2
+
3
+ afterEach(() => {
4
+ document.body.innerHTML = ''
5
+ })