vueless 1.3.7-beta.2 → 1.3.7-beta.4

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.
Files changed (37) hide show
  1. package/components.d.ts +1 -0
  2. package/components.ts +1 -0
  3. package/constants.d.ts +2 -0
  4. package/constants.js +2 -0
  5. package/icons/storybook/help.svg +1 -0
  6. package/icons/storybook/logout.svg +1 -0
  7. package/package.json +2 -2
  8. package/ui.container-col/UCol.vue +0 -1
  9. package/ui.container-collapsible/UCollapsible.vue +190 -0
  10. package/ui.container-collapsible/config.ts +45 -0
  11. package/ui.container-collapsible/constants.ts +1 -0
  12. package/ui.container-collapsible/storybook/docs.mdx +17 -0
  13. package/ui.container-collapsible/storybook/stories.ts +238 -0
  14. package/ui.container-collapsible/tests/UCollapsible.test.ts +571 -0
  15. package/ui.container-collapsible/types.ts +57 -0
  16. package/ui.dropdown/UDropdown.vue +324 -0
  17. package/ui.dropdown/config.ts +27 -0
  18. package/ui.dropdown/constants.ts +1 -0
  19. package/ui.dropdown/storybook/docs.mdx +17 -0
  20. package/ui.dropdown/storybook/stories.ts +286 -0
  21. package/ui.dropdown/tests/UDropdown.test.ts +631 -0
  22. package/ui.dropdown/types.ts +127 -0
  23. package/ui.dropdown-badge/UDropdownBadge.vue +119 -227
  24. package/ui.dropdown-badge/config.ts +18 -15
  25. package/ui.dropdown-badge/storybook/stories.ts +13 -40
  26. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +201 -67
  27. package/ui.dropdown-button/UDropdownButton.vue +121 -226
  28. package/ui.dropdown-button/config.ts +32 -28
  29. package/ui.dropdown-button/storybook/stories.ts +15 -42
  30. package/ui.dropdown-button/tests/UDropdownButton.test.ts +189 -73
  31. package/ui.dropdown-link/UDropdownLink.vue +123 -233
  32. package/ui.dropdown-link/config.ts +15 -18
  33. package/ui.dropdown-link/storybook/stories.ts +31 -58
  34. package/ui.dropdown-link/tests/UDropdownLink.test.ts +190 -71
  35. package/ui.form-listbox/UListbox.vue +2 -3
  36. package/ui.form-listbox/config.ts +2 -2
  37. package/ui.form-select/config.ts +1 -1
@@ -1,21 +1,17 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, computed, ref, useId, useTemplateRef } from "vue";
3
- import { isEqual } from "lodash-es";
2
+ import { computed, useTemplateRef } from "vue";
4
3
 
5
4
  import { useUI } from "../composables/useUI";
6
5
  import { getDefaults } from "../utils/ui";
7
6
 
8
7
  import UIcon from "../ui.image-icon/UIcon.vue";
9
8
  import ULink from "../ui.button-link/ULink.vue";
10
- import ULisbox from "../ui.form-listbox/UListbox.vue";
11
-
12
- import vClickOutside from "../v.click-outside/vClickOutside";
9
+ import UDropdown from "../ui.dropdown/UDropdown.vue";
13
10
 
14
11
  import { COMPONENT_NAME } from "./constants";
15
12
  import defaultConfig from "./config";
16
13
 
17
14
  import type { Props, Config } from "./types";
18
- import type { Option, SelectedValue } from "../ui.form-listbox/types";
19
15
 
20
16
  defineOptions({ inheritAttrs: false });
21
17
 
@@ -63,66 +59,9 @@ const emit = defineEmits([
63
59
  "update:search",
64
60
  ]);
65
61
 
