@schukai/monster 5.0.1 → 5.1.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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/source/components/form/control-bar.mjs +135 -9
- package/source/components/style/surface.css +2 -0
- package/source/components/style/surface.pcss +33 -0
- package/source/components/stylesheet/surface.mjs +38 -0
- package/source/types/version.mjs +1 -1
- package/test/cases/components/form/button-bar.mjs +100 -0
- package/test/cases/components/form/control-bar.mjs +223 -0
- package/test/cases/components/style/surface.mjs +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [5.1.1] - 2026-10-11
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **control-bar:** use container space for intrinsically sized hosts ([#603](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/603))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [5.1.0] - 2026-10-11
|
|
14
|
+
|
|
15
|
+
### Add Features
|
|
16
|
+
|
|
17
|
+
- **style:** add composable content surfaces ([#598](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/598))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## [5.0.1] - 2026-10-11
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
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.","exports":{".":"./source/monster.mjs","./CHANGELOG.md":"./CHANGELOG.md","./LICENSE":"./LICENSE","./README.md":"./README.md","./documentation/migrations/monster-5-options.md":"./documentation/migrations/monster-5-options.md","./package.json":"./package.json","./source/*":"./source/*","./test/*":"./test/*"},"files":["source","test/cases","test/util","documentation/migrations/monster-5-options.md","CHANGELOG.md"],"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"},"sideEffects":["./source/monster.mjs","./source/components/**","./source/i18n/**","./test/**"],"type":"module","version":"5.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","exports":{".":"./source/monster.mjs","./CHANGELOG.md":"./CHANGELOG.md","./LICENSE":"./LICENSE","./README.md":"./README.md","./documentation/migrations/monster-5-options.md":"./documentation/migrations/monster-5-options.md","./package.json":"./package.json","./source/*":"./source/*","./test/*":"./test/*"},"files":["source","test/cases","test/util","documentation/migrations/monster-5-options.md","CHANGELOG.md"],"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"},"sideEffects":["./source/monster.mjs","./source/components/**","./source/i18n/**","./test/**"],"type":"module","version":"5.1.1"}
|
|
@@ -262,6 +262,14 @@ const VISUAL_BORDER_CONTROL_SELECTOR = [
|
|
|
262
262
|
* - `monster-control-bar-layout-changed` is emitted when responsive layout state changes.
|
|
263
263
|
* - The event detail contains `{alignment, configuredAlignment, stacked, stackedAlignment, stackedBreakpoint, stackedBreakpointContainer}`.
|
|
264
264
|
*
|
|
265
|
+
* Available width:
|
|
266
|
+
* - The bar lays out its controls in the content width of its parent element.
|
|
267
|
+
* - A host that is narrower than its parent limits that width only when it cannot grow,
|
|
268
|
+
* for example because of an explicit `width`, `inline-size` or `max-width`.
|
|
269
|
+
* - A host whose width follows its content (grid item with `justify-self`, `auto` grid column,
|
|
270
|
+
* flex item without `flex-grow`) does not limit the width; its controls stay in the main row
|
|
271
|
+
* as long as the parent offers enough room.
|
|
272
|
+
*
|
|
265
273
|
* @fires monster-control-bar-layout-changed
|
|
266
274
|
*/
|
|
267
275
|
class ControlBar extends CustomElement {
|
|
@@ -1807,15 +1815,6 @@ function calculateControlBarDimensions() {
|
|
|
1807
1815
|
);
|
|
1808
1816
|
}
|
|
1809
1817
|
|
|
1810
|
-
const hostContentWidth = getElementContentWidth(this);
|
|
1811
|
-
if (hostContentWidth > 0) {
|
|
1812
|
-
const measuredSpace = this[dimensionsSymbol].getVia("data.space", 0);
|
|
1813
|
-
this[dimensionsSymbol].setVia(
|
|
1814
|
-
"data.space",
|
|
1815
|
-
Math.min(measuredSpace, hostContentWidth),
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
1818
|
this[dimensionsSymbol].setVia("data.visible", !(containerWidth === 0));
|
|
1820
1819
|
this[dimensionsSymbol].setVia("data.containerWidth", containerWidth);
|
|
1821
1820
|
this[dimensionsSymbol].setVia(
|
|
@@ -1885,10 +1884,137 @@ function calculateControlBarDimensions() {
|
|
|
1885
1884
|
this[dimensionsSymbol].setVia("data.switchWidth", switchWidth);
|
|
1886
1885
|
}
|
|
1887
1886
|
|
|
1887
|
+
limitSpaceToHostCapacity.call(this, itemReferences);
|
|
1888
|
+
|
|
1888
1889
|
this[dimensionsSymbol].setVia("data.calculated", true);
|
|
1889
1890
|
this[dimensionsSymbol].setVia("data.itemReferences", itemReferences);
|
|
1890
1891
|
}
|
|
1891
1892
|
|
|
1893
|
+
/**
|
|
1894
|
+
* Limits the available space to the host when the host is narrower than its
|
|
1895
|
+
* container for a reason that does not depend on its content, such as an
|
|
1896
|
+
* explicit `width` or `max-width`.
|
|
1897
|
+
*
|
|
1898
|
+
* A host whose width follows its content (grid item with `justify-self`,
|
|
1899
|
+
* `auto` grid column, flex item without `flex-grow`) must not limit the space:
|
|
1900
|
+
* its current width is the result of the previous layout. Once a control has
|
|
1901
|
+
* been moved into the popper, such a host shrinks to the overflow switch and
|
|
1902
|
+
* would never offer room for the control again. The host is therefore probed
|
|
1903
|
+
* with the width the controls need; only a host that cannot grow limits the
|
|
1904
|
+
* space.
|
|
1905
|
+
*
|
|
1906
|
+
* @private
|
|
1907
|
+
* @param {string[]} itemReferences
|
|
1908
|
+
* @return {void}
|
|
1909
|
+
*/
|
|
1910
|
+
function limitSpaceToHostCapacity(itemReferences) {
|
|
1911
|
+
const hostContentWidth = getElementContentWidth(this);
|
|
1912
|
+
if (!(hostContentWidth > 0)) {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
let measuredSpace = 0;
|
|
1917
|
+
try {
|
|
1918
|
+
measuredSpace = this[dimensionsSymbol].getVia("data.space", 0);
|
|
1919
|
+
} catch {}
|
|
1920
|
+
|
|
1921
|
+
if (hostContentWidth >= measuredSpace) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const requiredWidth = getRequiredItemWidth.call(this, itemReferences);
|
|
1926
|
+
let hostCapacity = hostContentWidth;
|
|
1927
|
+
if (requiredWidth > hostCapacity) {
|
|
1928
|
+
hostCapacity = measureHostCapacity.call(this, requiredWidth);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
if (requiredWidth > hostCapacity) {
|
|
1932
|
+
this[dimensionsSymbol].setVia(
|
|
1933
|
+
"data.space",
|
|
1934
|
+
Math.min(measuredSpace, hostCapacity),
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Sums the stored widths of all controls that are not hidden by themselves,
|
|
1941
|
+
* regardless of whether they are currently in the main row or in the popper.
|
|
1942
|
+
*
|
|
1943
|
+
* @private
|
|
1944
|
+
* @param {string[]} itemReferences
|
|
1945
|
+
* @return {number}
|
|
1946
|
+
*/
|
|
1947
|
+
function getRequiredItemWidth(itemReferences) {
|
|
1948
|
+
const widthByReference = new Map();
|
|
1949
|
+
for (const ref of itemReferences) {
|
|
1950
|
+
try {
|
|
1951
|
+
widthByReference.set(
|
|
1952
|
+
ref,
|
|
1953
|
+
this[dimensionsSymbol].getVia(`data.item.${ref}`),
|
|
1954
|
+
);
|
|
1955
|
+
} catch {
|
|
1956
|
+
// Items that have never been visible have no stored width.
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
let requiredWidth = 0;
|
|
1961
|
+
const counted = new Set();
|
|
1962
|
+
for (const item of this.children) {
|
|
1963
|
+
if (!(item instanceof HTMLElement) || isElementSelfHidden(item)) {
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
const ref = item.getAttribute("data-monster-reference");
|
|
1967
|
+
if (ref === null || counted.has(ref) || !widthByReference.has(ref)) {
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
counted.add(ref);
|
|
1971
|
+
requiredWidth += widthByReference.get(ref) || 0;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
return requiredWidth;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* Measures how wide the host content box becomes when the shadow control
|
|
1979
|
+
* container requests at least the given width. The probe is applied and
|
|
1980
|
+
* removed within the same synchronous layout pass, so observers never see it.
|
|
1981
|
+
*
|
|
1982
|
+
* A host with an explicit `width` or `max-width` keeps its size, a host whose
|
|
1983
|
+
* width follows its content grows with the request.
|
|
1984
|
+
*
|
|
1985
|
+
* @private
|
|
1986
|
+
* @param {number} requiredWidth
|
|
1987
|
+
* @return {number}
|
|
1988
|
+
*/
|
|
1989
|
+
function measureHostCapacity(requiredWidth) {
|
|
1990
|
+
const control = this[controlElementSymbol];
|
|
1991
|
+
if (!(control instanceof HTMLElement)) {
|
|
1992
|
+
return getElementContentWidth(this);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const previousValue = control.style.getPropertyValue("min-inline-size");
|
|
1996
|
+
const previousPriority = control.style.getPropertyPriority("min-inline-size");
|
|
1997
|
+
control.style.setProperty(
|
|
1998
|
+
"min-inline-size",
|
|
1999
|
+
`${Math.ceil(requiredWidth)}px`,
|
|
2000
|
+
"important",
|
|
2001
|
+
);
|
|
2002
|
+
|
|
2003
|
+
try {
|
|
2004
|
+
return getElementContentWidth(this);
|
|
2005
|
+
} finally {
|
|
2006
|
+
if (previousValue === "") {
|
|
2007
|
+
control.style.removeProperty("min-inline-size");
|
|
2008
|
+
} else {
|
|
2009
|
+
control.style.setProperty(
|
|
2010
|
+
"min-inline-size",
|
|
2011
|
+
previousValue,
|
|
2012
|
+
previousPriority,
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1892
2018
|
/**
|
|
1893
2019
|
* @private
|
|
1894
2020
|
* @param {HTMLElement} element
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** generated from surface.pcss **/
|
|
2
|
+
:where(.monster-surface){box-sizing:border-box;min-inline-size:0;display:grid;gap:var(--monster-surface-gap,var(--monster-space-3));padding:var(--monster-surface-padding,var(--monster-space-5));border-width:var(--monster-border-width);border-style:var(--monster-border-style);border-color:var(--monster-color-border-1);border-radius:var(--monster-border-radius);box-shadow:var(--monster-box-shadow-1)}.monster-surface-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--monster-surface-grid-min-inline-size,20rem)),1fr));gap:var(--monster-surface-grid-gap,var(--monster-space-5));align-items:stretch}.monster-surface-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--monster-surface-actions-gap,var(--monster-space-3));margin-block-start:auto}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
:where(.monster-surface) {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
min-inline-size: 0;
|
|
4
|
+
display: grid;
|
|
5
|
+
gap: var(--monster-surface-gap, var(--monster-space-3));
|
|
6
|
+
padding: var(--monster-surface-padding, var(--monster-space-5));
|
|
7
|
+
border-width: var(--monster-border-width);
|
|
8
|
+
border-style: var(--monster-border-style);
|
|
9
|
+
border-color: var(--monster-color-border-1);
|
|
10
|
+
border-radius: var(--monster-border-radius);
|
|
11
|
+
box-shadow: var(--monster-box-shadow-1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.monster-surface-grid {
|
|
15
|
+
display: grid;
|
|
16
|
+
grid-template-columns: repeat(
|
|
17
|
+
auto-fit,
|
|
18
|
+
minmax(
|
|
19
|
+
min(100%, var(--monster-surface-grid-min-inline-size, 20rem)),
|
|
20
|
+
1fr
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
gap: var(--monster-surface-grid-gap, var(--monster-space-5));
|
|
24
|
+
align-items: stretch;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.monster-surface-actions {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-wrap: wrap;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: var(--monster-surface-actions-gap, var(--monster-space-3));
|
|
32
|
+
margin-block-start: auto;
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © Volker Schukai and all contributing authors, 2026. All rights reserved.
|
|
3
|
+
* Node module: @schukai/monster
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
7
|
+
*
|
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
|
10
|
+
* For more information about purchasing a commercial license, please contact Volker Schukai.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { addAttributeToken } from "../../dom/attributes.mjs";
|
|
14
|
+
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
|
|
15
|
+
|
|
16
|
+
export { SurfaceStyleSheet };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @private
|
|
20
|
+
* @type {CSSStyleSheet}
|
|
21
|
+
*/
|
|
22
|
+
const SurfaceStyleSheet = new CSSStyleSheet();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
SurfaceStyleSheet.insertRule(
|
|
26
|
+
`
|
|
27
|
+
@layer surface {
|
|
28
|
+
:where(.monster-surface){box-sizing:border-box;min-inline-size:0;display:grid;gap:var(--monster-surface-gap,var(--monster-space-3));padding:var(--monster-surface-padding,var(--monster-space-5));border-width:var(--monster-border-width);border-style:var(--monster-border-style);border-color:var(--monster-color-border-1);border-radius:var(--monster-border-radius);box-shadow:var(--monster-box-shadow-1)}.monster-surface-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--monster-surface-grid-min-inline-size,20rem)),1fr));gap:var(--monster-surface-grid-gap,var(--monster-space-5));align-items:stretch}.monster-surface-actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--monster-surface-actions-gap,var(--monster-space-3));margin-block-start:auto}
|
|
29
|
+
}`,
|
|
30
|
+
0,
|
|
31
|
+
);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
addAttributeToken(
|
|
34
|
+
document.getRootNode().querySelector("html"),
|
|
35
|
+
ATTRIBUTE_ERRORMESSAGE,
|
|
36
|
+
e + "",
|
|
37
|
+
);
|
|
38
|
+
}
|
package/source/types/version.mjs
CHANGED
|
@@ -280,6 +280,106 @@ describe("ButtonBar", function () {
|
|
|
280
280
|
}
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
+
it("should not limit the space to a host whose width follows its content (#603)", async function () {
|
|
284
|
+
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
285
|
+
const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
286
|
+
const scheduledCallbacks = [];
|
|
287
|
+
const flushFrames = async () => {
|
|
288
|
+
while (scheduledCallbacks.length > 0) {
|
|
289
|
+
scheduledCallbacks.shift()();
|
|
290
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
291
|
+
}
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
window.requestAnimationFrame = (callback) => {
|
|
297
|
+
scheduledCallbacks.push(callback);
|
|
298
|
+
return scheduledCallbacks.length;
|
|
299
|
+
};
|
|
300
|
+
globalThis.requestAnimationFrame = window.requestAnimationFrame;
|
|
301
|
+
|
|
302
|
+
const mocks = document.getElementById("mocks");
|
|
303
|
+
mocks.innerHTML = `
|
|
304
|
+
<div id="intrinsic-wrapper">
|
|
305
|
+
<monster-button-bar id="intrinsic-button-bar">
|
|
306
|
+
<button id="intrinsic-a" type="button">One</button>
|
|
307
|
+
<button id="intrinsic-b" type="button">Two</button>
|
|
308
|
+
</monster-button-bar>
|
|
309
|
+
</div>
|
|
310
|
+
`;
|
|
311
|
+
const wrapper = document.getElementById("intrinsic-wrapper");
|
|
312
|
+
const bar = document.getElementById("intrinsic-button-bar");
|
|
313
|
+
const buttons = [
|
|
314
|
+
document.getElementById("intrinsic-a"),
|
|
315
|
+
document.getElementById("intrinsic-b"),
|
|
316
|
+
];
|
|
317
|
+
const control = bar.shadowRoot.querySelector(
|
|
318
|
+
'[data-monster-role="control"]',
|
|
319
|
+
);
|
|
320
|
+
const switchButton = bar.shadowRoot.querySelector(
|
|
321
|
+
'[data-monster-role="switch"]',
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
wrapper.style.boxSizing = "border-box";
|
|
325
|
+
Object.defineProperty(wrapper, "clientWidth", {
|
|
326
|
+
configurable: true,
|
|
327
|
+
value: 600,
|
|
328
|
+
});
|
|
329
|
+
// The host collapsed to the switch after an earlier overflow, but its
|
|
330
|
+
// width follows its content: it takes any width the bar probes for.
|
|
331
|
+
const probes = [];
|
|
332
|
+
Object.defineProperty(bar, "clientWidth", {
|
|
333
|
+
configurable: true,
|
|
334
|
+
get: () => {
|
|
335
|
+
const requested = parseFloat(control.style.minInlineSize) || 0;
|
|
336
|
+
if (requested > 0) {
|
|
337
|
+
probes.push(requested);
|
|
338
|
+
}
|
|
339
|
+
return Math.max(44, requested);
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
Object.defineProperty(switchButton, "offsetWidth", {
|
|
343
|
+
configurable: true,
|
|
344
|
+
value: 40,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
for (const button of buttons) {
|
|
348
|
+
Object.defineProperty(button, "offsetWidth", {
|
|
349
|
+
configurable: true,
|
|
350
|
+
value: 114,
|
|
351
|
+
});
|
|
352
|
+
Object.defineProperty(button, "offsetHeight", {
|
|
353
|
+
configurable: true,
|
|
354
|
+
value: 30,
|
|
355
|
+
});
|
|
356
|
+
button.getBoundingClientRect = () => ({
|
|
357
|
+
width: 114.2,
|
|
358
|
+
height: 30,
|
|
359
|
+
top: 0,
|
|
360
|
+
right: 114.2,
|
|
361
|
+
bottom: 30,
|
|
362
|
+
left: 0,
|
|
363
|
+
x: 0,
|
|
364
|
+
y: 0,
|
|
365
|
+
toJSON: () => {},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await flushFrames();
|
|
370
|
+
|
|
371
|
+
expect(buttons.map((button) => button.getAttribute("slot"))).to.deep.equal(
|
|
372
|
+
[null, null],
|
|
373
|
+
);
|
|
374
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
375
|
+
expect(probes).to.include(229);
|
|
376
|
+
expect(control.style.minInlineSize).to.equal("");
|
|
377
|
+
} finally {
|
|
378
|
+
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
379
|
+
globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
283
383
|
it("should use the host content box when reserving overflow switch space", async function () {
|
|
284
384
|
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
285
385
|
const originalGlobalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
@@ -3017,4 +3017,227 @@ describe("ControlBar", function () {
|
|
|
3017
3017
|
globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
|
|
3018
3018
|
}
|
|
3019
3019
|
});
|
|
3020
|
+
|
|
3021
|
+
describe("host width (#603)", function () {
|
|
3022
|
+
const BUTTON_WIDTH = 114.2;
|
|
3023
|
+
const SWITCH_WIDTH = 40;
|
|
3024
|
+
let restore;
|
|
3025
|
+
|
|
3026
|
+
afterEach(function () {
|
|
3027
|
+
if (typeof restore === "function") {
|
|
3028
|
+
restore();
|
|
3029
|
+
restore = null;
|
|
3030
|
+
}
|
|
3031
|
+
document.getElementById("mocks").replaceChildren();
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
/**
|
|
3035
|
+
* Mounts a bar with two controls inside a 600px wrapper. The host's
|
|
3036
|
+
* clientWidth is simulated by `hostWidth`, which receives the min width the
|
|
3037
|
+
* bar requests on its shadow control container during the probe (or 0).
|
|
3038
|
+
*/
|
|
3039
|
+
async function mountBar({ hostWidth }) {
|
|
3040
|
+
const OriginalResizeObserver = window.ResizeObserver;
|
|
3041
|
+
const originalGlobalResizeObserver = globalThis.ResizeObserver;
|
|
3042
|
+
const originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
3043
|
+
const originalGlobalRequestAnimationFrame =
|
|
3044
|
+
globalThis.requestAnimationFrame;
|
|
3045
|
+
|
|
3046
|
+
class TrackingResizeObserver extends ResizeObserverMock {
|
|
3047
|
+
static instances = [];
|
|
3048
|
+
|
|
3049
|
+
constructor(callback) {
|
|
3050
|
+
super(callback);
|
|
3051
|
+
TrackingResizeObserver.instances.push(this);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
const scheduledCallbacks = [];
|
|
3056
|
+
const flushFrames = async () => {
|
|
3057
|
+
while (scheduledCallbacks.length > 0) {
|
|
3058
|
+
scheduledCallbacks.shift()();
|
|
3059
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3060
|
+
}
|
|
3061
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3062
|
+
};
|
|
3063
|
+
|
|
3064
|
+
restore = () => {
|
|
3065
|
+
window.ResizeObserver = OriginalResizeObserver;
|
|
3066
|
+
globalThis.ResizeObserver = originalGlobalResizeObserver;
|
|
3067
|
+
window.requestAnimationFrame = originalRequestAnimationFrame;
|
|
3068
|
+
globalThis.requestAnimationFrame = originalGlobalRequestAnimationFrame;
|
|
3069
|
+
};
|
|
3070
|
+
|
|
3071
|
+
window.ResizeObserver = TrackingResizeObserver;
|
|
3072
|
+
globalThis.ResizeObserver = TrackingResizeObserver;
|
|
3073
|
+
window.requestAnimationFrame = (callback) => {
|
|
3074
|
+
scheduledCallbacks.push(callback);
|
|
3075
|
+
return scheduledCallbacks.length;
|
|
3076
|
+
};
|
|
3077
|
+
globalThis.requestAnimationFrame = window.requestAnimationFrame;
|
|
3078
|
+
|
|
3079
|
+
const mocks = document.getElementById("mocks");
|
|
3080
|
+
mocks.innerHTML = `
|
|
3081
|
+
<div id="host-width-wrapper">
|
|
3082
|
+
<monster-control-bar id="host-width-bar">
|
|
3083
|
+
<button id="host-width-one" type="button">One</button>
|
|
3084
|
+
<button id="host-width-two" type="button">Two</button>
|
|
3085
|
+
</monster-control-bar>
|
|
3086
|
+
</div>
|
|
3087
|
+
`;
|
|
3088
|
+
|
|
3089
|
+
const wrapper = document.getElementById("host-width-wrapper");
|
|
3090
|
+
const bar = document.getElementById("host-width-bar");
|
|
3091
|
+
const controls = [
|
|
3092
|
+
document.getElementById("host-width-one"),
|
|
3093
|
+
document.getElementById("host-width-two"),
|
|
3094
|
+
];
|
|
3095
|
+
const control = bar.shadowRoot.querySelector(
|
|
3096
|
+
'[data-monster-role="control"]',
|
|
3097
|
+
);
|
|
3098
|
+
const switchButton = bar.shadowRoot.querySelector(
|
|
3099
|
+
'[data-monster-role="switch"]',
|
|
3100
|
+
);
|
|
3101
|
+
|
|
3102
|
+
let wrapperWidth = 600;
|
|
3103
|
+
wrapper.style.boxSizing = "border-box";
|
|
3104
|
+
Object.defineProperty(wrapper, "clientWidth", {
|
|
3105
|
+
configurable: true,
|
|
3106
|
+
get: () => wrapperWidth,
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
const probes = [];
|
|
3110
|
+
Object.defineProperty(bar, "clientWidth", {
|
|
3111
|
+
configurable: true,
|
|
3112
|
+
get: () => {
|
|
3113
|
+
const requested = parseFloat(control.style.minInlineSize) || 0;
|
|
3114
|
+
if (requested > 0) {
|
|
3115
|
+
probes.push(requested);
|
|
3116
|
+
}
|
|
3117
|
+
return hostWidth(requested);
|
|
3118
|
+
},
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
for (const button of controls) {
|
|
3122
|
+
Object.defineProperty(button, "offsetWidth", {
|
|
3123
|
+
configurable: true,
|
|
3124
|
+
value: Math.floor(BUTTON_WIDTH),
|
|
3125
|
+
});
|
|
3126
|
+
Object.defineProperty(button, "offsetHeight", {
|
|
3127
|
+
configurable: true,
|
|
3128
|
+
value: 30,
|
|
3129
|
+
});
|
|
3130
|
+
button.getBoundingClientRect = () => ({
|
|
3131
|
+
width: BUTTON_WIDTH,
|
|
3132
|
+
height: 30,
|
|
3133
|
+
top: 0,
|
|
3134
|
+
right: BUTTON_WIDTH,
|
|
3135
|
+
bottom: 30,
|
|
3136
|
+
left: 0,
|
|
3137
|
+
x: 0,
|
|
3138
|
+
y: 0,
|
|
3139
|
+
toJSON: () => {},
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
Object.defineProperty(switchButton, "offsetWidth", {
|
|
3143
|
+
configurable: true,
|
|
3144
|
+
value: SWITCH_WIDTH,
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
await flushFrames();
|
|
3148
|
+
|
|
3149
|
+
const resizeObserver = TrackingResizeObserver.instances.find((observer) =>
|
|
3150
|
+
observer.elements.includes(wrapper),
|
|
3151
|
+
);
|
|
3152
|
+
|
|
3153
|
+
return {
|
|
3154
|
+
bar,
|
|
3155
|
+
controls,
|
|
3156
|
+
control,
|
|
3157
|
+
switchButton,
|
|
3158
|
+
probes,
|
|
3159
|
+
flushFrames,
|
|
3160
|
+
resizeWrapper: async (width) => {
|
|
3161
|
+
wrapperWidth = width;
|
|
3162
|
+
resizeObserver.triggerResize([{ target: wrapper }]);
|
|
3163
|
+
await flushFrames();
|
|
3164
|
+
},
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
const slots = (controls) =>
|
|
3169
|
+
controls.map((control) => control.getAttribute("slot"));
|
|
3170
|
+
|
|
3171
|
+
it("should not limit the space to a host whose width follows its content", async function () {
|
|
3172
|
+
// The host currently shows only the overflow switch, as after a
|
|
3173
|
+
// previous overflow, but grows to whatever its content requests.
|
|
3174
|
+
const { controls, control, switchButton, probes } = await mountBar({
|
|
3175
|
+
hostWidth: (requested) => Math.max(44, requested),
|
|
3176
|
+
});
|
|
3177
|
+
|
|
3178
|
+
expect(slots(controls)).to.deep.equal([null, null]);
|
|
3179
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
3180
|
+
expect(probes).to.include(Math.ceil(BUTTON_WIDTH * 2));
|
|
3181
|
+
expect(control.style.minInlineSize).to.equal("");
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
it("should keep limiting the space to a host that cannot grow", async function () {
|
|
3185
|
+
const { controls, control, switchButton, probes } = await mountBar({
|
|
3186
|
+
hostWidth: () => 80,
|
|
3187
|
+
});
|
|
3188
|
+
|
|
3189
|
+
expect(slots(controls)).to.deep.equal(["popper", "popper"]);
|
|
3190
|
+
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
3191
|
+
expect(probes).to.include(Math.ceil(BUTTON_WIDTH * 2));
|
|
3192
|
+
expect(control.style.minInlineSize).to.equal("");
|
|
3193
|
+
});
|
|
3194
|
+
|
|
3195
|
+
it("should use the width a constrained host can still offer", async function () {
|
|
3196
|
+
// max-width style limit: the host grows, but only up to 200px.
|
|
3197
|
+
const { controls, switchButton } = await mountBar({
|
|
3198
|
+
hostWidth: (requested) => Math.min(200, Math.max(44, requested)),
|
|
3199
|
+
});
|
|
3200
|
+
|
|
3201
|
+
expect(slots(controls)).to.deep.equal([null, "popper"]);
|
|
3202
|
+
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
3203
|
+
});
|
|
3204
|
+
|
|
3205
|
+
it("should not probe a host that already fills its container", async function () {
|
|
3206
|
+
const { controls, probes } = await mountBar({
|
|
3207
|
+
hostWidth: () => 600,
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3210
|
+
expect(slots(controls)).to.deep.equal([null, null]);
|
|
3211
|
+
expect(probes).to.be.empty;
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
it("should not probe a narrower host when the controls already fit", async function () {
|
|
3215
|
+
const { controls, probes, switchButton } = await mountBar({
|
|
3216
|
+
hostWidth: () => 300,
|
|
3217
|
+
});
|
|
3218
|
+
|
|
3219
|
+
expect(slots(controls)).to.deep.equal([null, null]);
|
|
3220
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
3221
|
+
expect(probes).to.be.empty;
|
|
3222
|
+
});
|
|
3223
|
+
|
|
3224
|
+
it("should restore controls into an intrinsic host after the container grows again", async function () {
|
|
3225
|
+
let visibleWidth = BUTTON_WIDTH * 2;
|
|
3226
|
+
const { controls, switchButton, resizeWrapper } = await mountBar({
|
|
3227
|
+
// The intrinsic host is as wide as its visible content, but takes any
|
|
3228
|
+
// requested width.
|
|
3229
|
+
hostWidth: (requested) => Math.max(Math.floor(visibleWidth), requested),
|
|
3230
|
+
});
|
|
3231
|
+
expect(slots(controls)).to.deep.equal([null, null]);
|
|
3232
|
+
|
|
3233
|
+
visibleWidth = SWITCH_WIDTH;
|
|
3234
|
+
await resizeWrapper(100);
|
|
3235
|
+
expect(slots(controls)).to.deep.equal(["popper", "popper"]);
|
|
3236
|
+
expect(switchButton.hasAttribute("hidden")).to.be.false;
|
|
3237
|
+
|
|
3238
|
+
await resizeWrapper(600);
|
|
3239
|
+
expect(slots(controls)).to.deep.equal([null, null]);
|
|
3240
|
+
expect(switchButton.hasAttribute("hidden")).to.be.true;
|
|
3241
|
+
});
|
|
3242
|
+
});
|
|
3020
3243
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { expect } from "chai";
|
|
3
|
+
|
|
4
|
+
const source = readFileSync(
|
|
5
|
+
new URL("../../../../source/components/style/surface.pcss", import.meta.url),
|
|
6
|
+
"utf8",
|
|
7
|
+
);
|
|
8
|
+
const generatedStyleSheet = readFileSync(
|
|
9
|
+
new URL(
|
|
10
|
+
"../../../../source/components/stylesheet/surface.mjs",
|
|
11
|
+
import.meta.url,
|
|
12
|
+
),
|
|
13
|
+
"utf8",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe("Content surfaces", () => {
|
|
17
|
+
it("provides theme-composable static content geometry", () => {
|
|
18
|
+
expect(source).to.include(":where(.monster-surface)");
|
|
19
|
+
expect(source).to.include(
|
|
20
|
+
"gap: var(--monster-surface-gap, var(--monster-space-3));",
|
|
21
|
+
);
|
|
22
|
+
expect(source).to.include(
|
|
23
|
+
"padding: var(--monster-surface-padding, var(--monster-space-5));",
|
|
24
|
+
);
|
|
25
|
+
expect(source).to.include("border-width: var(--monster-border-width);");
|
|
26
|
+
expect(source).to.include("border-style: var(--monster-border-style);");
|
|
27
|
+
expect(source).to.include("border-color: var(--monster-color-border-1);");
|
|
28
|
+
expect(source).to.include("border-radius: var(--monster-border-radius);");
|
|
29
|
+
expect(source).not.to.match(/^\s*(?:background-color|color):/mu);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("provides reusable responsive grids and action rows", () => {
|
|
33
|
+
expect(source).to.include(".monster-surface-grid");
|
|
34
|
+
expect(source).to.include("grid-template-columns: repeat(");
|
|
35
|
+
expect(source).to.include("auto-fit,");
|
|
36
|
+
expect(source).to.include(".monster-surface-actions");
|
|
37
|
+
expect(source).to.include("margin-block-start: auto;");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("keeps the generated stylesheet synchronized", () => {
|
|
41
|
+
expect(generatedStyleSheet).to.include(":where(.monster-surface)");
|
|
42
|
+
expect(generatedStyleSheet).to.include(
|
|
43
|
+
"gap:var(--monster-surface-gap,var(--monster-space-3))",
|
|
44
|
+
);
|
|
45
|
+
expect(generatedStyleSheet).to.include(
|
|
46
|
+
"grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--monster-surface-grid-min-inline-size,20rem)),1fr))",
|
|
47
|
+
);
|
|
48
|
+
expect(generatedStyleSheet).to.include(".monster-surface-actions");
|
|
49
|
+
});
|
|
50
|
+
});
|