fix(ui): keep floating menus inside viewport and clipping containers
Quality / quality (ubuntu-latest) (push) Failing after 4s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-10-19 17:51:15 +05:30
1 parent 9520651330
commit 7c1fcd19d4
16 files changed
+138 -33

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.74",
"version": "0.8.75",
"type": "module",
"main": "src/index.ts",
"exports": {
+58 -9
View File
@@ -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);
}
}
/*
@@ -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<string, number>, clipped = false) {
const win = new Window();
win.document.body.innerHTML = '<div id="clip"><div data-ui-component="ComboBox"><div data-wrn-anchored></div></div></div>';
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();
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/design-studio",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.83",
"version": "0.8.84",
"type": "module",
"main": "src/index.ts",
"exports": {
+1 -1
View File
@@ -338,7 +338,7 @@ component AdvancedSelect {
</div>
<div
class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-wrn-anchored="true" class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-show="open"
style="{open ? '' : 'display: none'}"
data-wrn-owner="AdvancedSelect" data-wrn-part="selectDropdown" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}">
+1 -1
View File
@@ -293,7 +293,7 @@ component ComboBox {
</div>
<div
class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-wrn-anchored="true" class="wrn-next__select-dropdown {fixed ? 'wrn-next__select-dropdown--fixed' : ''}"
data-show="shouldShowDropdown()"
style="{shouldShowDropdown() ? '' : 'display: none'}"
data-wrn-owner="ComboBox" data-wrn-part="selectDropdown" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}">
+1 -1
View File
@@ -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. -->
<Dropdown
class="wrn-table__page-dropdown"
data-wrn-anchored="true" class="wrn-table__page-dropdown"
width="sm"
size="sm"
placement="top-end"
+1 -1
View File
@@ -157,7 +157,7 @@ size: string = "default"
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--date-picker {class}" data-variant="{variant}" data-inline="{inline}" data-invalid="{error ? 'true' : 'false'}" data-expanded="{expanded}" @keydown="calendarKeydown(event)" data-wrn-owner="DatePicker" data-wrn-part="root" data-wrn-component="DatePicker" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">
<div class="wrn-next__field-heading" data-wrn-owner="DatePicker" data-wrn-part="heading" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><label for="{id || name}" data-wrn-owner="DatePicker" data-wrn-part="label" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{label}</label>{#if cornerHint}<span class="wrn-next__field-hint" data-wrn-owner="DatePicker" data-wrn-part="hint" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{cornerHint}</span>{/if}</div>
<div class="wrn-next__picker-control" data-wrn-owner="DatePicker" data-wrn-part="pickerControl" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><input id="{id || name}" class="wrn-next__sr-only wrn-next__picker-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" data-wrn-owner="DatePicker" data-wrn-part="srOnly" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"/><button type="button" class="wrn-next__date-trigger" aria-label="{label}" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openCalendar(event)" @focus="handleFocus(event)" @blur="handleBlur(event)" data-wrn-owner="DatePicker" data-wrn-part="dateTrigger" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><span>{displayValue()}</span><i class="icon-[lucide--calendar-days]" aria-hidden="true"></i></button>{#if currentValue && !readonly && !disabled}<button type="button" class="wrn-next__picker-clear" aria-label="Clear date" @click="clearDate(event)" data-wrn-owner="DatePicker" data-wrn-part="pickerClear" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">×</button>{/if}</div>
<button type="button" class="wrn-picker-dismiss" aria-label="Close calendar" data-show="expanded" @click="finishDate(event)" data-wrn-owner="DatePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"></button><div data-show="expanded" class="wrn-next__calendar" role="dialog" aria-label="{label}" data-wrn-owner="DatePicker" data-wrn-part="calendar" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><div class="wrn-next__date-selectors" data-wrn-owner="DatePicker" data-wrn-part="dateSelectors" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><label data-wrn-owner="DatePicker" data-wrn-part="label" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)" data-wrn-owner="DatePicker" data-wrn-part="control" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label data-wrn-owner="DatePicker" data-wrn-part="label" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)" data-wrn-owner="DatePicker" data-wrn-part="control" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each calendarYears() as calendarYear}<option value="{calendarYear}" selected="{String(calendarYear) === selectedYear}" data-text="calendarYear"></option>{/each}</select></label></div><div class="wrn-next__calendar-weekdays" aria-hidden="true" data-wrn-owner="DatePicker" data-wrn-part="calendarWeekdays" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each weekdays() as weekday}<span>{weekday}</span>{/each}</div><div class="wrn-next__calendar-grid" data-wrn-owner="DatePicker" data-wrn-part="calendarGrid" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each calendarDays() as day}<button type="button" data-value="{day.value}" data-today="{day.today}" style="grid-column: {day.column}" disabled="{day.disabled}" aria-label="{day.label}" aria-pressed="{day.value === currentValue.slice(0, 10)}" @click="selectDay(event)" data-wrn-owner="DatePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{day.day}</button>{/each}</div>{#if type === "datetime-local"}<TimePicker label="Time" value="{selectedTime}" @change="selectedTime = payload.value; currentValue = (currentValue ? currentValue.slice(0, 10) : selectedYear + '-' + selectedMonth + '-01') + 'T' + selectedTime; output.change({ value: currentValue, name: name })" />{/if}<button type="button" class="wrn-next__date-done" disabled="{!currentValue}" @click="finishDate(event)" data-wrn-owner="DatePicker" data-wrn-part="dateDone" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Done</button></div>
<button type="button" class="wrn-picker-dismiss" aria-label="Close calendar" data-show="expanded" @click="finishDate(event)" data-wrn-owner="DatePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"></button><div data-show="expanded" data-wrn-anchored="true" class="wrn-next__calendar" role="dialog" aria-label="{label}" data-wrn-owner="DatePicker" data-wrn-part="calendar" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><div class="wrn-next__date-selectors" data-wrn-owner="DatePicker" data-wrn-part="dateSelectors" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><label data-wrn-owner="DatePicker" data-wrn-part="label" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Month<select value="{selectedMonth}" @change="updateDate('month', event.currentTarget.value, event)" data-wrn-owner="DatePicker" data-wrn-part="control" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each months as month}<option value="{month.value}" selected="{month.value === selectedMonth}">{month.label}</option>{/each}</select></label><label data-wrn-owner="DatePicker" data-wrn-part="label" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Year<select value="{selectedYear}" @change="updateDate('year', event.currentTarget.value, event)" data-wrn-owner="DatePicker" data-wrn-part="control" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each calendarYears() as calendarYear}<option value="{calendarYear}" selected="{String(calendarYear) === selectedYear}" data-text="calendarYear"></option>{/each}</select></label></div><div class="wrn-next__calendar-weekdays" aria-hidden="true" data-wrn-owner="DatePicker" data-wrn-part="calendarWeekdays" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each weekdays() as weekday}<span>{weekday}</span>{/each}</div><div class="wrn-next__calendar-grid" data-wrn-owner="DatePicker" data-wrn-part="calendarGrid" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each calendarDays() as day}<button type="button" data-value="{day.value}" data-today="{day.today}" style="grid-column: {day.column}" disabled="{day.disabled}" aria-label="{day.label}" aria-pressed="{day.value === currentValue.slice(0, 10)}" @click="selectDay(event)" data-wrn-owner="DatePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{day.day}</button>{/each}</div>{#if type === "datetime-local"}<TimePicker label="Time" value="{selectedTime}" @change="selectedTime = payload.value; currentValue = (currentValue ? currentValue.slice(0, 10) : selectedYear + '-' + selectedMonth + '-01') + 'T' + selectedTime; output.change({ value: currentValue, name: name })" />{/if}<button type="button" class="wrn-next__date-done" disabled="{!currentValue}" @click="finishDate(event)" data-wrn-owner="DatePicker" data-wrn-part="dateDone" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Done</button></div>
{#if helperText}<small class="wrn-next__field-help" data-wrn-owner="DatePicker" data-wrn-part="help" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{helperText}</small>{/if}<small class="wrn-next__field-error" data-error="{name}" data-wrn-owner="DatePicker" data-wrn-part="error" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{error}</small>
</div>
}
+1 -1
View File
@@ -225,7 +225,7 @@ size: string = "default"
<span data-t="{translationKey(item, 'label')}">{item.label}</span>
<span class="wrn-navbar__chevron" aria-hidden="true" data-wrn-owner="Navbar" data-wrn-part="chevron" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}"></span>
</button>
<div class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}" data-wrn-owner="Navbar" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}">
<div data-wrn-anchored="true" class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}" data-wrn-owner="Navbar" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}">
{#if item.description}<p class="wrn-navbar__panel-intro" data-t="{translationKey(item, 'description')}" data-wrn-owner="Navbar" data-wrn-part="panelIntro" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}">{item.description}</p>{/if}
{#each dropdownItems(item) as child}
<div class="wrn-navbar__group" data-wrn-owner="Navbar" data-wrn-part="group" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}">
@@ -50,7 +50,7 @@ size: string = "default"
<span class="{themeIcon}" aria-hidden="true"></span>
{#if !iconOnly && !compact}<span>{themeLabel}</span>{/if}
</button>
<div class="wrn-preferences__panel" data-wrn-owner="PreferenceSwitcher" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<div data-wrn-anchored="true" class="wrn-preferences__panel" data-wrn-owner="PreferenceSwitcher" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<strong>{themeLabel}</strong>
<div class="wrn-preferences__options" data-wrn-owner="PreferenceSwitcher" data-wrn-part="options" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<button type="button" data-wrn-theme-set="light" @click="choose('theme', 'light')" data-wrn-owner="PreferenceSwitcher" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}"><span class="icon-[lucide--sun]" aria-hidden="true"></span>Light</button>
@@ -64,7 +64,7 @@ size: string = "default"
<span class="{colorIcon}" aria-hidden="true"></span>
{#if !iconOnly && !compact}<span>{colorLabel}</span>{/if}
</button>
<div class="wrn-preferences__panel wrn-preferences__panel--colors" data-wrn-owner="PreferenceSwitcher" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<div data-wrn-anchored="true" class="wrn-preferences__panel wrn-preferences__panel--colors" data-wrn-owner="PreferenceSwitcher" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<strong>{colorLabel}</strong>
<div class="wrn-preferences__swatches" data-wrn-owner="PreferenceSwitcher" data-wrn-part="swatches" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
{#each colors as item}
@@ -80,7 +80,7 @@ size: string = "default"
{#if showCurrentLanguage}<span class="wrn-preferences__current-language" aria-hidden="true" data-wrn-owner="PreferenceSwitcher" data-wrn-part="currentLanguage" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">EN</span>{/if}
{#if !iconOnly && !compact}<span>{languageLabel}</span>{/if}
</button>
<div class="wrn-preferences__panel wrn-preferences__panel--language" data-wrn-owner="PreferenceSwitcher" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<div data-wrn-anchored="true" class="wrn-preferences__panel wrn-preferences__panel--language" data-wrn-owner="PreferenceSwitcher" data-wrn-part="panel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
<strong>{languageLabel}</strong>
{#each languages as item}
<button type="button" data-wrn-lang-set="{item.value}" lang="{item.value}" @click="choose('language', item.value)" data-wrn-owner="PreferenceSwitcher" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-compact="{compact}">
+1 -1
View File
@@ -58,7 +58,7 @@ size: string = "default"
<input id="{id || name}" class="wrn-next__sr-only wrn-next__time-value" type="text" name="{name}" value="{currentValue}" required="{required}" readonly aria-hidden="true" tabindex="-1" @invalid="handleInvalid(event)" data-wrn-owner="TimePicker" data-wrn-part="srOnly" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"/>
<button type="button" class="wrn-next__time-trigger" disabled="{disabled || readonly}" aria-haspopup="dialog" aria-expanded="{expanded}" @click="openPicker(event)" @focus="handleFocus(event)" @blur="handleBlur(event)" data-wrn-owner="TimePicker" data-wrn-part="timeTrigger" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><span>{currentValue || placeholder || "Select time"}</span><i class="{icon}" aria-hidden="true"></i></button>
</div>
<button type="button" class="wrn-time-dismiss" aria-label="Close time picker" data-show="expanded" @click="closePicker(event)" data-wrn-owner="TimePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"></button><div data-show="expanded" class="wrn-next__time-panel" role="dialog" aria-label="{label}" data-wrn-owner="TimePicker" data-wrn-part="timePanel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><div><strong>Hour</strong><div class="wrn-next__time-options" data-wrn-owner="TimePicker" data-wrn-part="timeOptions" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each hours as hour}<button type="button" data-part="hour" data-value="{hour}" aria-pressed="{!!currentValue && String(hour) === selectedHour}" @click="selectTimePart(event)" data-wrn-owner="TimePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{hour}</button>{/each}</div></div><div><strong>Minute</strong><div class="wrn-next__time-options" data-wrn-owner="TimePicker" data-wrn-part="timeOptions" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each minutes as minute}<button type="button" data-part="minute" data-value="{minute}" aria-pressed="{!!currentValue && String(minute) === selectedMinute}" @click="selectTimePart(event)" data-wrn-owner="TimePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{minute}</button>{/each}</div></div><button type="button" class="wrn-next__time-done" @click="closePicker(event)" data-wrn-owner="TimePicker" data-wrn-part="timeDone" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Done</button></div>
<button type="button" class="wrn-time-dismiss" aria-label="Close time picker" data-show="expanded" @click="closePicker(event)" data-wrn-owner="TimePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"></button><div data-show="expanded" data-wrn-anchored="true" class="wrn-next__time-panel" role="dialog" aria-label="{label}" data-wrn-owner="TimePicker" data-wrn-part="timePanel" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}"><div><strong>Hour</strong><div class="wrn-next__time-options" data-wrn-owner="TimePicker" data-wrn-part="timeOptions" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each hours as hour}<button type="button" data-part="hour" data-value="{hour}" aria-pressed="{!!currentValue && String(hour) === selectedHour}" @click="selectTimePart(event)" data-wrn-owner="TimePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{hour}</button>{/each}</div></div><div><strong>Minute</strong><div class="wrn-next__time-options" data-wrn-owner="TimePicker" data-wrn-part="timeOptions" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{#each minutes as minute}<button type="button" data-part="minute" data-value="{minute}" aria-pressed="{!!currentValue && String(minute) === selectedMinute}" @click="selectTimePart(event)" data-wrn-owner="TimePicker" data-wrn-part="button" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{minute}</button>{/each}</div></div><button type="button" class="wrn-next__time-done" @click="closePicker(event)" data-wrn-owner="TimePicker" data-wrn-part="timeDone" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">Done</button></div>
{#if helperText}<small class="wrn-next__field-help" data-wrn-owner="TimePicker" data-wrn-part="help" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{helperText}</small>{/if}
<small class="wrn-next__field-error" data-error="{name}" data-wrn-owner="TimePicker" data-wrn-part="error" data-wrn-axis-size="{size}" data-wrn-axis-color="{color}" data-wrn-axis-variant="{variant}">{error}</small>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/ui",
"version": "0.8.120",
"version": "0.8.121",
"private": true,
"type": "module",
"main": "src/index.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"');
}
});