@schukai/monster 4.148.0 → 4.148.1

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 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.148.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.148.1"}
@@ -323,6 +323,8 @@ const lookupCacheSymbol = Symbol("lookupCache");
323
323
  const lookupInProgressSymbol = Symbol("lookupInProgress");
324
324
  const unresolvedSelectionValuesSymbol = Symbol("unresolvedSelectionValues");
325
325
  const fetchRequestVersionSymbol = Symbol("fetchRequestVersion");
326
+ const remoteLifecycleVersionSymbol = Symbol("remoteLifecycleVersion");
327
+ const activeFetchRequestsSymbol = Symbol("activeFetchRequests");
326
328
  const remoteInfoRequestSymbol = Symbol("remoteInfoRequest");
327
329
  const remoteInfoStableMessageSymbol = Symbol("remoteInfoStableMessage");
328
330
 
@@ -419,6 +421,8 @@ class Select extends CustomControl {
419
421
  this[lookupInProgressSymbol] = new Map();
420
422
  this[unresolvedSelectionValuesSymbol] = new Set();
421
423
  this[optionsMapSymbol] = new Map();
424
+ this[remoteLifecycleVersionSymbol] = 0;
425
+ this[activeFetchRequestsSymbol] = new Set();
422
426
  this[closeOnSelectAutoSymbol] = true;
423
427
  initOptionObserver.call(this);
424
428
  }
