@milaboratories/uikit 2.2.21 → 2.2.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.2.21",
3
+ "version": "2.2.22",
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.10",
33
33
  "yarpm": "^1.2.0",
34
34
  "svgo": "^3.3.2",
35
- "@platforma-sdk/model": "^1.14.1",
36
- "@milaboratories/helpers": "^1.6.8"
35
+ "@milaboratories/helpers": "^1.6.8",
36
+ "@platforma-sdk/model": "^1.14.1"
37
37
  },
38
38
  "scripts": {
39
39
  "dev": "vite",
@@ -9,20 +9,19 @@ export default {
9
9
 
10
10
  <script lang="ts" setup generic="M = unknown">
11
11
  import './pl-dropdown.scss';
12
- import { computed, reactive, ref, unref, useSlots, watch, watchPostEffect } from 'vue';
13
- import { tap, tapIf } from '@/helpers/functions';
12
+ import { computed, reactive, ref, unref, useSlots, useTemplateRef, watch, watchPostEffect } from 'vue';
13
+ import { tap } from '@/helpers/functions';
14
14
  import { PlTooltip } from '@/components/PlTooltip';
15
15
  import DoubleContour from '@/utils/DoubleContour.vue';
16
16
  import { useLabelNotch } from '@/utils/useLabelNotch';
17
17
  import type { ListOption, ListOptionNormalized } from '@/types';
18
- import { scrollIntoView } from '@/helpers/dom';
19
18
  import { deepEqual } from '@/helpers/objects';
20
19
  import DropdownListItem from '@/components/DropdownListItem.vue';
21
20
  import LongText from '@/components/LongText.vue';
22
21
  import { normalizeListOptions } from '@/helpers/utils';
23
- import { useElementPosition } from '@/composition/usePosition';
24
22
  import { PlIcon16 } from '../PlIcon16';
25
23
  import { PlMaskIcon24 } from '../PlMaskIcon24';
24
+ import { DropdownOverlay } from '@/utils/DropdownOverlay';
26
25
 
27
26
  const emit = defineEmits<{
28
27
  /**
@@ -104,10 +103,11 @@ const props = withDefaults(
104
103
 
105
104
  const slots = useSlots();
106
105
 
107
- const root = ref<HTMLElement | undefined>();
108
- const list = ref<HTMLElement | undefined>();
106
+ const rootRef = ref<HTMLElement | undefined>();
109
107
  const input = ref<HTMLInputElement | undefined>();
110
108
 
109
+ const overlay = useTemplateRef('overlay');
110
+
111
111
  const data = reactive({
112
112
  search: '',
113
113
  activeIndex: -1,
@@ -216,7 +216,7 @@ const selectOption = (v: M | undefined) => {
216
216
  emit('update:modelValue', v);
217
217
  data.search = '';
218
218
  data.open = false;
219
- root?.value?.focus();
219
+ rootRef?.value?.focus();
220
220
  };
221
221
 
222
222
  const clear = () => emit('update:modelValue', undefined);
@@ -230,24 +230,12 @@ const onInputFocus = () => (data.open = true);
230
230
  const onFocusOut = (event: FocusEvent) => {
231
231
  const relatedTarget = event.relatedTarget as Node | null;
232
232
 
233
- if (!root.value?.contains(relatedTarget) && !list.value?.contains(relatedTarget)) {
233
+ if (!rootRef.value?.contains(relatedTarget) && !overlay.value?.listRef?.contains(relatedTarget)) {
234
234
  data.search = '';
235
235
  data.open = false;
236
236
  }
237
237
  };
238
238
 
239
- const scrollIntoActive = () => {
240
- const $list = list.value;
241
-
242
- if (!$list) {
243
- return;
244
- }
245
-
246
- tapIf($list.querySelector('.hovered-item') as HTMLElement, (opt) => {
247
- scrollIntoView($list, opt);
248
- });
249
- };
250
-
251
239
  const handleKeydown = (e: { code: string; preventDefault(): void }) => {
252
240
  if (!['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.code)) {
253
241
  return;
@@ -266,7 +254,7 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
266
254
 
267
255
  if (e.code === 'Escape') {
268
256
  data.open = false;
269
- root.value?.focus();
257
+ rootRef.value?.focus();
270
258
  }
271
259
 
272
260
  const filtered = unref(filteredRef);
@@ -290,7 +278,7 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
290
278
  data.activeIndex = filteredRef.value[newIndex].index ?? -1;
291
279
  };
292
280
 
293
- useLabelNotch(root);
281
+ useLabelNotch(rootRef);
294
282
 
295
283
  watch(() => props.modelValue, updateActive, { immediate: true });
296
284
 
@@ -303,44 +291,15 @@ watchPostEffect(() => {
303
291
  data.search; // to watch
304
292
 
305
293
  if (data.activeIndex >= 0 && data.open) {
306
- scrollIntoActive();
294
+ overlay.value?.scrollIntoActive();
307
295
  }
308
296
  });
309
-
310
- const optionsStyle = reactive({
311
- top: '0px',
312
- left: '0px',
313
- width: '0px',
314
- });
315
-
316
- watch(list, (el) => {
317
- if (el) {
318
- const rect = el.getBoundingClientRect();
319
- data.optionsHeight = rect.height;
320
- window.dispatchEvent(new CustomEvent('adjust'));
321
- }
322
- });
323
-
324
- useElementPosition(root, (pos) => {
325
- const focusWidth = 3; // see css
326
-
327
- const downTopOffset = pos.top + pos.height + focusWidth;
328
-
329
- if (downTopOffset + data.optionsHeight > pos.clientHeight) {
330
- optionsStyle.top = pos.top - data.optionsHeight - focusWidth + 'px';
331
- } else {
332
- optionsStyle.top = downTopOffset + 'px';
333
- }
334
-
335
- optionsStyle.left = pos.left + 'px';
336
- optionsStyle.width = pos.width + 'px';
337
- });
338
297
  </script>
339
298
 
340
299
  <template>
341
300
  <div class="pl-dropdown__envelope">
342
301
  <div
343
- ref="root"
302
+ ref="rootRef"
344
303
  :tabindex="tabindex"
345
304
  class="pl-dropdown"
346
305
  :class="{ open: data.open, error, disabled: isDisabled }"
@@ -383,20 +342,18 @@ useElementPosition(root, (pos) => {
383
342
  </template>
384
343
  </PlTooltip>
385
344
  </label>
386
- <Teleport v-if="data.open" to="body">
387
- <div ref="list" class="pl-dropdown__options" :style="optionsStyle" tabindex="-1">
388
- <DropdownListItem
389
- v-for="(item, index) in filteredRef"
390
- :key="index"
391
- :option="item"
392
- :is-selected="item.isSelected"
393
- :is-hovered="item.isActive"
394
- :size="optionSize"
395
- @click.stop="selectOption(item.value)"
396
- />
397
- <div v-if="!filteredRef.length" class="nothing-found">Nothing found</div>
398
- </div>
399
- </Teleport>
345
+ <DropdownOverlay v-if="data.open" ref="overlay" :root="rootRef" class="pl-dropdown__options" tabindex="-1" :gap="3">
346
+ <DropdownListItem
347
+ v-for="(item, index) in filteredRef"
348
+ :key="index"
349
+ :option="item"
350
+ :is-selected="item.isSelected"
351
+ :is-hovered="item.isActive"
352
+ :size="optionSize"
353
+ @click.stop="selectOption(item.value)"
354
+ />
355
+ <div v-if="!filteredRef.length" class="nothing-found">Nothing found</div>
356
+ </DropdownOverlay>
400
357
  <DoubleContour class="pl-dropdown__contour" />
401
358
  </div>
402
359
  </div>
@@ -9,18 +9,17 @@ export default {
9
9
 
10
10
  <script lang="ts" setup generic="M = unknown">
11
11
  import './pl-dropdown-multi.scss';
12
- import { computed, reactive, ref, unref, useSlots, watch, watchPostEffect } from 'vue';
13
- import { tap, tapIf } from '@/helpers/functions';
12
+ import { computed, reactive, ref, unref, useSlots, useTemplateRef, watch, watchPostEffect } from 'vue';
13
+ import { tap } from '@/helpers/functions';
14
14
  import { PlTooltip } from '@/components/PlTooltip';
15
15
  import { PlChip } from '@/components/PlChip';
16
16
  import DoubleContour from '@/utils/DoubleContour.vue';
17
17
  import { useLabelNotch } from '@/utils/useLabelNotch';
18
18
  import type { ListOption } from '@/types';
19
- import { scrollIntoView } from '@/helpers/dom';
20
19
  import DropdownListItem from '@/components/DropdownListItem.vue';
21
20
  import { deepEqual, deepIncludes } from '@/helpers/objects';
22
21
  import { normalizeListOptions } from '@/helpers/utils';
23
- import { useElementPosition } from '@/composition/usePosition';
22
+ import DropdownOverlay from '@/utils/DropdownOverlay/DropdownOverlay.vue';
24
23
 
25
24
  const emit = defineEmits<{
26
25
  (e: 'update:modelValue', v: M[]): void;
@@ -77,9 +76,10 @@ const props = withDefaults(
77
76
  );
78
77
 
79
78
  const rootRef = ref<HTMLElement | undefined>();
80
- const list = ref<HTMLElement | undefined>();
81
79
  const input = ref<HTMLInputElement | undefined>();
82
80
 
81
+ const overlay = useTemplateRef('overlay');
82
+
83
83
  const data = reactive({
84
84
  search: '',
85
85
  activeOption: -1,
@@ -153,24 +153,14 @@ const toggleModel = () => (data.open = !data.open);
153
153
  const onFocusOut = (event: FocusEvent) => {
154
154
  const relatedTarget = event.relatedTarget as Node | null;
155
155
 
156
- if (!rootRef.value?.contains(relatedTarget) && !list.value?.contains(relatedTarget)) {
156
+ console.log('>>>> overlay.value?.$el', overlay.value?.$el);
157
+
158
+ if (!rootRef.value?.contains(relatedTarget) && !overlay.value?.listRef?.contains(relatedTarget)) {
157
159
  data.search = '';
158
160
  data.open = false;
159
161
  }
160
162
  };
161
163
 
162
- const scrollIntoActive = () => {
163
- const $list = list.value;
164
-
165
- if (!$list) {
166
- return;
167
- }
168
-
169
- tapIf($list.querySelector('.hovered-item') as HTMLElement, (opt) => {
170
- scrollIntoView($list, opt);
171
- });
172
- };
173
-
174
164
  const handleKeydown = (e: { code: string; preventDefault(): void }) => {
175
165
  const { open, activeOption } = data;
176
166
 
@@ -206,7 +196,7 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
206
196
 
207
197
  data.activeOption = Math.abs(activeOption + d + length) % length;
208
198
 
209
- requestAnimationFrame(scrollIntoActive);
199
+ requestAnimationFrame(() => overlay.value?.scrollIntoActive());
210
200
  };
211
201
 
212
202
  useLabelNotch(rootRef);
@@ -221,38 +211,9 @@ watchPostEffect(() => {
221
211
  data.search;
222
212
 
223
213
  if (data.open) {
224
- scrollIntoActive();
225
- }
226
- });
227
-
228
- const optionsStyle = reactive({
229
- top: '0px',
230
- left: '0px',
231
- width: '0px',
232
- });
233
-
234
- watch(list, (el) => {
235
- if (el) {
236
- const rect = el.getBoundingClientRect();
237
- data.optionsHeight = rect.height;
238
- window.dispatchEvent(new CustomEvent('adjust'));
214
+ overlay.value?.scrollIntoActive();
239
215
  }
240
216
  });
241
-
242
- useElementPosition(rootRef, (pos) => {
243
- const focusWidth = 5; // see css
244
-
245
- const downTopOffset = pos.top + pos.height + focusWidth;
246
-
247
- if (downTopOffset + data.optionsHeight > pos.clientHeight) {
248
- optionsStyle.top = pos.top - data.optionsHeight - focusWidth + 'px';
249
- } else {
250
- optionsStyle.top = downTopOffset + 'px';
251
- }
252
-
253
- optionsStyle.left = pos.left + 'px';
254
- optionsStyle.width = pos.width + 'px';
255
- });
256
217
  </script>
257
218
 
258
219
  <template>
@@ -297,27 +258,33 @@ useElementPosition(rootRef, (pos) => {
297
258
  </template>
298
259
  </PlTooltip>
299
260
  </label>
300
- <Teleport v-if="data.open" to="body">
301
- <div ref="list" class="pl-multi-dropdown__options" :style="optionsStyle" tabindex="-1" @focusout="onFocusOut">
302
- <div class="pl-multi-dropdown__open-chips-container">
303
- <PlChip v-for="(opt, i) in selectedOptionsRef" :key="i" closeable small @close="unselectOption(opt.value)">
304
- {{ opt.label || opt.value }}
305
- </PlChip>
306
- </div>
307
- <DropdownListItem
308
- v-for="(item, index) in filteredOptionsRef"
309
- :key="index"
310
- :option="item"
311
- :text-item="'text'"
312
- :is-selected="item.selected"
313
- :is-hovered="data.activeOption == index"
314
- size="medium"
315
- use-checkbox
316
- @click.stop="selectOption(item.value)"
317
- />
318
- <div v-if="!filteredOptionsRef.length" class="nothing-found">Nothing found</div>
261
+ <DropdownOverlay
262
+ v-if="data.open"
263
+ ref="overlay"
264
+ :root="rootRef"
265
+ class="pl-multi-dropdown__options"
266
+ :gap="3"
267
+ tabindex="-1"
268
+ @focusout="onFocusOut"
269
+ >
270
+ <div class="pl-multi-dropdown__open-chips-container">
271
+ <PlChip v-for="(opt, i) in selectedOptionsRef" :key="i" closeable small @close="unselectOption(opt.value)">
272
+ {{ opt.label || opt.value }}
273
+ </PlChip>
319
274
  </div>
320
- </Teleport>
275
+ <DropdownListItem
276
+ v-for="(item, index) in filteredOptionsRef"
277
+ :key="index"
278
+ :option="item"
279
+ :text-item="'text'"
280
+ :is-selected="item.selected"
281
+ :is-hovered="data.activeOption == index"
282
+ size="medium"
283
+ use-checkbox
284
+ @click.stop="selectOption(item.value)"
285
+ />
286
+ <div v-if="!filteredOptionsRef.length" class="nothing-found">Nothing found</div>
287
+ </DropdownOverlay>
321
288
  <DoubleContour class="pl-multi-dropdown__contour" />
322
289
  </div>
323
290
  </div>
package/src/index.ts CHANGED
@@ -90,6 +90,7 @@ export { useDraggable } from './composition/useDraggable';
90
90
  */
91
91
 
92
92
  export { default as PlCloseModalBtn } from './utils/PlCloseModalBtn.vue';
93
+ export * from './utils/DropdownOverlay';
93
94
 
94
95
  /**
95
96
  * Technical
@@ -0,0 +1,81 @@
1
+ <script lang="ts" setup>
2
+ import { useElementPosition } from '@/composition/usePosition';
3
+ import { scrollIntoView } from '@/helpers/dom';
4
+ import { tapIf } from '@milaboratories/helpers';
5
+ import { reactive, ref, toRef, watch } from 'vue';
6
+
7
+ const props = defineProps<{
8
+ root: HTMLElement | undefined; // element to "track"
9
+ gap?: number; // additional gap between overlay and "root" component
10
+ }>();
11
+
12
+ const data = reactive({
13
+ optionsHeight: 0,
14
+ });
15
+
16
+ const optionsStyle = reactive({
17
+ top: '0px',
18
+ left: '0px',
19
+ width: '0px',
20
+ });
21
+
22
+ const rootRef = toRef(props, 'root');
23
+
24
+ const listRef = ref<HTMLElement>();
25
+
26
+ const scrollIntoActive = () => {
27
+ const $list = listRef.value;
28
+
29
+ if (!$list) {
30
+ return;
31
+ }
32
+
33
+ tapIf($list.querySelector('.hovered-item') as HTMLElement, (opt) => {
34
+ scrollIntoView($list, opt);
35
+ });
36
+ };
37
+
38
+ defineExpose({
39
+ scrollIntoActive,
40
+ listRef,
41
+ });
42
+
43
+ watch(listRef, (el) => {
44
+ if (el) {
45
+ requestAnimationFrame(() => {
46
+ const rect = el.getBoundingClientRect();
47
+ data.optionsHeight = rect.height;
48
+ window.dispatchEvent(new CustomEvent('adjust'));
49
+ });
50
+ }
51
+ });
52
+
53
+ useElementPosition(rootRef, (pos) => {
54
+ const bodyRect = document.body.getBoundingClientRect();
55
+
56
+ const top = pos.top - bodyRect.top;
57
+
58
+ const left = pos.left - bodyRect.left;
59
+
60
+ const gap = props.gap ?? 0;
61
+
62
+ const downTopOffset = top + pos.height + gap;
63
+
64
+ if (downTopOffset + data.optionsHeight > pos.clientHeight) {
65
+ optionsStyle.top = top - data.optionsHeight - gap + 'px';
66
+ } else {
67
+ optionsStyle.top = downTopOffset + 'px';
68
+ }
69
+
70
+ optionsStyle.left = left + 'px';
71
+ optionsStyle.width = pos.width + 'px';
72
+ });
73
+ </script>
74
+
75
+ <template>
76
+ <Teleport to="body">
77
+ <div ref="listRef" v-bind="$attrs" :style="optionsStyle" tabindex="-1">
78
+ <slot ref="list" />
79
+ </div>
80
+ </Teleport>
81
+ </template>
@@ -0,0 +1 @@
1
+ export { default as DropdownOverlay } from './DropdownOverlay.vue';