66
- type ULisboxRef = InstanceType<typeof ULisbox>;
67
-
68
- const isShownOptions = ref(false);
69
- const isClickingOption = ref(false);
70
- const listboxRef = useTemplateRef<ULisboxRef>("dropdown-list");
71
- const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
72
-
73
- const elementId = props.id || useId();
74
-
75
- const dropdownValue = computed({
76
- get: () => {
77
- if (props.multiple && !Array.isArray(props.modelValue)) {
78
- return props.modelValue ? [props.modelValue] : [];
79
- }
80
-
81
- return props.modelValue;
82
- },
83
- set: (value) => emit("update:modelValue", value),
84
- });
85
-
86
- const dropdownSearch = computed({
87
- get: () => props.search ?? "",
88
- set: (value: string) => emit("update:search", value),
89
- });
90
-
91
- const selectedOptions = computed(() => {
92
- if (props.multiple) {
93
- return props.options.filter((option) => {
94
- return (
95
- option[props.valueKey] &&
96
- (dropdownValue.value as SelectedValue[]).find((selected) =>
97
- isEqual(selected, option[props.valueKey]),
98
- )
99
- );
100
- });
101
- }
102
-
103
- return [
104
- props.options.find(
105
- (option) => option[props.valueKey] && isEqual(option[props.valueKey], dropdownValue.value),
106
- ),
107
- ].filter((option) => !!option);
108
- });
109
-
110
- const linkLabel = computed(() => {
111
- if (!props.labelDisplayCount || !selectedOptions.value.length) {
112
- return props.label;
113
- }
114
-
115
- const selectedLabels = selectedOptions.value
116
- .slice(0, props.labelDisplayCount)
117
- .map((option) => option[props.labelKey]);
118
- const restLabelCount = selectedOptions.value.length - props.labelDisplayCount;
62
+ type UDropdownRef = InstanceType<typeof UDropdown>;
119
63
 
120
- if (restLabelCount > 0) {
121
- selectedLabels.push(`+${restLabelCount}`);
122
- }
123
-
124
- return selectedLabels.join(", ");
125
- });
64
+ const dropdownRef = useTemplateRef<UDropdownRef>("dropdown");
126
65
 
