@milaboratories/uikit 2.1.1 → 2.1.3

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 (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/pl-uikit.js +3279 -2982
  3. package/dist/pl-uikit.umd.cjs +10 -10
  4. package/dist/src/components/PlDropdown/PlDropdown.vue.d.ts +5 -1
  5. package/dist/src/components/PlDropdownLegacy/PlDropdownLegacy.vue.d.ts +78 -0
  6. package/dist/src/components/PlDropdownLegacy/__tests__/PlDropdownLegacy.spec.d.ts +1 -0
  7. package/dist/src/components/PlDropdownLegacy/index.d.ts +1 -0
  8. package/dist/src/components/PlDropdownRef/PlDropdownRef.vue.d.ts +12 -2
  9. package/dist/src/composition/useEventListener.d.ts +1 -1
  10. package/dist/src/index.d.ts +1 -0
  11. package/dist/style.css +1 -1
  12. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  13. package/package.json +3 -3
  14. package/src/assets/base.scss +4 -0
  15. package/src/assets/variables.scss +3 -1
  16. package/src/components/PlCheckbox/pl-checkbox.scss +2 -0
  17. package/src/components/PlChip/PlChip.vue +5 -5
  18. package/src/components/PlChip/pl-chip.scss +2 -2
  19. package/src/components/PlDropdown/PlDropdown.vue +86 -27
  20. package/src/components/PlDropdown/__tests__/PlDropdown.spec.ts +7 -6
  21. package/src/components/PlDropdown/pl-dropdown.scss +76 -68
  22. package/src/components/PlDropdownLegacy/PlDropdownLegacy.vue +370 -0
  23. package/src/components/PlDropdownLegacy/__tests__/PlDropdownLegacy.spec.ts +33 -0
  24. package/src/components/PlDropdownLegacy/index.ts +1 -0
  25. package/src/components/PlDropdownLegacy/pl-dropdown-legacy.scss +260 -0
  26. package/src/components/PlDropdownLine/PlDropdownLine.vue +81 -42
  27. package/src/components/PlDropdownLine/pl-dropdown-line.scss +5 -5
  28. package/src/components/PlDropdownMulti/PlDropdownMulti.vue +62 -27
  29. package/src/components/PlDropdownMulti/__tests__/PlDropdownMulti.spec.ts +12 -7
  30. package/src/components/PlDropdownMulti/pl-dropdown-multi.scss +11 -8
  31. package/src/components/PlDropdownRef/PlDropdownRef.vue +16 -3
  32. package/src/components/PlDropdownRef/__tests__/PlDropdownRef.spec.ts +11 -8
  33. package/src/composition/useEventListener.ts +3 -3
  34. package/src/composition/usePosition.ts +2 -2
  35. package/src/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "type": "module",
5
5
  "main": "dist/pl-uikit.umd.js",
6
6
  "module": "dist/pl-uikit.js",
@@ -32,8 +32,8 @@
32
32
  "vue-tsc": "^2.1.6",
33
33
  "yarpm": "^1.2.0",
34
34
  "svgo": "^3.3.2",
35
- "@milaboratories/helpers": "^1.6.6",
36
- "@platforma-sdk/model": "^1.8.0"
35
+ "@platforma-sdk/model": "^1.8.0",
36
+ "@milaboratories/helpers": "^1.6.6"
37
37
  },
38
38
  "scripts": {
39
39
  "dev": "vite",
@@ -70,6 +70,8 @@
70
70
  --title-bar-height: 0px; // @TODO
71
71
 
72
72
  --divider-color: var(--txt-01); // @TODO
73
+
74
+ --pl-dropdown-options-bg: #fff;
73
75
  }
74
76
 
75
77
  [data-theme='dark'] {
@@ -119,6 +121,8 @@
119
121
  --chip-close-ic-hover-color: var(--border-color-focus);
120
122
 
121
123
  --theme-switcher-icon-color: var(--txt-01);
124
+
125
+ --pl-dropdown-options-bg: #1b1b1f;
122
126
  }
123
127
 
124
128
  *,
@@ -66,10 +66,12 @@
66
66
  --main-spacing: 24px;
67
67
  --gap-v: 24px;
68
68
  --gap-h: 12px;
69
+ // @TODO global overlays
69
70
  --z-slide-shadow: 80;
70
71
  --z-slide-dialog: 81;
71
72
  --z-dialog: 100;
72
- --z-tooltip: 103;
73
+ --z-dropdown-options: 110;
74
+ --z-tooltip: 120;
73
75
  --z-context-menu: 1001;
