@v1nt1248/3nclient-lib 0.3.29 → 0.3.30

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
@@ -2,7 +2,7 @@
2
2
  "name": "@v1nt1248/3nclient-lib",
3
3
  "license": "AGPL-3.0-or-later",
4
4
  "author": "v1nt1248",
5
- "version": "0.3.29",
5
+ "version": "0.3.30",
6
6
  "description": "Library for 3NWeb clients",
7
7
  "type": "module",
8
8
  "files": [
@@ -57,6 +57,11 @@ export interface Ui3nAutocompleteProps<T extends Ui3nAutocompleteOptionBase> {
57
57
  * @default false
58
58
  */
59
59
  hideSelected?: boolean;
60
+ /**
61
+ * Whether to lock scroll when menu is open
62
+ * @default false
63
+ */
64
+ lockScroll?: boolean;
60
65
  /**
61
66
  * Items to display
62
67
  */
@@ -19,6 +19,7 @@
19
19
  const vUi3nHtml = Ui3nHtml;
20
20
 
21
21
  const props = withDefaults(defineProps<Ui3nAutocompleteProps<T>>(), {
22
+ lockScroll: false,
22
23
  clearOnSelect: false,
23
24
  filterKeys: () => [],
24
25
  hideSelected: false,
@@ -133,7 +134,7 @@
133
134
  function handlePressingDownKey() {
134
135
  if (activeItemsIndex.value === null) {
135
136
  activeItemsIndex.value = 0;
136
- menuBodyEl.value && menuBodyEl.value.focus();
137
+ menuBodyEl.value && menuBodyEl.value.focus({ preventScroll: true });
137
138
  return;
138
139
  }
139
140
 
@@ -145,7 +146,7 @@
145
146
  function handlePressingUpKey() {
146
147
  if (activeItemsIndex.value === null) {
147
148
  activeItemsIndex.value = size(filteredItems.value) - 1;
148
- menuBodyEl.value && menuBodyEl.value.focus();
149
+ menuBodyEl.value && menuBodyEl.value.focus({ preventScroll: true });
149
150
  return;
150
151
  }
151
152
 
@@ -167,7 +168,7 @@
167
168
  if (activeItemsIndex.value === null && !isMenuOpen.value) {
168
169
  isMenuOpen.value = true;
169
170
  activeItemsIndex.value = 0;
170
- menuBodyEl.value && menuBodyEl.value.focus();
171
+ menuBodyEl.value && menuBodyEl.value.focus({ preventScroll: true });
171
172
  return;
172
173
  }
173
174
 
@@ -322,8 +323,10 @@
322
323
 
323
324
  <ui3n-menu
324
325
  v-model="isMenuOpen"
326
+ :lock-scroll="lockScroll"
325
327
  :offset-x="2"
326
328
  :offset-y="4"
329
+ position-strategy="fixed"
327
330
  position-autoupdate
328
331
  :content-border-radius="[0, 0, 8, 8]"
329
332
  :disabled="disabled"
@@ -381,7 +384,7 @@
381
384
  :placeholder="isEmpty(modelValue) && placeholder ? placeholder : ''"
382
385
  :disabled="disabled"
383
386
  @input="onInput"
384
- @focusin="emits('update:focused', true)"
387
+ @focusin.prevent="emits('update:focused', true)"
385
388
  @focusout="onBlur"
386
389
  @keydown.down="onKeydown($event, 'down')"
387
390
  @keydown.up="onKeydown($event, 'up')"
@@ -31,6 +31,11 @@ export interface Ui3nMenuProps {
31
31
  * @default true
32
32
  */
33
33
  allowFlip?: boolean;
34
+ /**
35
+ * Whether to lock scroll when menu is open
36
+ * @default false
37
+ */
38
+ lockScroll?: boolean;
34
39
  /**
35
40
  * Offset X
36
41
  * @default 0
@@ -1,11 +1,12 @@
1
1
  <script lang="ts" setup>
2
- import { computed, ref, watch, useTemplateRef } from 'vue';
2
+ import { computed, nextTick, onUnmounted, ref, watch, useTemplateRef } from 'vue';
3
3
  import { autoUpdate, flip, useFloating, offset, shift } from '@floating-ui/vue';
4
4
  import { default as vClickOutside } from '../../directives/ui3n-click-outside';
5
5
  import { Nullable } from '@/types';
6
6
  import type { Ui3nMenuEmits, Ui3nMenuProps, Ui3nMenuSlots, Ui3nMenuExpose } from './types';
7
7
 
8
8
  const props = withDefaults(defineProps<Ui3nMenuProps>(), {
9
+ lockScroll: false,
9
10
  positionStrategy: 'absolute',
10
11
  offsetX: 0,
11
12
  offsetY: 0,
@@ -23,6 +24,8 @@
23
24
  const menuContentElement = useTemplateRef<HTMLDivElement>('menu-content-element');
24
25
  const isShow = ref(false);
25
26
 
27
+ let targetScrollElement: HTMLElement | null = null;
28
+
26
29
  const outerTriggerElement = ref<Nullable<HTMLElement>>(props.triggerElement || null);
27
30
 
28
31
  const usedTriggerElement = computed(() => outerTriggerElement.value || menuTriggerElement.value);
@@ -72,6 +75,109 @@
72
75
  };
73
76
  });
74
77
 
78
+ function preventScrollEvent(e: Event) {
79
+ e.preventDefault();
80
+ }
81
+
82
+ function getVerticalScrollParent(element: HTMLElement | null): HTMLElement {
83
+ if (!element) {
84
+ return document.body;
85
+ }
86
+
87
+ let parent: HTMLElement | null = element.parentElement;
88
+
89
+ while (parent && parent !== document.body && parent !== document.documentElement) {
90
+ const style = window.getComputedStyle(parent);
91
+
92
+ if (/(auto|scroll)/.test(style.overflowY || style.overflow)) {
93
+ if (parent.scrollHeight > parent.clientHeight) {
94
+ return parent;
95
+ }
96
+ }
97
+ parent = parent.parentElement;
98
+ }
99
+
100
+ return document.body;
101
+ }
102
+
103
+ function lockScroll() {
104
+ if (!props.lockScroll) {
105
+ return;
106
+ }
107
+
108
+ const trigger = usedTriggerElement.value;
109
+ targetScrollElement = getVerticalScrollParent(trigger);
110
+
111
+ if (targetScrollElement) {
112
+ const el = targetScrollElement;
113
+
114
+ const currentLocks = Number(el.dataset.scrollLocksCount || '0');
115
+
116
+ // If this is the FIRST menu opened for this container
117
+ if (currentLocks === 0) {
118
+ el.dataset.scrollLocksCount = '1';
119
+
120
+ // We remember the original overscroll-behavior-y in data-original-overscroll
121
+ el.dataset.originalOverscroll = el.style.overscrollBehaviorY || '';
122
+
123
+ // Isolating scrolling
124
+ el.style.overscrollBehaviorY = 'contain';
125
+
126
+ if (targetScrollElement === document.body) {
127
+ document.addEventListener('wheel', preventScrollEvent, { passive: false });
128
+ document.addEventListener('touchmove', preventScrollEvent, { passive: false });
129
+ } else {
130
+ targetScrollElement.addEventListener('wheel', preventScrollEvent, { passive: false });
131
+ targetScrollElement.addEventListener('touchmove', preventScrollEvent, { passive: false });
132
+ }
133
+ } else {
134
+ // If the container is already blocked by another menu, simply increment the counter
135
+ el.dataset.scrollLocksCount = String(currentLocks + 1);
136
+ }
137
+ }
138
+ }
139
+
140
+ function unlockScroll() {
141
+ if (!targetScrollElement) {
142
+ return;
143
+ }
144
+
145
+ const el = targetScrollElement;
146
+ const currentLocks = Number(el.dataset.scrollLocksCount || '0');
147
+
148
+ if (currentLocks > 0) {
149
+ const newLocks = currentLocks - 1;
150
+
151
+ // Release the lock only when the LAST menu holding this container has closed
152
+ if (newLocks === 0) {
153
+ const originalOverscroll = el.dataset.originalOverscroll;
154
+
155
+ if (originalOverscroll) {
156
+ el.style.overscrollBehaviorY = originalOverscroll;
157
+ } else {
158
+ el.style.removeProperty('overscroll-behavior-y');
159
+ }
160
+
161
+ if (targetScrollElement === document.body) {
162
+ document.removeEventListener('wheel', preventScrollEvent);
163
+ document.removeEventListener('touchmove', preventScrollEvent);
164
+ } else {
165
+ targetScrollElement.removeEventListener('wheel', preventScrollEvent);
166
+ targetScrollElement.removeEventListener('touchmove', preventScrollEvent);
167
+ }
168
+
169
+ // We completely remove data attributes from the DOM tree to avoid leaving garbage.
170
+ delete el.dataset.scrollLocksCount;
171
+ delete el.dataset.originalOverscroll;
172
+ } else {
173
+ // If there are still open menus, simply keep the reduced counter
174
+ el.dataset.scrollLocksCount = String(newLocks);
175
+ }
176
+ }
177
+
178
+ targetScrollElement = null;
179
+ }
180
+
75
181
  function toggleMenu() {
76
182
  if (props.disabled) {
77
183
  return;
@@ -101,13 +207,13 @@
101
207
  }
102
208
  }
103
209
 
104
- watch(isPositioned, val => {
105
- val ? emits('opened') : emits('closed');
106
- });
107
-
108
210
  watch(
109
211
  () => props.modelValue,
110
- val => {
212
+ async val => {
213
+ if (!val) {
214
+ unlockScroll();
215
+ }
216
+
111
217
  if (isShow.value !== val) {
112
218
  isShow.value = val;
113
219
  }
@@ -115,6 +221,15 @@
115
221
  { immediate: true },
116
222
  );
117
223
 
224
+ watch(isPositioned, val => {
225
+ if (val) {
226
+ lockScroll();
227
+ emits('opened');
228
+ } else {
229
+ emits('closed');
230
+ }
231
+ });
232
+
118
233
  watch(
119
234
  () => props.triggerElement,
120
235
  val => {
@@ -131,6 +246,10 @@
131
246
  }
132
247
  });
133
248
 
249
+ onUnmounted(() => {
250
+ unlockScroll();
251
+ });
252
+
134
253
  defineExpose<Ui3nMenuExpose>({
135
254
  isPositioned,
136
255
  update,
@@ -187,7 +306,7 @@
187
306
 
188
307
  .ui3nMenuContent {
189
308
  position: absolute;
190
- border-radius: var(--ui3n-menu-content-border-raduis);
309
+ border-radius: var(--ui3n-menu-content-border-radius);
191
310
  background-color: var(--ui3n-menu-content-bg);
192
311
  z-index: var(--ui3n-menu-zIndex, 1000);
193
312
  overflow: hidden;