127
66
  const toggleIconName = computed(() => {
128
67
  if (typeof props.toggleIcon === "string") {
@@ -132,197 +71,148 @@ const toggleIconName = computed(() => {
132
71
  return props.toggleIcon ? config.value.defaults.toggleIcon : "";
133
72
  });
134
73
 
135
- function getFullOptionLabels(value: Option | Option[]) {
136
- const labelKey = props.labelKey;
137
-
138
- if (Array.isArray(value)) {
139
- return value.map((item) => item[labelKey]).join(", ");
140
- }
141
-
142
- return "";
143
- }
144
-
145
- function onSearchChange(query: string) {
146
- emit("searchChange", query);
147
- }
148
-
149
- function onClickLink() {
150
- if (props.disabled) return;
151
-
152
- isShownOptions.value = !isShownOptions.value;
153
-
154
- if (isShownOptions.value) {
155
- nextTick(() => listboxRef.value?.wrapperRef?.focus());
156
-
157
- emit("open");
158
- }
159
- }
160
-
161
- function hideOptions() {
162
- isShownOptions.value = false;
163
- dropdownSearch.value = "";
164
-
165
- emit("close");
166
- }
167
-
168
- function onClickOption(option: Option) {
169
- isClickingOption.value = true;
170
-
171
- emit("clickOption", option);
172
-
173
- if (!props.multiple && props.closeOnSelect) hideOptions();
174
-
175
- nextTick(() => {
176
- setTimeout(() => {
177
- isClickingOption.value = false;
178
- }, 10);
179
- });
180
- }
181
-
182
- function handleClickOutside() {
183
- if (isClickingOption.value) return;
184
-
185
- hideOptions();
74
+ function hide() {
75
+ dropdownRef.value?.hide();
186
76
  }
187
77
 
188
78
  defineExpose({
189
79
  /**
190
- * A reference to the component's wrapper element for direct DOM manipulation.
80
+ * A reference to the dropdown wrapper element for direct DOM manipulation.
191
81
  * @property {HTMLDivElement}
192
82
  */
193
- wrapperRef,
83
+ wrapperRef: computed(() => dropdownRef.value?.wrapperRef),
194
84
 
195
85
  /**
196
- * Hides the dropdown options.
86
+ * Hides the dropdown.
197
87
  * @property {function}
198
88
  */
199
- hideOptions,
89
+ hide,
200
90
  });
201
91
 
202
- /**
203
- * Get element / nested component attributes for each config token ✨
92
+ /*
93
+ * Vueless: Get element / nested component attributes for each config token ✨
204
94
  * Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
205
95
  */
206
96
  const mutatedProps = computed(() => ({
207
97
  /* component state, not a props */
208
- opened: isShownOptions.value,
98
+ opened: dropdownRef.value?.isOpened ?? false,
209
99
  }));
210
100
 
211
- const { config, getDataTest, wrapperAttrs, dropdownLinkAttrs, listboxAttrs, toggleIconAttrs } =
212
- useUI<Config>(defaultConfig, mutatedProps);
101
+ const { config, getDataTest, dropdownLinkAttrs, toggleLinkAttrs, toggleIconAttrs } = useUI<Config>(
102
+ defaultConfig,
103
+ mutatedProps,
104
+ "toggleLink",
105
+ );
213
106
  </script>
214
107
 
215
108
  <template>
216
- <div
217
- ref="wrapper"
218
- v-click-outside="handleClickOutside"
219
- tabindex="1"
220
- v-bind="wrapperAttrs"
221
- :data-test="getDataTest('wrapper')"
222
- @keydown.enter="onClickLink"
223
- @keydown.space.prevent="onClickLink"
109
+ <UDropdown
110
+ :id="id"
111
+ ref="dropdown"
112
+ :model-value="modelValue"
113
+ :label="label"
114
+ :label-display-count="labelDisplayCount"
115
+ :search="search"
116
+ :y-position="yPosition"
117
+ :x-position="xPosition"
118
+ :disabled="disabled"
119
+ :options="options"
120
+ :options-limit="optionsLimit"
121
+ :visible-options="visibleOptions"
122
+ :label-key="labelKey"
123
+ :value-key="valueKey"
124
+ :group-label-key="groupLabelKey"
125
+ :group-value-key="groupValueKey"
126
+ :searchable="searchable"
127
+ :multiple="multiple"
128
+ :color="color"
129
+ :size="size"
130
+ :close-on-select="closeOnSelect"
131
+ v-bind="dropdownLinkAttrs"
132
+ :data-test="dataTest"
133
+ @click-option="(option) => emit('clickOption', option)"
134
+ @update:model-value="(value) => emit('update:modelValue', value)"
135
+ @update:search="(value) => emit('update:search', value)"
136
+ @search-change="(query) => emit('searchChange', query)"
137
+ @open="emit('open')"
138
+ @close="emit('close')"
224
139
  >
225
- <!--
226
- @slot Use it to add something before the label.
227
- @binding {boolean} opened
228
- -->
229
- <slot name="left" :opened="isShownOptions" />
230
-
231
- <ULink
232
- :id="elementId"
233
- tabindex="-1"
234
- :size="size"
235
- :label="linkLabel"
236
- :color="color"
237
- :dashed="dashed"
238
- :disabled="disabled"
239
- :underlined="underlined"
240
- :title="getFullOptionLabels(selectedOptions)"
241
- v-bind="dropdownLinkAttrs"
242
- :data-test="getDataTest()"
243
- @click="onClickLink"
244
- >
245
- <template #default>
246
- <!--
247
- @slot Use it to add something instead of the default label.
248
- @binding {string} label
249
- @binding {boolean} opened
250
- -->
251
- <slot :label="linkLabel" :opened="isShownOptions" />
252
- </template>
253
- </ULink>
254
-
255
- <!--
256
- @slot Use it to add something instead of the toggle icon.
257
- @binding {boolean} opened
258
- @binding {function} toggle
259
- -->
260
- <slot name="toggle" :opened="isShownOptions" :toggle="onClickLink">
261
- <UIcon
262
- v-if="toggleIconName"
263
- interactive
140
+ <template #default="{ opened, displayLabel, fullLabel }">
141
+ <!--
142
+ @slot Use it to add something before the label.
143
+ @binding {boolean} opened
144
+ -->
145
+ <slot name="left" :opened="opened" />
146
+
147
+ <ULink
148
+ :size="size"
149
+ :label="displayLabel"
264
150
  :color="color"
151
+ :dashed="dashed"
265
152
  :disabled="disabled"
266
- :name="toggleIconName"
267
- v-bind="toggleIconAttrs"
268
- :data-test="getDataTest('toggle')"
269
- @click="onClickLink"
270
- />
271
- </slot>
272
-
273
- <ULisbox
274
- v-if="isShownOptions"
275
- ref="dropdown-list"
276
- v-model="dropdownValue"
277
- v-model:search="dropdownSearch"
278
- :searchable="searchable"
279
- :multiple="multiple"
280
- :size="size"
281
- :color="color"
282
- :options="options"
283
- :options-limit="optionsLimit"
284
- :visible-options="visibleOptions"
285
- :label-key="labelKey"
286
- :value-key="valueKey"
287
- :group-label-key="groupLabelKey"
288
- :group-value-key="groupValueKey"
289
- v-bind="listboxAttrs"
290
- :data-test="getDataTest('list')"
291
- @click-option="onClickOption"
292
- @search-change="onSearchChange"
293
- @update:search="(value) => emit('update:search', value)"
294
- >
295
- <template #before-option="{ option, index }">
296
- <!--
297
- @slot Use it to add something before option.
298
- @binding {object} option
299
- @binding {number} index
300
- -->
301
- <slot name="before-option" :option="option" :index="index" />
302
- </template>
303
-
304
- <template #option="{ option, index }">
305
- <!--
306
- @slot Use it to customize the option.
307
- @binding {object} option
308
- @binding {number} index
153
+ :underlined="underlined"
154
+ :title="fullLabel"
155
+ v-bind="toggleLinkAttrs"
156
+ tabindex="-1"
157
+ :data-test="getDataTest()"
158
+ >
159
+ <template #default>
160
+ <!--
161
+ @slot Use it to add something instead of the default label.
162
+ @binding {string} label
163
+ @binding {boolean} opened
309
164
  -->
310
- <slot name="option" :option="option" :index="index" />
311
- </template>
312
-
313
- <template #after-option="{ option, index }">
314
- <!--
315
- @slot Use it to add something after option.
316
- @binding {object} option
317
- @binding {number} index
318
- -->
319
- <slot name="after-option" :option="option" :index="index" />
320
- </template>
321
-
322
- <template #empty>
323
- <!-- @slot Use it to add something instead of empty state. -->
324
- <slot name="empty" />
325
- </template>
326
- </ULisbox>
327
- </div>
165
+ <slot :label="displayLabel" :opened="opened" />
166
+ </template>
167
+ </ULink>
168
+
169
+ <!--
170
+ @slot Use it to add something instead of the toggle icon.
171
+ @binding {boolean} opened
172
+ -->
173
+ <slot name="toggle" :opened="opened">
174
+ <UIcon
175
+ v-if="toggleIconName"
176
+ interactive
177
+ :color="color"
178
+ :disabled="disabled"
179
+ :name="toggleIconName"
180
+ v-bind="toggleIconAttrs"
181
+ :data-test="getDataTest('toggle')"
182
+ />
183
+ </slot>
184
+ </template>
185
+
186
+ <template #before-option="{ option, index }">
187
+ <!--
188
+ @slot Use it to add something before option.
189
+ @binding {object} option
190
+ @binding {number} index
191
+ -->
192
+ <slot name="before-option" :option="option" :index="index" />
193
+ </template>
194
+
195
+ <template #option="{ option, index }">
196
+ <!--
197
+ @slot Use it to customize the option.
198
+ @binding {object} option
199
+ @binding {number} index
200
+ -->
201
+ <slot name="option" :option="option" :index="index" />
202
+ </template>
203
+
204
+ <template #after-option="{ option, index }">
205
+ <!--
206
+ @slot Use it to add something after option.
207
+ @binding {object} option
208
+ @binding {number} index
209
+ -->
210
+ <slot name="after-option" :option="option" :index="index" />
211
+ </template>
212
+
213
+ <template #empty>
214
+ <!-- @slot Use it to add something instead of empty state. -->
215
+ <slot name="empty" />
216
+ </template>
217
+ </UDropdown>
328
218
  </template>
@@ -1,9 +1,19 @@
1
1
  export default /*tw*/ {
2
- wrapper: `
3
- inline-flex gap-0.5 relative items-center justify-between rounded h-max
4
- focus-visible:outline focus-visible:outline-medium focus-visible:outline-offset-4 focus-visible:outline-{color}
5
- `,
6
- dropdownLink: "{ULink} focus-visible:outline-hidden",
2
+ dropdownLink: {
3
+ base: `
4
+ {UDropdown}
5
+ inline-flex gap-0.5 items-center justify-between rounded-small h-max
6
+ focus-visible:outline-offset-4
7
+ `,
8
+ defaults: {
9
+ size: {
10
+ sm: "sm",
11
+ md: "md",
12
+ lg: "lg",
13
+ },
14
+ },
15
+ },
16
+ toggleLink: "{ULink} focus-visible:outline-hidden",
7
17
  toggleIcon: {
8
18
  base: "{UIcon} block transition duration-300",
9
19
  defaults: {
@@ -15,19 +25,6 @@ export default /*tw*/ {
15
25
  },
16
26
  compoundVariants: [{ opened: true, class: "rotate-180" }],
17
27
  },
18
- listbox: {
19
- base: "{UListbox} w-fit",
20
- variants: {
21
- yPosition: {
22
- top: "bottom-full mb-2",
23
- bottom: "top-full mt-2",
24
- },
25
- xPosition: {
26
- left: "left-0",
27
- right: "right-0",
28
- },
29
- },
30
- },
31
28
  defaults: {
32
29
  color: "primary",
33
30
  size: "md",
@@ -98,29 +98,29 @@ const EnumTemplate: StoryFn<EnumUDropdownLinkArgs> = (
98
98
  const GroupValuesTemplate: StoryFn<DefaultUDropdownLinkArgs> = (
99
99
  args: DefaultUDropdownLinkArgs,
100
100
  ) => ({
101
- components: { UDropdownLink },
101
+ components: { UDropdownLink, URow },
102
102
  setup() {
103
103
  return {
104
104
  args,
105
105
  };
106
106
  },
107
107
  template: `
108
- <UDropdownLink
109
- v-bind="args"
110
- v-model="args.modelValue"
111
- label="Single"
112
- :config="{ listbox: 'min-w-[200px]' }"
113
- class="max-w-96 mr-20"
114
- />
115
-
116
- <UDropdownLink
117
- v-bind="args"
118
- v-model="args.modelValueMultiple"
119
- label="Multiple"
120
- multiple
121
- :config="{ listbox: 'min-w-[200px]' }"
122
- class="mt-5 max-w-96"
123
- />
108
+ <URow gap="2xl">
109
+ <UDropdownLink
110
+ v-bind="args"
111
+ v-model="args.modelValue"
112
+ label="Single"
113
+ :config="{ listbox: 'min-w-[200px]' }"
114
+ />
115
+
116
+ <UDropdownLink
117
+ v-bind="args"
118
+ v-model="args.modelValueMultiple"
119
+ label="Multiple"
120
+ multiple
121
+ :config="{ listbox: 'min-w-[200px]' }"
122
+ />
123
+ </URow>
124
124
  `,
125
125
  });
126
126
 
@@ -136,6 +136,13 @@ Default.parameters = {
136
136
 
137
137
  export const Disabled = DefaultTemplate.bind({});
138
138
  Disabled.args = { disabled: true };
139
+ Disabled.parameters = {
140
+ docs: {
141
+ story: {
142
+ height: "120px",
143
+ },
144
+ },
145
+ };
139
146
 
140
147
  export const Searchable = DefaultTemplate.bind({});
141
148
  Searchable.args = { searchable: true };
@@ -176,7 +183,7 @@ export const ListboxXPosition = EnumTemplate.bind({});
176
183
  ListboxXPosition.args = {
177
184
  enum: "xPosition",
178
185
  label: "{enumValue}",
179
- class: "w-40 py-1 px-2.5 border border-dashed border-primary",
186
+ config: { dropdownLink: "w-40 py-1 px-2.5 border border-dashed border-primary" },
180
187
  };
181
188
 
182
189
  export const ListboxYPosition = EnumTemplate.bind({});
@@ -364,7 +371,7 @@ export const OptionSlots: StoryFn<DefaultUDropdownLinkArgs> = (args) => ({
364
371
  ]"
365
372
  >
366
373
  <template #before-option="{ option }">
367
- <UAvatar :src="option.avatar" size="sm" />
374
+ <UAvatar :src="option.avatar" size="xs" />
368
375
  </template>
369
376
  </UDropdownLink>
370
377
 
@@ -407,7 +414,7 @@ export const OptionSlots: StoryFn<DefaultUDropdownLinkArgs> = (args) => ({
407
414
  ]"
408
415
  >
409
416
  <template #option="{ option }">
410
- <URow align="center" gap="xs">
417
+ <URow justify="between" align="center" gap="xs" block>
411
418
  <UCol gap="none">
412
419
  <UText size="sm">{{ option.label }}</UText>
413
420
  <UText variant="lifted" size="xs">{{ option.role }}</UText>
@@ -426,47 +433,13 @@ export const OptionSlots: StoryFn<DefaultUDropdownLinkArgs> = (args) => ({
426
433
  v-model="args.afterOptionModel"
427
434
  label="After option slot"
428
435
  :options="[
429
- {
430
- label: 'John Doe',
431
- value: '1',
432
- role: 'Developer',
433
- avatar: johnDoe,
434
- status: 'online',
435
- statusColor: 'success',
436
- },
437
- {
438
- label: 'Jane Smith',
439
- value: '2',
440
- role: 'Designer',
441
- avatar: emilyDavis,
442
- status: 'away',
443
- statusColor: 'warning',
444
- },
445
- {
446
- label: 'Mike Johnson',
447
- value: '3',
448
- role: 'Product Manager',
449
- avatar: alexJohnson,
450
- status: 'offline',
451
- statusColor: 'grayscale',
452
- },
453
- {
454
- label: 'Sarah Wilson',
455
- value: '4',
456
- role: 'QA Engineer',
457
- avatar: patMorgan,
458
- status: 'online',
459
- statusColor: 'success',
460
- },
436
+ { label: 'John Doe', value: '1', verified: true },
437
+ { label: 'Jane Smith', value: '2', verified: true },
438
+ { label: 'Mike Johnson', value: '3', verified: false },
461
439
  ]"
462
440
  >
463
441
  <template #after-option="{ option }">
464
- <UBadge
465
- :label="option.status"
466
- :color="option.statusColor"
467
- size="sm"
468
- variant="subtle"
469
- />
442
+ <UIcon v-if="option.verified" name="verified" size="xs" color="success" />
470
443
  </template>
471
444
  </UDropdownLink>
472
445
  </URow>