diff --git a/bun.lock b/bun.lock index affe5a9f..5f49ddf1 100644 --- a/bun.lock +++ b/bun.lock @@ -376,7 +376,7 @@ }, "packages/csr": { "name": "@wrnexus/csr", - "version": "0.8.74", + "version": "0.8.75", "dependencies": { "@wrnexus/core": "^0.8.12", }, @@ -391,7 +391,7 @@ }, "packages/design-studio": { "name": "@wrnexus/design-studio", - "version": "0.1.4", + "version": "0.1.5", "bin": { "wrnexus-design-studio": "src/cli.ts", }, @@ -413,7 +413,7 @@ }, "packages/dev-server": { "name": "@wrnexus/dev-server", - "version": "0.8.83", + "version": "0.8.84", "dependencies": { "@wrnexus/authz": "workspace:*", "@wrnexus/cache": "workspace:*", @@ -782,7 +782,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.120", + "version": "0.8.121", "dependencies": { "@wrnexus/core": "workspace:*", "@wrnexus/design-system": "workspace:*", diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json index 72f016e1..dc5414bf 100644 --- a/docs/ui-visual-contract-0.8.json +++ b/docs/ui-visual-contract-0.8.json @@ -3,7 +3,7 @@ "releaseLine": "0.8", "artifacts": { "packages/ui/components/Accordion.wrn": "51714cc2103e4727fc64a5e6cb37f060d5b495c72e1660c1a04948982b7f4c53", - "packages/ui/components/AdvancedSelect.wrn": "b812acc74cd717af6f102d29fd07b47de2b2ad6e3879706730e0a32eada6f3ee", + "packages/ui/components/AdvancedSelect.wrn": "4b867a384feb3c089804d7c7f20bb7b152ffc650ece503480d109a532e7ef3a7", "packages/ui/components/AnnouncementBar.wrn": "b2b833350eb9a37627e4047f371c12ff7a8c25385a70294c0fc25ddfd4bb74c8", "packages/ui/components/AuthForm.wrn": "8a7bf17ee92d98caae1e987eeade29d0effc89b0eca4b8787dc10b2abad25de8", "packages/ui/components/AuthSplitLayout.wrn": "e5ca3b96f68fbc666d47ad2efac9f6c37fe321fcf222109aec5f05f4433b99d6", @@ -21,14 +21,14 @@ "packages/ui/components/Collapse.wrn": "c5ecae44b455498e5eae527100abdc27bf9e18f7a361ce563d45087c0cc4a3d3", "packages/ui/components/ColorPicker.wrn": "561b1cef73ff86f50072fefabade212b0cf10a48e6dd6c6aa93fd6e900fe3f50", "packages/ui/components/Columns.wrn": "c892798d910745453b617a7f0dcf20ab1fd6506095c150de0f36e9fcf7661783", - "packages/ui/components/Combobox.wrn": "1268286b6dcb00f187d985d5e7fedf0371c8febf04c16907beb10cf36ced2d25", + "packages/ui/components/Combobox.wrn": "2096855460f58c250a056037ae3355496b0cdbf179f33baa8c2a55d7532ca60b", "packages/ui/components/Confetti.wrn": "91f72aa66babd721d5b8406992b13df442cd3d2a093540b0e504380242479ca6", "packages/ui/components/Container.wrn": "eefa6f3dfaecb1e86172f6f2b886adfad7dcacf07f75098f7d07948bff5d0a50", "packages/ui/components/ContextMenu.wrn": "5dcf067343e35307c1e0de1c319112e6741bcc9b1c827cef5d09721383f4d066", "packages/ui/components/CustomScrollbar.wrn": "5325c9429b432b113e232a19786bb3f873454f8f4c7c0d6a3ecf81897fa35f09", "packages/ui/components/DataMap.wrn": "66752f98718c748f25e0eed76d39f504fc20b26d3b3ae841db85f5d5f02df6cb", - "packages/ui/components/DataTable.wrn": "c25aa8849d140562970cd2ffaa33b720cc903e809ef7fb20a564425e8295d5b9", - "packages/ui/components/DatePicker.wrn": "107be1aa2ae2b329b50c9063f2f25bf4ac7dccebe6a2b6ca412cc12afac8f3af", + "packages/ui/components/DataTable.wrn": "eb2e2cfe1eb5306b85261199a833605213e0d2094f3823a5acfedd30147144a1", + "packages/ui/components/DatePicker.wrn": "7ab1567a878bb78bb7c3eac637236c7ca157e942a79ac9262b1a78f3190d6c98", "packages/ui/components/DeviceFrame.wrn": "ae6a19ee27ba24d5673e3b4fa951bfcf8f12f6ef40bad0e61f349666e13afa64", "packages/ui/components/Divider.wrn": "3d1065aa2dccb11f31eff35105ce7a3a0321317850e4df02b6c298e4a492cadc", "packages/ui/components/DragAndDrop.wrn": "5af6c01a9b58eca44868205708f47738c1c2b25589b94035cb2f4f31eec06ba6", @@ -62,13 +62,13 @@ "packages/ui/components/MetricGrid.wrn": "c9f228b4b6bd371ad94c1e534501b9708ea555bef1e64c8bdf95fa5e07576d1d", "packages/ui/components/Modal.wrn": "7ad50502de6f6ff20ec2dc62fdc923f78afc222509771c5f26e5a56810ead374", "packages/ui/components/Nav.wrn": "dd7d67b15ffe88fb991321a3707c82770bc0c14ced5cc6f2710adf7ef2045c6c", - "packages/ui/components/Navbar.wrn": "02081bcbbf88f9fbbaff7348ad7183d914a424c8fe22ed3c870b61620c2920fa", + "packages/ui/components/Navbar.wrn": "22b2b22befd57605d3fa8aa0714d46230790720ea12425dbe642feaef6a47675", "packages/ui/components/PageHeader.wrn": "7586f4579031e9391d673eb413dfe2ca65f9e3da2a0257c6f987bfc3b3482927", "packages/ui/components/Pagination.wrn": "df6faf0dbafd1249092b45e586159b575c3d37c73d81847376a28c3ea8fdb5cd", "packages/ui/components/PinInput.wrn": "c17b2a1bb38ba0420de7c9179e64fd104cbd8d3b84127a308a4333d46232b3fb", "packages/ui/components/Popover.wrn": "aa73c63bde88d6f52061a98fdc8c84f1147e784abfeb802f094a89c061b9a893", "packages/ui/components/PortalDashboard.wrn": "ef67fdf9faac850ef34b1d2564386d41a0cfa4c754ed0988e85f2e7ea2f50cdb", - "packages/ui/components/PreferenceSwitcher.wrn": "b1a5a72997dec35a1294f8a7724abc52ba9e50a108e175da9442816ca9adf0ec", + "packages/ui/components/PreferenceSwitcher.wrn": "33637d758e63a4a6ce5ca51362f34387aacc1f64134434912c7ecd119b7cb5a8", "packages/ui/components/PublicPageShell.wrn": "1e28f4f387f235198a723ed13b0128fe484894e4962b73a482a5a28c8acf003f", "packages/ui/components/RangeSlider.wrn": "a4005dcca80e3e23b6488e865b92486b368f46e00d3f05e7b0663d421652be06", "packages/ui/components/Rating.wrn": "8456d577da017eac6e54db78b4beced73930da6fe8ef7a5b74c65c2902571ea9", @@ -84,7 +84,7 @@ "packages/ui/components/StyledIcon.wrn": "64ac0aeaa9f0b833f68943dc0116acc1d0a9c8816d3b4bb2c91d00cf7ec3dc0a", "packages/ui/components/Tabs.wrn": "462b8d821dd4390213fc6e75263d30d2b0f9a3d19f7b83053ae41f780c2c2671", "packages/ui/components/TextLink.wrn": "3d8ec57f065fbb12a2ba42e85f1d66ff24adf041e8a1ec4b59cc3f1a17004f4e", - "packages/ui/components/TimePicker.wrn": "1695e1cc66f1c41f61bd194db0ba8abfba45a528ca414bb700272a1884f908de", + "packages/ui/components/TimePicker.wrn": "385a90f2f6db134ab86450bc2a3e60f2237b511989ce3c8b1bdc2bfe1f0a8276", "packages/ui/components/Timeline.wrn": "1f46194e222b7eb04750785542a8933028fb4d23a94f199874ffe27d9dc74e79", "packages/ui/components/Toaster.wrn": "e46b15818d8f50467521e58c5dae605871c107c09e80e8dade60da675b40b8e6", "packages/ui/components/ToggleCount.wrn": "b95bc9967d40d954427caea544c1b465f30dede623ed8d25b0aefee5b2453910", diff --git a/packages/csr/package.json b/packages/csr/package.json index ff2b4f00..a2595eac 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.8.74", + "version": "0.8.75", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index 146d285f..6ab4b357 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -3244,6 +3244,16 @@ export const REACTIVE_RUNTIME = String.raw` function clampAnchored(element) { // Measure the untouched position; the offset is recomputed from scratch. element.style.translate = ""; + // Restore authored limits before measuring, including after a resize. + if (element.__wrnAnchorLimits) { + var saved = element.__wrnAnchorLimits; + element.style.maxWidth = saved[0]; + element.style.maxHeight = saved[1]; + element.style.minWidth = saved[2]; + element.style.minHeight = saved[3]; + element.style.overflow = saved[4]; + element.style.boxSizing = saved[5]; + } var arrow = element.querySelector("[data-wrn-anchor-arrow]"); if (arrow) arrow.style.translate = ""; if (!isRenderedElement(element)) return; @@ -3251,6 +3261,38 @@ export const REACTIVE_RUNTIME = String.raw` var rect = element.getBoundingClientRect(); var viewportWidth = window.innerWidth || document.documentElement.clientWidth; var viewportHeight = window.innerHeight || document.documentElement.clientHeight; + var visual = window.visualViewport; + var left = (visual ? visual.offsetLeft : 0) + ANCHOR_MARGIN; + var top = (visual ? visual.offsetTop : 0) + ANCHOR_MARGIN; + var right = left + (visual ? visual.width : viewportWidth) - ANCHOR_MARGIN * 2; + var bottom = top + (visual ? visual.height : viewportHeight) - ANCHOR_MARGIN * 2; + // A preview, sidebar or scroll container may clip well before the screen edge. + for (var parent = element.parentElement; parent && parent !== document.documentElement; parent = parent.parentElement) { + var css = window.getComputedStyle(parent); + var clipX = /auto|scroll|hidden|clip/.test(css.overflowX || css.overflow); + var clipY = /auto|scroll|hidden|clip/.test(css.overflowY || css.overflow); + if (!clipX && !clipY) continue; + var bounds = parent.getBoundingClientRect(); + if (clipX) { + left = Math.max(left, bounds.left + parent.clientLeft + ANCHOR_MARGIN); + right = Math.min(right, bounds.left + parent.clientLeft + parent.clientWidth - ANCHOR_MARGIN); + } + if (clipY) { + top = Math.max(top, bounds.top + parent.clientTop + ANCHOR_MARGIN); + bottom = Math.min(bottom, bounds.top + parent.clientTop + parent.clientHeight - ANCHOR_MARGIN); + } + } + if (right <= left || bottom <= top) return; + if (!element.__wrnAnchorLimits) element.__wrnAnchorLimits = [element.style.maxWidth, element.style.maxHeight, element.style.minWidth, element.style.minHeight, element.style.overflow, element.style.boxSizing]; + if (rect.width > right - left || rect.height > bottom - top) { + element.style.boxSizing = "border-box"; + element.style.minWidth = "0px"; + element.style.minHeight = "0px"; + element.style.maxWidth = Math.min(rect.width, right - left) + "px"; + element.style.maxHeight = Math.min(rect.height, bottom - top) + "px"; + element.style.overflow = "auto"; + rect = element.getBoundingClientRect(); + } /* * Clamp only while the overlay's own trigger is on screen. @@ -3263,7 +3305,7 @@ export const REACTIVE_RUNTIME = String.raw` * one very much does need moving; skipping it left the menu invisible * with no way to reach it. The trigger tells the two apart. */ - var host = element.closest("[data-ui-component]") || element.parentElement; + var host = element.parentElement && element.parentElement.closest("[data-wrn-menu]") || element.closest("[data-ui-component]") || element.parentElement; var anchor = host ? host.getBoundingClientRect() : rect; var anchorVisible = anchor.bottom > 0 && @@ -3278,17 +3320,20 @@ export const REACTIVE_RUNTIME = String.raw` var shiftX = 0; var shiftY = 0; - if (rect.width < viewportWidth - ANCHOR_MARGIN * 2) { - if (rect.left < ANCHOR_MARGIN) shiftX = ANCHOR_MARGIN - rect.left; - else if (rect.right > viewportWidth - ANCHOR_MARGIN) { - shiftX = viewportWidth - ANCHOR_MARGIN - rect.right; + if (rect.width <= right - left) { + if (rect.left < left) shiftX = left - rect.left; + else if (rect.right > right) { + shiftX = right - rect.right; } } - if (rect.height < viewportHeight - ANCHOR_MARGIN * 2) { - if (rect.top < ANCHOR_MARGIN) shiftY = ANCHOR_MARGIN - rect.top; - else if (rect.bottom > viewportHeight - ANCHOR_MARGIN) { - shiftY = viewportHeight - ANCHOR_MARGIN - rect.bottom; + if (rect.height <= bottom - top) { + if (rect.top < top) shiftY = top - rect.top; + else if (rect.bottom > bottom) { + // Prefer opening above the trigger when there is enough room. + shiftY = anchor.top - rect.height - ANCHOR_MARGIN >= top && rect.top >= anchor.bottom + ? anchor.top - rect.height - ANCHOR_MARGIN - rect.top + : bottom - rect.bottom; } } @@ -3392,6 +3437,10 @@ export const REACTIVE_RUNTIME = String.raw` window.addEventListener("resize", scheduleAnchoredReposition); window.addEventListener("scroll", scheduleAnchoredReposition, true); + if (window.visualViewport) { + window.visualViewport.addEventListener("resize", scheduleAnchoredReposition); + window.visualViewport.addEventListener("scroll", scheduleAnchoredReposition); + } } /* diff --git a/packages/csr/test/anchored-overlays.test.ts b/packages/csr/test/anchored-overlays.test.ts new file mode 100644 index 00000000..8fdd4819 --- /dev/null +++ b/packages/csr/test/anchored-overlays.test.ts @@ -0,0 +1,50 @@ +import { test, expect } from "bun:test"; +import { Window } from "happy-dom"; +import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; + +function fixture(panel: Record, clipped = false) { + const win = new Window(); + win.document.body.innerHTML = '
'; + const element = win.document.querySelector('[data-wrn-anchored]')! as any; + const host = element.parentElement; + const rect = (left: number, top: number, width: number, height: number) => ({left, top, width, height, right:left+width, bottom:top+height}); + host.getBoundingClientRect = () => rect(270, 170, 40, 30); + element.getBoundingClientRect = () => rect(panel.left, panel.top, + Math.min(panel.width, parseFloat(element.style.maxWidth) || Infinity), + Math.min(panel.height, parseFloat(element.style.maxHeight) || Infinity)); + if (clipped) { + const clip = win.document.querySelector('#clip')! as any; + clip.style.overflow = 'hidden'; + clip.getBoundingClientRect = () => rect(100, 50, 220, 220); + Object.defineProperty(clip, 'clientWidth', {value:220}); + Object.defineProperty(clip, 'clientHeight', {value:220}); + } + const start = REACTIVE_RUNTIME.indexOf(' function isRenderedElement('); + const end = REACTIVE_RUNTIME.indexOf(' function repositionAnchored(', start); + const clamp = new Function('window', 'document', 'ANCHOR_MARGIN', REACTIVE_RUNTIME.slice(start,end) + '; return clampAnchored;')(win, win.document, 8); + return {win, element, clamp}; +} + +test('anchored panels fit clipping containers and positioning does not drift', async () => { + const {win, element, clamp} = fixture({left:40,top:208,width:180,height:100}, true); + clamp(element); + expect(element.style.translate).toBe('68px -146px'); + clamp(element); + expect(element.style.translate).toBe('68px -146px'); + await win.happyDOM.close(); +}); + +test('oversized panels become scrollable and recover authored sizing when space returns', async () => { + const {win, element, clamp} = fixture({left:40,top:60,width:500,height:400}, true); + element.style.minWidth = '300px'; + clamp(element); + expect(element.style.maxWidth).toBe('204px'); + expect(element.style.maxHeight).toBe('204px'); + expect(element.style.overflow).toBe('auto'); + element.parentElement.parentElement.style.overflow = 'visible'; + clamp(element); + expect(element.style.minWidth).toBe('300px'); + expect(element.style.maxWidth).toBe(''); + await win.happyDOM.close(); +}); + diff --git a/packages/design-studio/package.json b/packages/design-studio/package.json index 4f61a710..779ea844 100644 --- a/packages/design-studio/package.json +++ b/packages/design-studio/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/design-studio", - "version": "0.1.4", + "version": "0.1.5", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index e1239601..1b5ada95 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.8.83", + "version": "0.8.84", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ui/components/AdvancedSelect.wrn b/packages/ui/components/AdvancedSelect.wrn index 11af535d..8178b687 100644 --- a/packages/ui/components/AdvancedSelect.wrn +++ b/packages/ui/components/AdvancedSelect.wrn @@ -338,7 +338,7 @@ component AdvancedSelect {
diff --git a/packages/ui/components/Combobox.wrn b/packages/ui/components/Combobox.wrn index fb054051..77c9272e 100644 --- a/packages/ui/components/Combobox.wrn +++ b/packages/ui/components/Combobox.wrn @@ -293,7 +293,7 @@ component ComboBox {
diff --git a/packages/ui/components/DataTable.wrn b/packages/ui/components/DataTable.wrn index 6adab5b7..c592037b 100644 --- a/packages/ui/components/DataTable.wrn +++ b/packages/ui/components/DataTable.wrn @@ -959,7 +959,7 @@ component DataTable { slot so it stays reactive -- a prop would be fixed at the value the dropdown was first rendered with. -->
{#if cornerHint}{cornerHint}{/if}
{#if currentValue && !readonly && !disabled}{/if}
- + {#if helperText}{helperText}{/if}{error}
} diff --git a/packages/ui/components/Navbar.wrn b/packages/ui/components/Navbar.wrn index 48fa1259..90dbb69d 100644 --- a/packages/ui/components/Navbar.wrn +++ b/packages/ui/components/Navbar.wrn @@ -225,7 +225,7 @@ size: string = "default" {item.label} -
+
{#if item.description}

{item.description}

{/if} {#each dropdownItems(item) as child}
diff --git a/packages/ui/components/PreferenceSwitcher.wrn b/packages/ui/components/PreferenceSwitcher.wrn index c4d202f9..3d5dcb1d 100644 --- a/packages/ui/components/PreferenceSwitcher.wrn +++ b/packages/ui/components/PreferenceSwitcher.wrn @@ -50,7 +50,7 @@ size: string = "default" {#if !iconOnly && !compact}{themeLabel}{/if} -
+
{themeLabel}
@@ -64,7 +64,7 @@ size: string = "default" {#if !iconOnly && !compact}{colorLabel}{/if} -
+
{colorLabel}
{#each colors as item} @@ -80,7 +80,7 @@ size: string = "default" {#if showCurrentLanguage}{/if} {#if !iconOnly && !compact}{languageLabel}{/if} -
+
{languageLabel} {#each languages as item}
- + {#if helperText}{helperText}{/if} {error}
diff --git a/packages/ui/package.json b/packages/ui/package.json index cf935534..560d6e1d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.8.120", + "version": "0.8.121", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/test/account-shell-regressions.test.ts b/packages/ui/test/account-shell-regressions.test.ts index 1783532c..667948e2 100644 --- a/packages/ui/test/account-shell-regressions.test.ts +++ b/packages/ui/test/account-shell-regressions.test.ts @@ -246,3 +246,9 @@ describe("account shell UI regressions", () => { expect(source("Footer")).toMatch(/\.wrn-footer__copyright-right\s*\{\s*grid-column: 3;/); }); }); + +test('floating selection and preference panels opt into shared boundary handling', async () => { + for (const name of ['PreferenceSwitcher', 'ComboBox', 'AdvancedSelect', 'TimePicker', 'DataTable', 'Navbar']) { + expect(readFileSync(uiComponentPath(name), 'utf8')).toContain('data-wrn-anchored="true"'); + } +});