@7365admin1/layer-common 4.73.1-staging.463 → 4.73.1-staging.465

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.
@@ -818,6 +818,20 @@ select option {
818
818
  text-align: center;
819
819
  }
820
820
 
821
+ /* The footer of a paging AppSelect. Same voice as the empty state - it is the
822
+ same kind of message about the list rather than an option in it - with a rule
823
+ above so it does not read as one more row to click. */
824
+ .app-select__loading {
825
+ margin: 6px 0 0;
826
+ padding: 9px 8px;
827
+ border-top: 1px solid var(--border);
828
+ font-family: var(--font-sans);
829
+ font-size: var(--fs-cell);
830
+ font-weight: 600;
831
+ color: var(--muted);
832
+ text-align: center;
833
+ }
834
+
821
835
  .app-select__option {
822
836
  display: flex;
823
837
  align-items: center;
@@ -53,7 +53,12 @@
53
53
  </div>
54
54
  </template>
55
55
 
56
- <div class="app-select__menu" role="listbox">
56
+ <div
57
+ ref="singleMenu"
58
+ class="app-select__menu"
59
+ role="listbox"
60
+ @scroll.passive="onMenuScroll"
61
+ >
57
62
  <div v-if="searchable" class="app-select__search">
58
63
  <v-icon icon="mdi-magnify" size="16" />
59
64
  <input
@@ -83,6 +88,11 @@
83
88
  <p v-if="searchable && !visibleOptions.length" class="app-select__empty">
84
89
  {{ normalized.length ? "No matches" : "Nothing to choose from" }}
85
90
  </p>
91
+ <!-- In the list, not under the field: the wait belongs where the reader's
92
+ eye already is, at the end of what they just scrolled through. -->
93
+ <p v-if="loadingMore" class="app-select__loading" role="status">
94
+ Loading more...
95
+ </p>
86
96
  </div>
87
97
  </v-menu>
88
98
 
@@ -184,6 +194,34 @@ defineOptions({ inheritAttrs: false });
184
194
  */
185
195
  const model = defineModel<any>({ default: "" });
186
196
 
197
+ /**
198
+ * ASKING FOR THE NEXT PAGE, FROM INSIDE THE LIST.
199
+ *
200
+ * A dropdown that pages should load as it is scrolled - a button under the
201
+ * field asks the reader to leave the list they are reading to fetch more of it.
202
+ * The menu is already the scroll container (`max-height: 280px; overflow-y:
203
+ * auto` in `primitives.css`), so the end of the list is the natural trigger.
204
+ */
205
+ const emit = defineEmits<{ (e: "load-more"): void }>();
206
+
207
+ /** How close to the end counts as the end. About one row and a half. */
208
+ const LOAD_MORE_SLACK_PX = 48;
209
+
210
+ const singleMenu = ref<HTMLElement | null>(null);
211
+ /* Scroll fires per pixel. This makes the ask happen once per ARRIVAL at the
212
+ end, not once per event, and rearms when the list grows under the reader. */
213
+ const askedAtBottom = ref(false);
214
+
215
+ function onMenuScroll(event: Event) {
216
+ const el = event.target as HTMLElement | null;
217
+ if (!el || !props.hasMore || props.loadingMore) return;
218
+ const near = el.scrollHeight - el.scrollTop - el.clientHeight <= LOAD_MORE_SLACK_PX;
219
+ if (near && !askedAtBottom.value) emit("load-more");
220
+ askedAtBottom.value = near;
221
+ }
222
+
223
+
224
+
187
225
  const props = defineProps({
188
226
  label: { type: String, default: "" },
189
227
  /** Strings, or the `{ title, value }` shape `v-select` already takes. */
@@ -204,6 +242,15 @@ const props = defineProps({
204
242
  * enough to read does not want a search field in front of it.
205
243
  */
206
244
  searchable: { type: Boolean, default: false },
245
+ /**
246
+ * MORE OPTIONS EXIST THAN ARE LOADED - see the `load-more` emit.
247
+ *
248
+ * Opt-in, and single-select only for now. A caller that leaves these alone
249
+ * behaves exactly as it did: no footer, no listener, no emit.
250
+ */
251
+ hasMore: { type: Boolean, default: false },
252
+ /** A page is on its way; shows the footer and stops further asking. */
253
+ loadingMore: { type: Boolean, default: false },
207
254
  searchPlaceholder: { type: String, default: "Search" },
208
255
  });
209
256
 
@@ -231,6 +278,24 @@ const visibleOptions = computed(() => {
231
278
  );
232
279
  });
233
280
 
281
+ /*
282
+ * A page that does not fill the menu leaves nothing to scroll, and the reader
283
+ * would be stranded with no way to reach the rest. That is not hypothetical
284
+ * here: a page of ten whose rows are mostly filtered out by the caller can draw
285
+ * four options in a box built for eight. So whenever the list settles short of
286
+ * its own container, the next page is asked for straight away.
287
+ */
288
+ watch(
289
+ () => [visibleOptions.value.length, props.hasMore, props.loadingMore, singleOpen.value],
290
+ async () => {
291
+ if (!props.hasMore || props.loadingMore || !singleOpen.value) return;
292
+ await nextTick();
293
+ const el = singleMenu.value;
294
+ if (el && el.scrollHeight <= el.clientHeight) emit("load-more");
295
+ },
296
+ { flush: "post" },
297
+ );
298
+
234
299
  /* A stale term would hide the list the next time the popover opens, so it is
235
300
  dropped on every open - and the caret goes into the box, since opening a
236
301
  searchable select is almost always a prelude to typing. */