@@ -720,6 +724,7 @@ class Select extends CustomControl {
720
724
  */
721
725
  reset() {
722
726
  try {
727
+ invalidatePendingRemoteWork.call(this);
723
728
  hide.call(this);
724
729
 
725
730
  // Clear the lookup cache
@@ -963,6 +968,8 @@ class Select extends CustomControl {
963
968
  */
964
969
  disconnectedCallback() {
965
970
  super.disconnectedCallback();
971
+ invalidatePendingRemoteWork.call(this);
972
+ hide.call(this, false);
966
973
  clearPendingOpenIntent.call(this);
967
974
  if (!this[usesHostDismissSymbol]) {
968
975
  const document = getDocument();
@@ -2021,6 +2028,65 @@ function runSelectionLookupWhenVisible(self) {
2021
2028
  function isEmptyLookupValue(value) {
2022
2029
  return isValueIsEmpty.call(this, value);
2023
2030
  }
2031
+
2032
+ function getRemoteLifecycleVersion() {
2033
+ return Number.isInteger(this[remoteLifecycleVersionSymbol])
2034
+ ? this[remoteLifecycleVersionSymbol]
2035
+ : 0;
2036
+ }
2037
+
2038
+ function isRemoteLifecycleCurrent(version) {
2039
+ return getRemoteLifecycleVersion.call(this) === version;
2040
+ }
2041
+
2042
+ function invalidatePendingRemoteWork() {
2043
+ this[remoteLifecycleVersionSymbol] = getRemoteLifecycleVersion.call(this) + 1;
2044
+ this[fetchRequestVersionSymbol] =
2045
+ (Number.isInteger(this[fetchRequestVersionSymbol])
2046
+ ? this[fetchRequestVersionSymbol]
2047
+ : 0) + 1;
2048
+
2049
+ if (this[activeFetchRequestsSymbol] instanceof Set) {
2050
+ this[activeFetchRequestsSymbol].clear();
2051
+ }
2052
+ this[isLoadingSymbol] = false;
2053
+ this[remoteInfoRequestSymbol] = null;
2054
+
2055
+ if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
2056
+ try {
2057
+ this[keyFilterEventSymbol].defuse();
2058
+ } catch (e) {}
2059
+ delete this[keyFilterEventSymbol];
2060
+ }
2061
+ }
2062
+
2063
+ function beginTrackedFetch() {
2064
+ if (!(this[activeFetchRequestsSymbol] instanceof Set)) {
2065
+ this[activeFetchRequestsSymbol] = new Set();
2066
+ }
2067
+
2068
+ const token = {};
2069
+ this[activeFetchRequestsSymbol].add(token);
2070
+ this[isLoadingSymbol] = true;
2071
+ return token;
2072
+ }
2073
+
2074
+ function isTrackedFetchActive(token) {
2075
+ return (
2076
+ this[activeFetchRequestsSymbol] instanceof Set &&
2077
+ this[activeFetchRequestsSymbol].has(token)
2078
+ );
2079
+ }
2080
+
2081
+ function finishTrackedFetch(token) {
2082
+ if (this[activeFetchRequestsSymbol] instanceof Set) {
2083
+ this[activeFetchRequestsSymbol].delete(token);
2084
+ this[isLoadingSymbol] = this[activeFetchRequestsSymbol].size > 0;
2085
+ return;
2086
+ }
2087
+
2088
+ this[isLoadingSymbol] = false;
2089
+ }
2024
2090
  /**
2025
2091
  *
2026
2092
  * @param url
@@ -2029,6 +2095,7 @@ function isEmptyLookupValue(value) {
2029
2095
  */
2030
2096
  function fetchIt(url, controlOptions) {
2031
2097
  const self = this;
2098
+ const lifecycleVersion = getRemoteLifecycleVersion.call(this);
2032
2099
 
2033
2100
  if (url instanceof URL) {
2034
2101
  url = url.toString();
@@ -2066,7 +2133,10 @@ function fetchIt(url, controlOptions) {
2066
2133
  fetchData
2067
2134
  .call(this, url)
2068
2135
  .then((map) => {
2069
- if (requestVersion !== this[fetchRequestVersionSymbol]) {
2136
+ if (
2137
+ requestVersion !== this[fetchRequestVersionSymbol] ||
2138
+ !isRemoteLifecycleCurrent.call(this, lifecycleVersion)
2139
+ ) {
2070
2140
  resolve();
2071
2141
  return;
2072
2142
  }
@@ -2088,6 +2158,13 @@ function fetchIt(url, controlOptions) {
2088
2158
  this[lastFetchedDataSymbol] = map;
2089
2159
 
2090
2160
  queueMicrotask(() => {
2161
+ if (
2162
+ requestVersion !== this[fetchRequestVersionSymbol] ||
2163
+ !isRemoteLifecycleCurrent.call(this, lifecycleVersion)
2164
+ ) {
2165
+ resolve();
2166
+ return;
2167
+ }
2091
2168
  checkOptionState.call(this);
2092
2169
  if (
2093
2170
  getFilterMode.call(this) === FILTER_MODE_REMOTE &&
@@ -2112,7 +2189,10 @@ function fetchIt(url, controlOptions) {
2112
2189
  reject(new Error("invalid response"));
2113
2190
  })
2114
2191
  .catch((e) => {
2115
- if (requestVersion !== this[fetchRequestVersionSymbol]) {
2192
+ if (
2193
+ requestVersion !== this[fetchRequestVersionSymbol] ||
2194
+ !isRemoteLifecycleCurrent.call(this, lifecycleVersion)
2195
+ ) {
2116
2196
  resolve();
2117
2197
  return;
2118
2198
  }
@@ -2125,6 +2205,10 @@ function fetchIt(url, controlOptions) {
2125
2205
  })
2126
2206
  .run()
2127
2207
  .catch((e) => {
2208
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
2209
+ resolve();
2210
+ return;
2211
+ }
2128
2212
  setStatusOrRemoveBadges.call(this, "error");
2129
2213
  addErrorAttribute(this, e);
2130
2214
  clearOptionsOnError.call(this);
@@ -2852,6 +2936,8 @@ function buildSelectionItem(value, preferredLabel) {
2852
2936
  * @returns {Promise<void>}
2853
2937
  */
2854
2938
  async function lookupValueAndCache(value) {
2939
+ const lifecycleVersion = getRemoteLifecycleVersion.call(this);
2940
+ const lookupToken = {};
2855
2941
  const lookupUrl = this.getOption("lookup.url");
2856
2942
  const lookupValue = getSelectionValueLabel.call(this, value);
2857
2943
  const cacheKey = getSelectionCacheKey.call(this, value);
@@ -2875,7 +2961,7 @@ async function lookupValueAndCache(value) {
2875
2961
  let found = false;
2876
2962
  let refreshSelection = false;
2877
2963
  try {
2878
- this[lookupInProgressSymbol].set(value, true);
2964
+ this[lookupInProgressSymbol].set(value, lookupToken);
2879
2965
 
2880
2966
  const markerOpen = this.getOption("filter.marker.open", "{");
2881
2967
  const markerClose = this.getOption("filter.marker.close", "}");
@@ -2885,6 +2971,9 @@ async function lookupValueAndCache(value) {
2885
2971
  );
2886
2972
 
2887
2973
  const data = await fetchData.call(this, url);
2974
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
2975
+ return;
2976
+ }
2888
2977
 
2889
2978
  const mappingOptions = this.getOption("mapping", {});
2890
2979
  const map = buildMap(
@@ -2932,6 +3021,9 @@ async function lookupValueAndCache(value) {
2932
3021
  refreshSelection = true;
2933
3022
  }
2934
3023
  } catch (e) {
3024
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
3025
+ return;
3026
+ }
2935
3027
  hasError = true;
2936
3028
  addErrorAttribute(this, e);
2937
3029
 
@@ -2944,16 +3036,19 @@ async function lookupValueAndCache(value) {
2944
3036
  refreshSelection = true;
2945
3037
  }
2946
3038
  } finally {
2947
- this[lookupInProgressSymbol].delete(value);
2948
-
2949
- if (refreshSelection) {
2950
- await setSelection.call(this, this.getOption("selection"));
3039
+ if (this[lookupInProgressSymbol].get(value) === lookupToken) {
3040
+ this[lookupInProgressSymbol].delete(value);
2951
3041
  }
3042
+ if (isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
3043
+ if (refreshSelection) {
3044
+ await setSelection.call(this, this.getOption("selection"));
3045
+ }
2952
3046
 
2953
- if (hasError) {
2954
- setStatusOrRemoveBadges.call(this, "error");
2955
- } else {
2956
- setStatusOrRemoveBadges.call(this);
3047
+ if (hasError) {
3048
+ setStatusOrRemoveBadges.call(this, "error");
3049
+ } else {
3050
+ setStatusOrRemoveBadges.call(this);
3051
+ }
2957
3052
  }
2958
3053
  }
2959
3054
  }
@@ -3573,11 +3668,14 @@ function resolveSelectViewportMetrics({
3573
3668
  offsetTop = 0,
3574
3669
  padding = SELECT_VIEWPORT_PADDING,
3575
3670
  }) {
3671
+ const hasVisualWidth = Number.isFinite(visualWidth) && visualWidth > 0;
3672
+ const hasVisualHeight = Number.isFinite(visualHeight) && visualHeight > 0;
3673
+
3576
3674
  return {
3577
- width: Math.max(layoutWidth, visualWidth),
3578
- height: Math.max(layoutHeight, visualHeight),
3579
- left: offsetLeft,
3580
- top: offsetTop,
3675
+ width: hasVisualWidth ? visualWidth : Math.max(0, layoutWidth),
3676
+ height: hasVisualHeight ? visualHeight : Math.max(0, layoutHeight),
3677
+ left: hasVisualWidth ? offsetLeft : 0,
3678
+ top: hasVisualHeight ? offsetTop : 0,
3581
3679
  padding,
3582
3680
  };
3583
3681
  }
@@ -3676,6 +3774,7 @@ function getSelectPopperPositionOptions() {
3676
3774
  }
3677
3775
 
3678
3776
  popperOptions.adaptiveSize = false;
3777
+ popperOptions.hideWhenReferenceHidden = true;
3679
3778
 
3680
3779
  if (
3681
3780
  resolveParentPopperContentBoundary(
@@ -4090,7 +4189,12 @@ function filterFromRemoteByValue(
4090
4189
  openPopper,
4091
4190
  formatOptions = {},
4092
4191
  ) {
4192
+ const lifecycleVersion = getRemoteLifecycleVersion.call(this);
4193
+
4093
4194
  return new Processing(() => {
4195
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
4196
+ return;
4197
+ }
4094
4198
  let url = formatURL.call(this, optionUrl, params, formatOptions);
4095
4199
 
4096
4200
  if (url.indexOf(disabledRequestMarker.toString()) !== -1) {
@@ -4104,12 +4208,18 @@ function filterFromRemoteByValue(
4104
4208
  disableHiding: true,
4105
4209
  })
4106
4210
  .then(() => {
4211
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
4212
+ return;
4213
+ }
4107
4214
  checkOptionState.call(this);
4108
4215
  if (openPopper === true) {
4109
4216
  show.call(this);
4110
4217
  }
4111
4218
  })
4112
4219
  .catch((e) => {
4220
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
4221
+ return;
4222
+ }
4113
4223
  if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
4114
4224
  this.setOption("total", null);
4115
4225
  resetPaginationState.call(this);
@@ -4897,31 +5007,24 @@ function setSelection(selection) {
4897
5007
  * @throws {TypeError} unsupported response
4898
5008
  */
4899
5009
  function fetchData(url) {
4900
- const self = this;
4901
5010
  if (!url) url = this.getOption("url");
4902
5011
  if (!url) return Promise.resolve();
4903
5012
 
4904
5013
  const fetchOptions = this.getOption("fetch", {});
4905
-
4906
- let delayWatch = false;
5014
+ const fetchToken = beginTrackedFetch.call(this);
4907
5015
 
4908
5016
  // if fetch short time, do not show loading badge, because of flickering
4909
5017
  requestAnimationFrame(() => {
4910
- if (delayWatch === true) return;
5018
+ if (!isTrackedFetchActive.call(this, fetchToken)) return;
4911
5019
  setStatusOrRemoveBadges.call(this, "loading");
4912
- delayWatch = true;
4913
5020
  });
4914
5021
 
4915
5022
  url = formatURL.call(this, url);
4916
5023
 
4917
- self[isLoadingSymbol] = true;
4918
5024
  const global = getGlobal();
4919
- return global
4920
- .fetch(url, fetchOptions)
5025
+ return Promise.resolve()
5026
+ .then(() => global.fetch(url, fetchOptions))
4921
5027
  .then((response) => {
4922
- self[isLoadingSymbol] = false;
4923
- delayWatch = true;
4924
-
4925
5028
  if (!(response.status >= 200 && response.status < 300)) {
4926
5029
  throw new Error(`HTTP error! status: ${response.status}`);
4927
5030
  }
@@ -4940,23 +5043,24 @@ function fetchData(url) {
4940
5043
  throw new TypeError("the result cannot be parsed, check the URL");
4941
5044
  }
4942
5045
  })
4943
- .catch((e) => {
4944
- self[isLoadingSymbol] = false;
4945
- delayWatch = true;
4946
- throw e;
5046
+ .finally(() => {
5047
+ finishTrackedFetch.call(this, fetchToken);
4947
5048
  });
4948
5049
  }
4949
5050
 
4950
5051
  /**
4951
5052
  * @private
5053
+ * @param {boolean} updateStatus
4952
5054
  */
4953
- function hide() {
5055
+ function hide(updateStatus = true) {
4954
5056
  clearPendingOpenIntent.call(this);
4955
5057
  cancelFloatingLayout(this[popperElementSymbol]);
4956
5058
  resetSelectPopperDimensionStyles.call(this);
4957
5059
  closePositionedPopper(this[popperElementSymbol]);
4958
5060
  unlockControlBarSelectHostWidthAfterOpenPopper.call(this);
4959
- setStatusOrRemoveBadges.call(this, "closed");
5061
+ if (updateStatus) {
5062
+ setStatusOrRemoveBadges.call(this, "closed");
5063
+ }
4960
5064
  removeAttributeToken(this[controlElementSymbol], "class", "open");
4961
5065
  unregisterFromHost.call(this);
4962
5066
  }
@@ -5080,8 +5184,10 @@ function show() {
5080
5184
  * @return {boolean}
5081
5185
  */
5082
5186
  function isInControlBar() {
5083
- return this.closest("monster-control-bar,monster-button-bar") instanceof
5084
- HTMLElement;
5187
+ return (
5188
+ this.closest("monster-control-bar,monster-button-bar") instanceof
5189
+ HTMLElement
5190
+ );
5085
5191
  }
5086
5192
 
5087
5193
  /**
@@ -5138,6 +5244,8 @@ function unregisterFromHost() {
5138
5244
  * @private
5139
5245
  */
5140
5246
  function initTotal() {
5247
+ const lifecycleVersion = getRemoteLifecycleVersion.call(this);
5248
+
5141
5249
  if (getFilterMode.call(this) !== FILTER_MODE_REMOTE) {
5142
5250
  return;
5143
5251
  }
@@ -5169,6 +5277,9 @@ function initTotal() {
5169
5277
  const remoteInfoRequest = getGlobal()
5170
5278
  .fetch(url, fetchOptions)
5171
5279
  .then((response) => {
5280
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
5281
+ return;
5282
+ }
5172
5283
  if (!response.ok) {
5173
5284
  addErrorAttribute(
5174
5285
  this,
@@ -5179,6 +5290,9 @@ function initTotal() {
5179
5290
  return response.text();
5180
5291
  })
5181
5292
  .then((text) => {
5293
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
5294
+ return;
5295
+ }
5182
5296
  if (!text) return;
5183
5297
  try {
5184
5298
  const data = JSON.parse(String(text));
@@ -5189,6 +5303,9 @@ function initTotal() {
5189
5303
  }
5190
5304
  })
5191
5305
  .catch((e) => {
5306
+ if (!isRemoteLifecycleCurrent.call(this, lifecycleVersion)) {
5307
+ return;
5308
+ }
5192
5309
  addErrorAttribute(this, e);
5193
5310
  })
5194
5311
  .finally(() => {
@@ -49,6 +49,9 @@ function enqueueFloatingLayout({
49
49
  }
50
50
 
51
51
  let job = jobs.get(popperElement);
52
+ if (job?.cancelled === true) {
53
+ return Promise.resolve();
54
+ }
52
55
  if (!job) {
53
56
  job = createJob(popperElement);
54
57
  jobs.set(popperElement, job);
@@ -84,8 +87,12 @@ function cancelFloatingLayout(popperElement) {
84
87
  }
85
88
 
86
89
  job.cancelled = true;
90
+ job.pending = false;
91
+ job.reasons = 0;
87
92
  job.resolve();
88
- jobs.delete(popperElement);
93
+ if (!job.running) {
94
+ jobs.delete(popperElement);
95
+ }
89
96
  }
90
97
 
91
98
  function flushFloatingLayoutQueueForTests() {
@@ -175,21 +182,25 @@ async function flushQueue() {
175
182
  }
176
183
 
177
184
  flushing = true;
178
- const currentJobs = Array.from(jobs.values());
179
-
180
- for (const job of currentJobs) {
181
- await flushJob(job);
182
- }
183
-
184
- flushing = false;
185
+ try {
186
+ const currentJobs = Array.from(jobs.values());
185
187
 
186
- if (Array.from(jobs.values()).some((job) => job.pending && !job.cancelled)) {
187
- for (const job of jobs.values()) {
188
- if (job.pending) {
189
- job.pending = false;
188
+ for (const job of currentJobs) {
189
+ await flushJob(job);
190
+ }
191
+ } finally {
192
+ flushing = false;
193
+
194
+ if (
195
+ Array.from(jobs.values()).some((job) => job.pending && !job.cancelled)
196
+ ) {
197
+ for (const job of jobs.values()) {
198
+ if (job.pending) {
199
+ job.pending = false;
200
+ }
190
201
  }
202
+ scheduleQueueFlush();
191
203
  }
192
- scheduleQueueFlush();
193
204
  }
194
205
  }
195
206
 
@@ -227,14 +238,23 @@ async function flushJob(job) {
227
238
  }
228
239
  } finally {
229
240
  job.running = false;
230
- recordLayoutSignature(job);
231
- if (job.pending && shouldSuppressOscillatingPendingLayout(job)) {
241
+ if (job.cancelled) {
232
242
  job.pending = false;
233
243
  job.reasons = 0;
234
- }
235
- if (!job.pending && job.reasons === 0) {
236
244
  job.resolve();
237
- jobs.delete(job.popperElement);
245
+ if (jobs.get(job.popperElement) === job) {
246
+ jobs.delete(job.popperElement);
247
+ }
248
+ } else {
249
+ recordLayoutSignature(job);
250
+ if (job.pending && shouldSuppressOscillatingPendingLayout(job)) {
251
+ job.pending = false;
252
+ job.reasons = 0;
253
+ }
254
+ if (!job.pending && job.reasons === 0) {
255
+ job.resolve();
256
+ jobs.delete(job.popperElement);
257
+ }
238
258
  }
239
259
  }
240
260
  }
@@ -33,6 +33,7 @@ import {
33
33
 
34
34
  export {
35
35
  applyAdaptiveFloatingElementSize,
36
+ applyFloatingReferenceVisibility,
36
37
  closePositionedPopper,
37
38
  createVisibilityRecoveryConfig,
38
39
  getFloatingVisibleRatio,
@@ -106,8 +107,7 @@ function enableFloatingPositioning(controlElement, popperElement, config) {
106
107
 
107
108
  enqueueFloatingLayout({
108
109
  popperElement,
109
- reason:
110
- FLOATING_LAYOUT_REASON.POSITION | FLOATING_LAYOUT_REASON.RESIZE,
110
+ reason: FLOATING_LAYOUT_REASON.POSITION | FLOATING_LAYOUT_REASON.RESIZE,
111
111
  isActive: () => isPositionedPopperOpen(popperElement),
112
112
  position: () => {
113
113
  runFloatingUpdateHook(popperElement);
@@ -177,6 +177,13 @@ function syncFloatingPopover(
177
177
  if (!isPositionedPopperOpen(popperElement)) {
178
178
  return;
179
179
  }
180
+ if (
181
+ config.hideWhenReferenceHidden === true &&
182
+ !applyFloatingReferenceVisibility(controlElement, popperElement)
183
+ ) {
184
+ visibilityRecoveryMap.delete(popperElement);
185
+ return;
186
+ }
180
187
 
181
188
  Object.assign(popperElement.style, {
182
189
  top: "0",
@@ -202,12 +209,49 @@ function syncFloatingPopover(
202
209
  });
203
210
  }
204
211
 
212
+ function applyFloatingReferenceVisibility(referenceElement, popperElement) {
213
+ if (!(popperElement instanceof HTMLElement)) {
214
+ return true;
215
+ }
216
+
217
+ const referenceRect =
218
+ referenceElement instanceof HTMLElement
219
+ ? referenceElement.getBoundingClientRect()
220
+ : null;
221
+ const referenceArea = referenceRect
222
+ ? referenceRect.width * referenceRect.height
223
+ : 0;
224
+ let referenceHidden = false;
225
+ if (referenceElement instanceof HTMLElement && referenceArea > 0) {
226
+ referenceHidden = !getVisibleElementRect(referenceElement, referenceRect);
227
+ } else if (referenceElement instanceof HTMLElement) {
228
+ const referenceStyle = getComputedStyle(referenceElement);
229
+ referenceHidden =
230
+ referenceStyle.display === "none" ||
231
+ referenceStyle.visibility === "hidden";
232
+ }
233
+
234
+ if (referenceHidden) {
235
+ popperElement.dataset.monsterReferenceHidden = "true";
236
+ popperElement.style.visibility = "hidden";
237
+ return false;
238
+ }
239
+
240
+ if (popperElement.dataset.monsterReferenceHidden === "true") {
241
+ delete popperElement.dataset.monsterReferenceHidden;
242
+ popperElement.style.removeProperty("visibility");
243
+ }
244
+
245
+ return true;
246
+ }
247
+
205
248
  function closePositionedPopper(popperElement) {
206
249
  cancelFloatingLayout(popperElement);
207
250
  stopAutoUpdate(popperElement);
208
251
  cancelFloatingAppearanceFrame(popperElement);
209
252
  visibilityRecoveryMap.delete(popperElement);
210
253
  delete popperElement.dataset.monsterAppearance;
254
+ delete popperElement.dataset.monsterReferenceHidden;
211
255
  popperElement.style.display = "none";
212
256
  popperElement.style.removeProperty("visibility");
213
257
  popperElement.style.removeProperty("position");
@@ -226,6 +270,7 @@ function normalizePopperConfig(options, controlElement, popperElement) {
226
270
  engine: "floating",
227
271
  strategy: "absolute",
228
272
  adaptiveSize: true,
273
+ hideWhenReferenceHidden: false,
229
274
  },
230
275
  options,
231
276
  );
@@ -423,7 +468,7 @@ function getFloatingVisibleRatio(floatingElement) {
423
468
  return 1;
424
469
  }
425
470
 
426
- const visibleRect = getVisibleFloatingRect(floatingElement, rect);
471
+ const visibleRect = getVisibleElementRect(floatingElement, rect);
427
472
  if (!visibleRect) {
428
473
  return 0;
429
474
  }
@@ -432,13 +477,26 @@ function getFloatingVisibleRatio(floatingElement) {
432
477
  return Math.max(0, Math.min(1, visibleArea / area));
433
478
  }
434
479
 
435
- function getVisibleFloatingRect(floatingElement, rect) {
480
+ function getVisibleElementRect(element, rect) {
436
481
  let visibleRect = normalizeRect(rect);
482
+ const visualViewport = window.visualViewport;
483
+ const viewportLeft = visualViewport?.offsetLeft || 0;
484
+ const viewportTop = visualViewport?.offsetTop || 0;
485
+ const viewportWidth =
486
+ visualViewport?.width ||
487
+ window.innerWidth ||
488
+ document.documentElement.clientWidth ||
489
+ 0;
490
+ const viewportHeight =
491
+ visualViewport?.height ||
492
+ window.innerHeight ||
493
+ document.documentElement.clientHeight ||
494
+ 0;
437
495
  const viewportRect = {
438
- top: 0,
439
- left: 0,
440
- right: window.innerWidth || document.documentElement.clientWidth || 0,
441
- bottom: window.innerHeight || document.documentElement.clientHeight || 0,
496
+ top: viewportTop,
497
+ left: viewportLeft,
498
+ right: viewportLeft + viewportWidth,
499
+ bottom: viewportTop + viewportHeight,
442
500
  };
443
501
  viewportRect.width = Math.max(0, viewportRect.right - viewportRect.left);
444
502
  viewportRect.height = Math.max(0, viewportRect.bottom - viewportRect.top);
@@ -448,7 +506,7 @@ function getVisibleFloatingRect(floatingElement, rect) {
448
506
  return null;
449
507
  }
450
508
 
451
- for (const clippingContainer of getFloatingClippingContainers(floatingElement)) {
509
+ for (const clippingContainer of getFloatingClippingContainers(element)) {
452
510
  visibleRect = intersectRects(
453
511
  visibleRect,
454
512
  normalizeRect(clippingContainer.getBoundingClientRect()),
@@ -483,9 +541,7 @@ function normalizeRect(rect) {
483
541
  const left = Number.isFinite(rect.left) ? rect.left : rect.x || 0;
484
542
  const top = Number.isFinite(rect.top) ? rect.top : rect.y || 0;
485
543
  const right = Number.isFinite(rect.right) ? rect.right : left + rect.width;
486
- const bottom = Number.isFinite(rect.bottom)
487
- ? rect.bottom
488
- : top + rect.height;
544
+ const bottom = Number.isFinite(rect.bottom) ? rect.bottom : top + rect.height;
489
545
 
490
546
  return {
491
547
  top,
@@ -5,6 +5,7 @@ let expect = chai.expect;
5
5
 
6
6
  let resolveClippingBoundaryElement;
7
7
  let applyAdaptiveFloatingElementSize;
8
+ let applyFloatingReferenceVisibility;
8
9
  let createVisibilityRecoveryConfig;
9
10
  let getFloatingVisibleRatio;
10
11
 
@@ -19,6 +20,7 @@ describe("form floating-ui boundary resolution", function () {
19
20
  .then((m) => {
20
21
  resolveClippingBoundaryElement = m.resolveClippingBoundaryElement;
21
22
  applyAdaptiveFloatingElementSize = m.applyAdaptiveFloatingElementSize;
23
+ applyFloatingReferenceVisibility = m.applyFloatingReferenceVisibility;
22
24
  createVisibilityRecoveryConfig = m.createVisibilityRecoveryConfig;
23
25
  getFloatingVisibleRatio = m.getFloatingVisibleRatio;
24
26
  done();
@@ -169,7 +171,8 @@ describe("form floating-ui boundary resolution", function () {
169
171
 
170
172
  popper.style.maxHeight = "300px";
171
173
  content.setAttribute("part", "content");
172
- content.textContent = "A long help text that still needs one readable line.";
174
+ content.textContent =
175
+ "A long help text that still needs one readable line.";
173
176
  content.style.fontSize = "16px";
174
177
  content.style.lineHeight = "24px";
175
178
  popper.appendChild(content);
@@ -476,4 +479,48 @@ describe("form floating-ui boundary resolution", function () {
476
479
  "arrow",
477
480
  ]);
478
481
  });
482
+
483
+ it("should hide a floating element while its reference is clipped", function () {
484
+ const reference = document.createElement("div");
485
+ const popper = document.createElement("div");
486
+ reference.getBoundingClientRect = () => ({
487
+ top: -100,
488
+ left: 10,
489
+ right: 110,
490
+ bottom: -50,
491
+ width: 100,
492
+ height: 50,
493
+ x: 10,
494
+ y: -100,
495
+ });
496
+
497
+ const visible = applyFloatingReferenceVisibility(reference, popper);
498
+
499
+ expect(visible).to.equal(false);
500
+ expect(popper.style.visibility).to.equal("hidden");
501
+ expect(popper.dataset.monsterReferenceHidden).to.equal("true");
502
+ });
503
+
504
+ it("should restore a floating element when its reference is visible again", function () {
505
+ const reference = document.createElement("div");
506
+ const popper = document.createElement("div");
507
+ reference.getBoundingClientRect = () => ({
508
+ top: 10,
509
+ left: 10,
510
+ right: 110,
511
+ bottom: 60,
512
+ width: 100,
513
+ height: 50,
514
+ x: 10,
515
+ y: 10,
516
+ });
517
+ popper.dataset.monsterReferenceHidden = "true";
518
+ popper.style.visibility = "hidden";
519
+
520
+ const visible = applyFloatingReferenceVisibility(reference, popper);
521
+
522
+ expect(visible).to.equal(true);
523
+ expect(popper.style.visibility).to.equal("");
524
+ expect(popper.dataset.monsterReferenceHidden).to.equal(undefined);
525
+ });
479
526
  });
@@ -86,6 +86,17 @@ function waitForCondition(check, {timeout = 4000, interval = 25} = {}) {
86
86
  });
87
87
  }
88
88
 
89
+ function configureRemotePaginatedSelect(select) {
90
+ select.setOption('url', 'https://example.com/items?filter={filter}&page={page}');
91
+ select.setOption('filter.mode', 'remote');
92
+ select.setOption('mapping.selector', 'items.*');
93
+ select.setOption('mapping.labelTemplate', '${name}');
94
+ select.setOption('mapping.valueTemplate', '${id}');
95
+ select.setOption('mapping.total', 'pagination.total');
96
+ select.setOption('mapping.currentPage', 'pagination.page');
97
+ select.setOption('mapping.objectsPerPage', 'pagination.perPage');
98
+ }
99
+
89
100
  let Select,
90
101
  SelectStyleSheet,
91
102
  getDefaultSelectPopperPositionProfile,
@@ -397,6 +408,46 @@ describe('Select', function () {
397
408
  }
398
409
  });
399
410
 
411
+ it('should keep the floating layout queue usable after cancelling a running reentrant job', async function () {
412
+ const cancelledPopper = document.createElement('div');
413
+ const nextPopper = document.createElement('div');
414
+ const releasePosition = createDeferred();
415
+ let positionStarted = false;
416
+ let positionFinished = false;
417
+ let nextMutationCount = 0;
418
+
419
+ enqueueFloatingLayout({
420
+ popperElement: cancelledPopper,
421
+ reason: FLOATING_LAYOUT_REASON.POSITION,
422
+ position: async () => {
423
+ positionStarted = true;
424
+ await releasePosition.promise;
425
+ await enqueueFloatingLayout({
426
+ popperElement: cancelledPopper,
427
+ reason: FLOATING_LAYOUT_REASON.SETTLE
428
+ });
429
+ positionFinished = true;
430
+ }
431
+ });
432
+
433
+ await waitForCondition(() => positionStarted === true);
434
+ cancelFloatingLayout(cancelledPopper);
435
+ releasePosition.resolve();
436
+ await waitForCondition(() => positionFinished === true);
437
+ await new Promise(resolve => setTimeout(resolve, 0));
438
+
439
+ enqueueFloatingLayout({
440
+ popperElement: nextPopper,
441
+ reason: FLOATING_LAYOUT_REASON.POSITION,
442
+ mutate: () => {
443
+ nextMutationCount += 1;
444
+ }
445
+ });
446
+ await flushFloatingLayoutQueueForTests();
447
+
448
+ expect(nextMutationCount).to.equal(1);
449
+ });
450
+
400
451
  it('should flush reentrant floating layout queue jobs through the watchdog', async function () {
401
452
  const originalRequestAnimationFrame = global.requestAnimationFrame;
402
453
  const originalCancelAnimationFrame = global.cancelAnimationFrame;
@@ -738,7 +789,7 @@ describe('Select', function () {
738
789
  expect(popper.style.display).to.equal('block');
739
790
  });
740
791
 
741
- it('should allow the popper to become wider than a narrow control', function (done) {
792
+ it('should allow the popper to become wider than a narrow control', async function () {
742
793
  const mocks = document.getElementById('mocks');
743
794
  const select = document.createElement('monster-select');
744
795
 
@@ -764,23 +815,12 @@ describe('Select', function () {
764
815
  y: 100
765
816
  });
766
817
 
767
- setTimeout(() => {
768
- try {
769
- shadowRoot.querySelector('[data-monster-role=container]').click();
770
- setTimeout(() => {
771
- try {
772
- expect(popper.style.minWidth).to.equal('240px');
773
- expect(popper.dataset.monsterWidthBehavior).to.equal('preferred');
774
- expect(popper.dataset.monsterPreferredWidth).to.equal('240');
775
- done();
776
- } catch (e) {
777
- done(e);
778
- }
779
- }, 80);
780
- } catch (e) {
781
- done(e);
782
- }
783
- }, 20);
818
+ await new Promise(resolve => setTimeout(resolve, 20));
819
+ shadowRoot.querySelector('[data-monster-role=container]').click();
820
+ await waitForCondition(() => popper.style.minWidth === '240px');
821
+
822
+ expect(popper.dataset.monsterWidthBehavior).to.equal('preferred');
823
+ expect(popper.dataset.monsterPreferredWidth).to.equal('240');
784
824
  });
785
825
 
786
826
  it('should use fixed positioning inside a control bar', function (done) {
@@ -1136,12 +1176,48 @@ describe('Select', function () {
1136
1176
  ]);
1137
1177
  });
1138
1178
 
1139
- it('should prefer the larger live viewport metrics after a resize', function () {
1179
+ it('should use the reduced visual viewport while a soft keyboard is open', function () {
1180
+ const result = resolveSelectViewportMetrics({
1181
+ layoutWidth: 390,
1182
+ layoutHeight: 844,
1183
+ visualWidth: 390,
1184
+ visualHeight: 480,
1185
+ offsetLeft: 0,
1186
+ offsetTop: 0,
1187
+ padding: 12
1188
+ });
1189
+
1190
+ expect(result.width).to.equal(390);
1191
+ expect(result.height).to.equal(480);
1192
+ expect(result.left).to.equal(0);
1193
+ expect(result.top).to.equal(0);
1194
+ expect(result.padding).to.equal(12);
1195
+ });
1196
+
1197
+ it('should preserve visual viewport offsets for zoomed mobile layouts', function () {
1198
+ const result = resolveSelectViewportMetrics({
1199
+ layoutWidth: 1200,
1200
+ layoutHeight: 900,
1201
+ visualWidth: 600,
1202
+ visualHeight: 450,
1203
+ offsetLeft: 20,
1204
+ offsetTop: 30,
1205
+ padding: 12
1206
+ });
1207
+
1208
+ expect(result.width).to.equal(600);
1209
+ expect(result.height).to.equal(450);
1210
+ expect(result.left).to.equal(20);
1211
+ expect(result.top).to.equal(30);
1212
+ expect(result.padding).to.equal(12);
1213
+ });
1214
+
1215
+ it('should fall back to layout viewport metrics without a visual viewport', function () {
1140
1216
  const result = resolveSelectViewportMetrics({
1141
1217
  layoutWidth: 1400,
1142
1218
  layoutHeight: 900,
1143
- visualWidth: 1024,
1144
- visualHeight: 700,
1219
+ visualWidth: 0,
1220
+ visualHeight: 0,
1145
1221
  offsetLeft: 20,
1146
1222
  offsetTop: 30,
1147
1223
  padding: 12
@@ -1149,8 +1225,8 @@ describe('Select', function () {
1149
1225
 
1150
1226
  expect(result.width).to.equal(1400);
1151
1227
  expect(result.height).to.equal(900);
1152
- expect(result.left).to.equal(20);
1153
- expect(result.top).to.equal(30);
1228
+ expect(result.left).to.equal(0);
1229
+ expect(result.top).to.equal(0);
1154
1230
  expect(result.padding).to.equal(12);
1155
1231
  });
1156
1232
 
@@ -1487,6 +1563,142 @@ describe('Select', function () {
1487
1563
  .catch((e) => done(e));
1488
1564
  }, 50);
1489
1565
  });
1566
+
1567
+ it('should ignore a remote page response that settles after reset', async function () {
1568
+ this.timeout(5000);
1569
+
1570
+ const deferredResponse = createDeferred();
1571
+ let requestStarted = false;
1572
+ global['fetch'] = function () {
1573
+ requestStarted = true;
1574
+ return deferredResponse.promise;
1575
+ };
1576
+
1577
+ const mocks = document.getElementById('mocks');
1578
+ const select = document.createElement('monster-select');
1579
+ configureRemotePaginatedSelect(select);
1580
+ mocks.appendChild(select);
1581
+
1582
+ const request = select.fetch('https://example.com/items?filter=old&page=3');
1583
+ await waitForCondition(() => requestStarted === true);
1584
+
1585
+ select.reset();
1586
+ await waitForCondition(() => {
1587
+ return select.getOption('options').length === 0 && select.getOption('total') === null;
1588
+ });
1589
+
1590
+ deferredResponse.resolve(
1591
+ await createJsonResponse({
1592
+ items: [{id: 'old-3', name: 'Old page 3'}],
1593
+ pagination: {
1594
+ total: 9,
1595
+ page: 3,
1596
+ perPage: 1
1597
+ }
1598
+ })
1599
+ );
1600
+ await request;
1601
+ await new Promise(resolve => setTimeout(resolve, 50));
1602
+
1603
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
1604
+ expect(select.getOption('options')).to.deep.equal([]);
1605
+ expect(select.getOption('total')).to.equal(null);
1606
+ expect(pagination.style.display).to.equal('none');
1607
+ expect(pagination.getOption('currentPage')).to.equal(null);
1608
+ expect(pagination.getOption('pages')).to.equal(null);
1609
+ });
1610
+
1611
+ it('should ignore a remote page response after disconnect and reconnect', async function () {
1612
+ this.timeout(5000);
1613
+
1614
+ const deferredResponse = createDeferred();
1615
+ let requestStarted = false;
1616
+ global['fetch'] = function () {
1617
+ requestStarted = true;
1618
+ return deferredResponse.promise;
1619
+ };
1620
+
1621
+ const mocks = document.getElementById('mocks');
1622
+ const select = document.createElement('monster-select');
1623
+ configureRemotePaginatedSelect(select);
1624
+ mocks.appendChild(select);
1625
+
1626
+ const request = select.fetch('https://example.com/items?filter=old&page=2');
1627
+ await waitForCondition(() => requestStarted === true);
1628
+
1629
+ select.remove();
1630
+ deferredResponse.resolve(
1631
+ await createJsonResponse({
1632
+ items: [{id: 'old-2', name: 'Old page 2'}],
1633
+ pagination: {
1634
+ total: 4,
1635
+ page: 2,
1636
+ perPage: 1
1637
+ }
1638
+ })
1639
+ );
1640
+ await request;
1641
+ mocks.appendChild(select);
1642
+ await new Promise(resolve => setTimeout(resolve, 50));
1643
+
1644
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
1645
+ expect(select.getOption('options')).to.deep.equal([]);
1646
+ expect(select.getOption('total')).to.equal(null);
1647
+ expect(pagination.style.display).to.equal('none');
1648
+ expect(pagination.getOption('currentPage')).to.equal(null);
1649
+ expect(pagination.getOption('pages')).to.equal(null);
1650
+ });
1651
+
1652
+ it('should keep the newest remote page when responses settle out of order', async function () {
1653
+ this.timeout(5000);
1654
+
1655
+ const page2Response = createDeferred();
1656
+ const page3Response = createDeferred();
1657
+ const requests = [];
1658
+ global['fetch'] = function (url) {
1659
+ const requestUrl = String(url);
1660
+ requests.push(requestUrl);
1661
+ return requestUrl.includes('page=2') ? page2Response.promise : page3Response.promise;
1662
+ };
1663
+
1664
+ const mocks = document.getElementById('mocks');
1665
+ const select = document.createElement('monster-select');
1666
+ configureRemotePaginatedSelect(select);
1667
+ mocks.appendChild(select);
1668
+
1669
+ const request2 = select.fetch('https://example.com/items?filter=all&page=2');
1670
+ const request3 = select.fetch('https://example.com/items?filter=all&page=3');
1671
+ await waitForCondition(() => requests.length === 2);
1672
+
1673
+ page3Response.resolve(
1674
+ await createJsonResponse({
1675
+ items: [{id: 'page-3', name: 'Page 3'}],
1676
+ pagination: {
1677
+ total: 3,
1678
+ page: 3,
1679
+ perPage: 1
1680
+ }
1681
+ })
1682
+ );
1683
+ await request3;
1684
+
1685
+ page2Response.resolve(
1686
+ await createJsonResponse({
1687
+ items: [{id: 'page-2', name: 'Page 2'}],
1688
+ pagination: {
1689
+ total: 3,
1690
+ page: 2,
1691
+ perPage: 1
1692
+ }
1693
+ })
1694
+ );
1695
+ await request2;
1696
+
1697
+ const pagination = select.shadowRoot.querySelector('[data-monster-role=pagination]');
1698
+ expect(select.getOption('options').map(option => option.value)).to.deep.equal(['page-3']);
1699
+ expect(pagination.getOption('currentPage')).to.equal(3);
1700
+ expect(pagination.getOption('pages')).to.equal(3);
1701
+ });
1490
1702
  });
1491
1703
 
1492
1704
  describe('document.createElement()', function () {
@@ -2488,6 +2700,47 @@ describe('Select', function () {
2488
2700
  expect(requests).to.have.length(1);
2489
2701
  });
2490
2702
 
2703
+ it('should cancel a pending remote filter request on reset', async function () {
2704
+ this.timeout(5000);
2705
+
2706
+ const mocks = document.getElementById('mocks');
2707
+ const requests = [];
2708
+ global['fetch'] = function (url) {
2709
+ requests.push(String(url));
2710
+ return createJsonResponse({
2711
+ items: [{id: 'alpha', name: 'Alpha'}],
2712
+ pagination: {
2713
+ total: 1,
2714
+ page: 1,
2715
+ perPage: 1
2716
+ }
2717
+ });
2718
+ };
2719
+
2720
+ const select = document.createElement('monster-select');
2721
+ configureRemotePaginatedSelect(select);
2722
+ select.setOption('filter.position', 'popper');
2723
+ mocks.appendChild(select);
2724
+
2725
+ await waitForCondition(() => {
2726
+ return select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]') instanceof HTMLInputElement;
2727
+ });
2728
+
2729
+ const filterInput = select.shadowRoot.querySelector('[data-monster-role=filter][name="popper-filter"]');
2730
+ filterInput.value = 'alpha';
2731
+ filterInput.dispatchEvent(new Event('input', {
2732
+ bubbles: true,
2733
+ composed: true
2734
+ }));
2735
+ select.reset();
2736
+
2737
+ await new Promise(resolve => setTimeout(resolve, 260));
2738
+
2739
+ expect(requests).to.deep.equal([]);
2740
+ expect(select.getOption('options')).to.deep.equal([]);
2741
+ expect(select.getOption('total')).to.equal(null);
2742
+ });
2743
+
2491
2744
  it('should keep unresolved lookup values visible and mark their badge', function (done) {
2492
2745
  this.timeout(3000);
2493
2746