74
76
  }
75
77
 
@@ -54,5 +54,7 @@
54
54
  cursor: var(--cursor-label);
55
55
  font-size: 14px;
56
56
  font-weight: 500;
57
+ display: inline-flex;
58
+ align-items: center;
57
59
  }
58
60
  }
@@ -21,16 +21,16 @@ onMounted(() => {
21
21
  </script>
22
22
 
23
23
  <template>
24
- <PlTooltip position="top" class="ui-chip-tooltip" :delay="500">
24
+ <PlTooltip position="top" class="pl-chip-tooltip" :delay="500">
25
25
  <template v-if="canShowTooltip" #tooltip>
26
26
  <slot />
27
27
  </template>
28
- <div ref="chip" class="ui-chip" :class="{ small }">
29
- <div class="ui-chip__text">
28
+ <div ref="chip" class="pl-chip" :class="{ small }">
29
+ <div class="pl-chip__text">
30
30
  <slot />
31
31
  </div>
32
- <div v-if="closeable" tabindex="0" class="ui-chip__close" @keydown.enter="$emit('close')" @click.stop="$emit('close')">
33
- <div class="ui-chip__close--icon" />
32
+ <div v-if="closeable" tabindex="0" class="pl-chip__close" @keydown.enter="$emit('close')" @click.stop="$emit('close')">
33
+ <div class="pl-chip__close--icon" />
34
34
  </div>
35
35
  </div>
36
36
  </PlTooltip>
@@ -1,10 +1,10 @@
1
1
  @import "@/assets/mixins";
2
2
 
3
- .ui-chip-tooltip {
3
+ .pl-chip-tooltip {
4
4
  display: inline-flex;
5
5
  }
6
6
 
7
- .ui-chip {
7
+ .pl-chip {
8
8
  --chip-color: var(--txt-01);
9
9
 
10
10
  position: relative;
@@ -20,6 +20,7 @@ import { deepEqual } from '@/helpers/objects';
20
20
  import DropdownListItem from '@/components/DropdownListItem.vue';
21
21
  import LongText from '@/components/LongText.vue';
22
22
  import { normalizeListOptions } from '@/helpers/utils';
23
+ import { useElementPosition } from '@/composition/usePosition';
23
24
 
24
25
  const emit = defineEmits<{
25
26
  /**
@@ -41,11 +42,15 @@ const props = withDefaults(
41
42
  /**
42
43
  * List of available options for the dropdown
43
44
  */
44
- options: Readonly<ListOption<M>[]>;
45
+ options?: Readonly<ListOption<M>[]>;
45
46
  /**
46
47
  * A helper text displayed below the dropdown when there are no errors (optional).
47
48
  */
48
49
  helper?: string;
50
+ /**
51
+ * A helper text displayed below the dropdown when there are no options yet or options is undefined (optional).
52
+ */
53
+ loadingOptionsHelper?: string;
49
54
  /**
50
55
  * Error message displayed below the dropdown (optional)
51
56
  */
@@ -82,6 +87,7 @@ const props = withDefaults(
82
87
  {
83
88
  label: '',
84
89
  helper: undefined,
90
+ loadingOptionsHelper: undefined,
85
91
  error: undefined,
86
92
  placeholder: '...',
87
93
  clearable: false,
@@ -90,6 +96,7 @@ const props = withDefaults(
90
96
  arrowIcon: undefined,
91
97
  arrowIconLarge: undefined,
92
98
  optionSize: 'small',
99
+ options: undefined,
93
100
  },
94
101
  );
95
102
 
@@ -103,6 +110,7 @@ const data = reactive({
103
110
  search: '',
104
111
  activeIndex: -1,
105
112
  open: false,
113
+ optionsHeight: 0,
106
114
  });
107
115
 
108
116
  const findActiveIndex = () =>
@@ -113,11 +121,27 @@ const findActiveIndex = () =>
113
121
 
114
122
  const updateActive = () => (data.activeIndex = findActiveIndex());
115
123
 
124
+ const isLoadingOptions = computed(() => {
125
+ return props.options === undefined;
126
+ });
127
+
128
+ const isDisabled = computed(() => {
129
+ if (isLoadingOptions.value) {
130
+ return true;
131
+ }
132
+
133
+ return props.disabled;
134
+ });
135
+
116
136
  const selectedIndex = computed(() => {
117
- return props.options.findIndex((o) => deepEqual(o.value, props.modelValue));
137
+ return (props.options ?? []).findIndex((o) => deepEqual(o.value, props.modelValue));
118
138
  });
119
139
 
120
140
  const computedError = computed(() => {
141
+ if (isLoadingOptions.value) {
142
+ return undefined;
143
+ }
144
+
121
145
  if (props.error) {
122
146
  return props.error;
123
147
  }
@@ -130,7 +154,7 @@ const computedError = computed(() => {
130
154
  });
131
155
 
132
156
  const optionsRef = computed(() =>
133
- normalizeListOptions(props.options).map((opt, index) => ({
157
+ normalizeListOptions(props.options ?? []).map((opt, index) => ({
134
158
  ...opt,
135
159
  index,
136
160
  isSelected: index === selectedIndex.value,
@@ -184,7 +208,7 @@ const filteredRef = computed(() => {
184
208
  return options;
185
209
  });
186
210
 
187
- const tabindex = computed(() => (props.disabled ? undefined : '0'));
211
+ const tabindex = computed(() => (isDisabled.value ? undefined : '0'));
188
212
 
189
213
  const selectOption = (v: M | undefined) => {
190
214
  emit('update:modelValue', v);
@@ -202,7 +226,9 @@ const toggleOpen = () => (data.open = !data.open);
202
226
  const onInputFocus = () => (data.open = true);
203
227
 
204
228
  const onFocusOut = (event: FocusEvent) => {
205
- if (!root?.value?.contains(event.relatedTarget as Node | null)) {
229
+ const relatedTarget = event.relatedTarget as Node | null;
230
+
231
+ if (!root.value?.contains(relatedTarget) && !list.value?.contains(relatedTarget)) {
206
232
  data.search = '';
207
233
  data.open = false;
208
234
  }
@@ -278,26 +304,55 @@ watchPostEffect(() => {
278
304
  scrollIntoActive();
279
305
  }
280
306
  });
307
+
308
+ const optionsStyle = reactive({
309
+ top: '0px',
310
+ left: '0px',
311
+ width: '0px',
312
+ });
313
+
314
+ watch(list, (el) => {
315
+ if (el) {
316
+ const rect = el.getBoundingClientRect();
317
+ data.optionsHeight = rect.height;
318
+ window.dispatchEvent(new CustomEvent('adjust'));
319
+ }
320
+ });
321
+
322
+ useElementPosition(root, (pos) => {
323
+ const focusWidth = 3; // see css
324
+
325
+ const downTopOffset = pos.top + pos.height + focusWidth;
326
+
327
+ if (downTopOffset + data.optionsHeight > pos.clientHeight) {
328
+ optionsStyle.top = pos.top - data.optionsHeight - focusWidth + 'px';
329
+ } else {
330
+ optionsStyle.top = downTopOffset + 'px';
331
+ }
332
+
333
+ optionsStyle.left = pos.left + 'px';
334
+ optionsStyle.width = pos.width + 'px';
335
+ });
281
336
  </script>
282
337
 
283
338
  <template>
284
- <div class="ui-dropdown__envelope">
339
+ <div class="pl-dropdown__envelope">
285
340
  <div
286
341
  ref="root"
287
342
  :tabindex="tabindex"
288
- class="ui-dropdown"
289
- :class="{ open: data.open, error, disabled }"
343
+ class="pl-dropdown"
344
+ :class="{ open: data.open, error, disabled: isDisabled }"
290
345
  @keydown="handleKeydown"
291
346
  @focusout="onFocusOut"
292
347
  >
293
- <div class="ui-dropdown__container">
294
- <div class="ui-dropdown__field">
348
+ <div class="pl-dropdown__container">
349
+ <div class="pl-dropdown__field">
295
350
  <input
296
351
  ref="input"
297
352
  v-model="data.search"
298
353
  type="text"
299
354
  tabindex="-1"
300
- :disabled="disabled"
355
+ :disabled="isDisabled"
301
356
  :placeholder="computedPlaceholder"
302
357
  spellcheck="false"
303
358
  autocomplete="chrome-off"
@@ -308,7 +363,8 @@ watchPostEffect(() => {
308
363
  <LongText class="input-value"> {{ textValue }} </LongText>
309
364
  </div>
310
365
 
311
- <div class="ui-dropdown__controls">
366
+ <div class="pl-dropdown__controls">
367
+ <div v-if="isLoadingOptions" class="mask-24 mask-loading"></div>
312
368
  <div v-if="clearable && hasValue" class="icon-16 icon-clear" @click.stop="clear" />
313
369
  <slot name="append" />
314
370
  <div v-if="arrowIconLarge" class="arrow-icon" :class="[`icon-24 ${arrowIconLarge}`]" @click.stop="toggleOpen" />
@@ -325,22 +381,25 @@ watchPostEffect(() => {
325
381
  </template>
326
382
  </PlTooltip>
327
383
  </label>
328
- <div v-if="data.open" ref="list" class="ui-dropdown__options">
329
- <DropdownListItem
330
- v-for="(item, index) in filteredRef"
331
- :key="index"
332
- :option="item"
333
- :is-selected="item.isSelected"
334
- :is-hovered="item.isActive"
335
- :size="optionSize"
336
- @click.stop="selectOption(item.value)"
337
- />
338
- <div v-if="!filteredRef.length" class="nothing-found">Nothing found</div>
339
- </div>
340
- <DoubleContour class="ui-dropdown__contour" />
384
+ <Teleport v-if="data.open" to="body">
385
+ <div ref="list" class="pl-dropdown__options" :style="optionsStyle" tabindex="-1">
386
+ <DropdownListItem
387
+ v-for="(item, index) in filteredRef"
388
+ :key="index"
389
+ :option="item"
390
+ :is-selected="item.isSelected"
391
+ :is-hovered="item.isActive"
392
+ :size="optionSize"
393
+ @click.stop="selectOption(item.value)"
394
+ />
395
+ <div v-if="!filteredRef.length" class="nothing-found">Nothing found</div>
396
+ </div>
397
+ </Teleport>
398
+ <DoubleContour class="pl-dropdown__contour" />
341
399
  </div>
342
400
  </div>
343
- <div v-if="computedError" class="ui-dropdown__error">{{ computedError }}</div>
344
- <div v-else-if="helper" class="ui-dropdown__helper">{{ helper }}</div>
401
+ <div v-if="computedError" class="pl-dropdown__error">{{ computedError }}</div>
402
+ <div v-else-if="isLoadingOptions && loadingOptionsHelper" class="pl-dropdown__helper">{{ loadingOptionsHelper }}</div>
403
+ <div v-else-if="helper" class="pl-dropdown__helper">{{ helper }}</div>
345
404
  </div>
346
405
  </template>
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
 
3
3
  import { mount } from '@vue/test-utils';
4
4
  import PlDropdown from '../PlDropdown.vue';
5
+ import { delay } from '@milaboratories/helpers';
5
6
 
6
7
  describe('PlDropdown', () => {
7
8
  it('modelValue', async () => {
@@ -18,13 +19,13 @@ describe('PlDropdown', () => {
18
19
 
19
20
  await wrapper.find('input').trigger('focus');
20
21
 
21
- expect(await wrapper.findAll('.dropdown-list-item').length).toBe(2);
22
+ const options = [...document.body.querySelectorAll('.dropdown-list-item')] as HTMLElement[];
22
23
 
23
- await wrapper
24
- .findAll('.dropdown-list-item')
25
- .filter((node) => node.text().match(/Option 2/))
26
- .at(0)
27
- ?.trigger('click');
24
+ expect(options.length).toBe(2);
25
+
26
+ options[1].click();
27
+
28
+ await delay(20);
28
29
 
29
30
  expect(wrapper.props('modelValue')).toBe(2);
30
31
 
@@ -1,12 +1,73 @@
1
1
  @import "@/assets/mixins";
2
2
 
3
- .ui-dropdown {
3
+ .pl-dropdown__options {
4
+ --option-hover-bg: var(--btn-sec-hover-grey);
5
+
6
+ z-index: var(--z-dropdown-options);
7
+ border: 1px solid var(--border-color-div-grey);
8
+ position: absolute;
9
+ background-color: var(--pl-dropdown-options-bg);
10
+ border-radius: 6px;
11
+ max-height: 244px;
12
+ box-shadow: 0px 4px 12px -2px rgba(15, 36, 77, 0.08), 0px 6px 24px -2px rgba(15, 36, 77, 0.08);
13
+
14
+ @include scrollbar;
15
+
16
+ .nothing-found {
17
+ padding: 0 10px;
18
+ height: var(--control-height);
19
+ line-height: var(--control-height);
20
+ background-color: #fff;
21
+ opacity: 0.5;
22
+ font-style: italic;
23
+ }
24
+
25
+ .option {
26
+ position: relative;
27
+ padding: 0 30px 0 10px;
28
+ height: var(--control-height);
29
+ line-height: var(--control-height);
30
+ cursor: pointer;
31
+ user-select: none;
32
+
33
+ .checkmark {
34
+ position: absolute;
35
+ display: none;
36
+ right: 10px;
37
+ @include abs-center-y();
38
+ }
39
+
40
+ >span {
41
+ display: block;
42
+ overflow: hidden;
43
+ white-space: nowrap;
44
+ max-width: 100%;
45
+ text-overflow: ellipsis;
46
+ }
47
+
48
+ &.selected {
49
+ background-color: var(--color-active-select);
50
+
51
+ .checkmark {
52
+ display: block;
53
+ }
54
+ }
55
+
56
+ &.active:not(.selected) {
57
+ background-color: var(--option-hover-bg);
58
+ }
59
+
60
+ &:hover {
61
+ background-color: var(--option-hover-bg);
62
+ }
63
+ }
64
+ }
65
+
66
+ .pl-dropdown {
4
67
  $root: &;
5
68
 
6
69
  --contour-color: var(--txt-01);
7
70
  --contour-border-width: 1px;
8
- --options-bg: #fff;
9
- --option-hover-bg: var(--btn-sec-hover-grey);
10
71
 
11
72
  --label-offset-left-x: 8px;
12
73
  --label-offset-right-x: 8px;
@@ -20,10 +81,6 @@
20
81
  font-size: var(--font-size-base);
21
82
  font-weight: var(--font-weigh-base);
22
83
 
23
- [data-theme='dark'] & {
24
- --options-bg: #1b1b1f;
25
- }
26
-
27
84
  &__envelope {
28
85
  font-family: var(--control-font-family);
29
86
  min-width: 160px;
@@ -51,64 +108,6 @@
51
108
  pointer-events: none;
52
109
  }
53
110
 
54
- &__options {
55
- position: relative;
56
- background-color: var(--options-bg);
57
- border-radius: 0 0 6px 6px;
58
- max-height: 244px;
59
- border-top: 1px solid var(--color-div-black);
60
-
61
- @include scrollbar;
62
-
63
- .nothing-found {
64
- padding: 0 10px;
65
- height: var(--control-height);
66
- line-height: var(--control-height);
67
- background-color: #fff;
68
- opacity: 0.5;
69
- font-style: italic;
70
- }
71
-
72
- .option {
73
- position: relative;
74
- padding: 0 30px 0 10px;
75
- height: var(--control-height);
76
- line-height: var(--control-height);
77
- cursor: pointer;
78
- user-select: none;
79
-
80
- .checkmark {
81
- position: absolute;
82
- display: none;
83
- right: 10px;
84
- @include abs-center-y();
85
- }
86
-
87
- > span {
88
- display: block;
89
- overflow: hidden;
90
- white-space: nowrap;
91
- max-width: 100%;
92
- text-overflow: ellipsis;
93
- }
94
-
95
- &.selected {
96
- background-color: var(--color-active-select);
97
- .checkmark {
98
- display: block;
99
- }
100
- }
101
-
102
- &.active:not(.selected) {
103
- background-color: var(--option-hover-bg);
104
- }
105
-
106
- &:hover {
107
- background-color: var(--option-hover-bg);
108
- }
109
- }
110
- }
111
-
112
111
  &__field {
113
112
  position: relative;
114
113
  border-radius: 6px;
@@ -184,14 +183,21 @@
184
183
  cursor: pointer;
185
184
  }
186
185
 
187
- .mask-16,.mask-24 {
186
+ .mask-16,
187
+ .mask-24 {
188
188
  background-color: var(--control-mask-fill);
189
189
  cursor: pointer;
190
190
  }
191
+
192
+ .mask-loading {
193
+ animation: spin 2.5s linear infinite;
194
+ background-color: #07AD3E;
195
+ }
191
196
  }
192
197
 
193
198
  .arrow-icon {
194
199
  cursor: pointer;
200
+
195
201
  // Default "arrow" icon (16x16)
196
202
  &.arrow-icon-default {
197
203
  background-color: var(--control-mask-fill);
@@ -214,7 +220,7 @@
214
220
  border-radius: 6px 6px 0 0;
215
221
  }
216
222
 
217
- .arrow {
223
+ .arrow-icon {
218
224
  background-color: var(--control-mask-fill);
219
225
  @include mask(url(@/assets/images/16_chevron-up.svg), 16px);
220
226
  }
@@ -246,5 +252,7 @@
246
252
  --control-mask-fill: var(--color-dis-01);
247
253
  --label-color: var(--color-dis-01);
248
254
  cursor: not-allowed;
255
+ pointer-events: none;
256
+ user-select: none;
249
257
  }
250
- }
258
+ }