@milaboratories/uikit 2.2.21 → 2.2.23

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.23",
4
4
  "type": "module",
5
5
  "main": "dist/pl-uikit.umd.js",
6
6
  "module": "dist/pl-uikit.js",
@@ -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>
@@ -11,6 +11,9 @@ import { defaultData, useVisibleItems, vTextOverflown } from './remote';
11
11
  import { PlSearchField } from '../PlSearchField';
12
12
  import { PlIcon16 } from '../PlIcon16';
13
13
 
14
+ // note that on a Mac, a click combined with the control key is intercepted by the operating system and used to open a context menu, so ctrlKey is not detectable on click events.
15
+ const isCtrlOrMeta = (ev: KeyboardEvent | MouseEvent) => ev.ctrlKey || ev.metaKey;
16
+
14
17
  defineEmits<{
15
18
  (e: 'update:modelValue', value: boolean): void;
16
19
  (e: 'import:files', value: ImportedFiles): void;
@@ -119,7 +122,10 @@ const setDirPath = (dirPath: string) => {
119
122
  };
120
123
 
121
124
  const selectFile = (ev: MouseEvent, file: FileDialogItem) => {
122
- const { shiftKey, metaKey } = ev;
125
+ const { shiftKey } = ev;
126
+
127
+ const ctrlOrMetaKey = isCtrlOrMeta(ev);
128
+
123
129
  const { lastSelected } = data;
124
130
 
125
131
  ev.preventDefault();
@@ -135,7 +141,7 @@ const selectFile = (ev: MouseEvent, file: FileDialogItem) => {
135
141
  return;
136
142
  }
137
143
 
138
- if (!metaKey && !shiftKey) {
144
+ if (!ctrlOrMetaKey && !shiftKey) {
139
145
  data.items.forEach((f) => {
140
146
  if (f.id !== file.id) {
141
147
  f.selected = false;
@@ -238,12 +244,14 @@ useEventListener(document, 'keydown', (ev: KeyboardEvent) => {
238
244
  return;
239
245
  }
240
246
 
241
- if (ev.metaKey && ev.code === 'KeyA') {
247
+ const ctrlOrMetaKey = isCtrlOrMeta(ev);
248
+
249
+ if (ctrlOrMetaKey && ev.code === 'KeyA') {
242
250
  ev.preventDefault();
243
251
  selectAll();
244
252
  }
245
253
 
246
- if (ev.metaKey && ev.shiftKey && ev.code === 'Period') {
254
+ if (ctrlOrMetaKey && ev.shiftKey && ev.code === 'Period') {
247
255
  ev.preventDefault();
248
256
  data.showHiddenItems = !data.showHiddenItems;
249
257
  }
@@ -13,7 +13,7 @@ import DoubleContour from '@/utils/DoubleContour.vue';
13
13
  import { useLabelNotch } from '@/utils/useLabelNotch';
14
14
  import { useValidation } from '@/utils/useValidation';
15
15
  import { PlIcon16 } from '../PlIcon16';
16
- import { PlIcon24 } from '../PlIcon24';
16
+ import { PlMaskIcon24 } from '../PlMaskIcon24';
17
17
  import type { Equal } from '@milaboratories/helpers';
18
18
 
19
19
  const slots = useSlots();
@@ -160,7 +160,7 @@ const displayErrors = computed(() => {
160
160
 
161
161
  const hasErrors = computed(() => displayErrors.value.length > 0);
162
162
 
163
- const canShowClearable = computed(() => props.clearable && nonEmpty.value && props.type !== 'password');
163
+ const canShowClearable = computed(() => props.clearable && nonEmpty.value && props.type !== 'password' && !props.disabled);
164
164
 
165
165
  const togglePasswordVisibility = () => (showPassword.value = !showPassword.value);
166
166
 
@@ -206,7 +206,7 @@ useLabelNotch(rootRef);
206
206
  />
207
207
  <div class="pl-text-field__append">
208
208
  <PlIcon16 v-if="canShowClearable" class="pl-text-field__clearable" name="delete-clear" @click="clear" />
209
- <PlIcon24 v-if="type === 'password'" :name="passwordIcon" style="cursor: pointer" @click="togglePasswordVisibility" />
209
+ <PlMaskIcon24 v-if="type === 'password'" :name="passwordIcon" style="cursor: pointer" @click="togglePasswordVisibility" />
210
210
  <slot name="append" />
211
211
  </div>
212
212
  <DoubleContour class="pl-text-field__contour" />
@@ -3,6 +3,7 @@
3
3
  .pl-text-field {
4
4
  $root: &;
5
5
 
6
+ --pl-text-field-text-color: var(--txt-01);
6
7
  --contour-color: var(--txt-01);
7
8
  --label-color: var(--txt-01);
8
9
  --contour-border-width: 1px;
@@ -36,7 +37,7 @@
36
37
  border: none;
37
38
  font-size: inherit;
38
39
  background-color: transparent;
39
- color: var(--txt-01);
40
+ color: var(--pl-text-field-text-color);
40
41
  caret-color: var(--border-color-focus);
41
42
  cursor: inherit;
42
43
 
@@ -140,6 +141,9 @@
140
141
 
141
142
  &.disabled {
142
143
  --contour-color: var(--color-dis-01);
144
+ --label-color: var(--dis-01);
145
+ --pl-text-field-text-color: var(--dis-01);
146
+ --mask-icon-bg-color: var(--dis-01);
143
147
  cursor: not-allowed;
144
148
  }
145
149
  }
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';