mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
feat(dashboard): reveal widget card chrome on hover for fine-pointer devices (#111914)
* feat(dashboard): reveal widget card chrome on hover for fine-pointer devices * test(dashboard): deterministic focus sink for widget chrome hide assertions * test(dashboard): finish animations before asserting widget chrome hides
This commit is contained in:
@@ -88,6 +88,72 @@ describe.skipIf(!hasBrowserLayout)("openclaw-board-view browser layout", () => {
|
||||
expect(Math.round(first?.height ?? 0)).toBe(BOARD_GRID_ROW_HEIGHT * 3 + BOARD_GRID_GAP * 2);
|
||||
});
|
||||
|
||||
it("hides widget chrome by default on fine-pointer devices", async () => {
|
||||
const view = await mount();
|
||||
const bar = view.querySelector<HTMLElement>(".board-widget__bar");
|
||||
const handle = view.querySelector<HTMLElement>(".board-widget__resize-handle");
|
||||
expect(getComputedStyle(bar!).visibility).toBe("hidden");
|
||||
expect(getComputedStyle(handle!).visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
// Chrome must stay revealed while focus is anywhere inside the cell (menu
|
||||
// close restores focus to its trigger), so hiding is proven by moving focus
|
||||
// to an outside sink rather than blur(), which leaves focus placement to the
|
||||
// platform and flakes on Linux.
|
||||
function focusSink(): HTMLButtonElement {
|
||||
const sink = document.createElement("button");
|
||||
sink.type = "button";
|
||||
document.body.append(sink);
|
||||
return sink;
|
||||
}
|
||||
|
||||
// Headless CI renderers can stall the animation timeline, leaving the 120ms
|
||||
// hide transition permanently mid-flight; finishing transitions asserts the
|
||||
// target visibility state instead of the renderer's clock. A genuinely
|
||||
// matching reveal selector still fails: its finished end state is visible.
|
||||
function expectChromeHidden(widget: HTMLElement, bar: HTMLElement): void {
|
||||
for (const animation of document.getAnimations()) {
|
||||
animation.finish();
|
||||
}
|
||||
const revealState = JSON.stringify({
|
||||
hover: widget.matches(":hover"),
|
||||
focusWithin: widget.matches(":focus-within"),
|
||||
dragging: widget.classList.contains("board-widget--dragging"),
|
||||
menuOpen: widget.querySelector(".board-widget__menu[open]") !== null,
|
||||
});
|
||||
expect(getComputedStyle(bar).visibility, revealState).toBe("hidden");
|
||||
}
|
||||
|
||||
it("reveals widget chrome while the widget has focus", async () => {
|
||||
const view = await mount();
|
||||
const sink = focusSink();
|
||||
const widget = view.querySelector<HTMLElement>('[data-test-id="board-widget"]');
|
||||
const bar = widget!.querySelector<HTMLElement>(".board-widget__bar");
|
||||
|
||||
widget!.focus();
|
||||
expect(getComputedStyle(bar!).visibility).toBe("visible");
|
||||
|
||||
sink.focus();
|
||||
expect(widget!.matches(":focus-within")).toBe(false);
|
||||
await vi.waitFor(() => expectChromeHidden(widget!, bar!));
|
||||
});
|
||||
|
||||
it("keeps widget chrome visible while its menu is open", async () => {
|
||||
const view = await mount();
|
||||
const sink = focusSink();
|
||||
const widget = view.querySelector<HTMLElement>('[data-test-id="board-widget"]');
|
||||
const bar = widget!.querySelector<HTMLElement>(".board-widget__bar");
|
||||
const menu = widget!.querySelector<HTMLElement & { open: boolean }>(".board-widget__menu");
|
||||
|
||||
menu!.open = true;
|
||||
await vi.waitFor(() => expect(getComputedStyle(bar!).visibility).toBe("visible"));
|
||||
|
||||
menu!.open = false;
|
||||
sink.focus();
|
||||
expect(widget!.matches(":focus-within")).toBe(false);
|
||||
await vi.waitFor(() => expectChromeHidden(widget!, bar!));
|
||||
});
|
||||
|
||||
it("snaps pointer resize to columns and rows before committing", async () => {
|
||||
const applyOps = vi.fn(async () => undefined);
|
||||
const view = await mount(applyOps);
|
||||
|
||||
@@ -461,6 +461,59 @@ openclaw-board-widget-cell {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
/* Focus keeps keyboard actions reachable; menu-open and dragging keep chrome
|
||||
pinned when interaction leaves the card bounds. */
|
||||
.board-widget {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.board-widget__bar {
|
||||
backdrop-filter: blur(6px);
|
||||
background: color-mix(in srgb, var(--board-surface) 92%, transparent);
|
||||
border-radius: 11px 11px 0 0;
|
||||
height: 38px;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition:
|
||||
opacity 120ms ease,
|
||||
visibility 0s linear 120ms;
|
||||
visibility: hidden;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.board-widget__resize-handle {
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 120ms ease,
|
||||
visibility 0s linear 120ms;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.board-widget:hover .board-widget__bar,
|
||||
.board-widget:hover .board-widget__resize-handle,
|
||||
.board-widget:focus-within .board-widget__bar,
|
||||
.board-widget:focus-within .board-widget__resize-handle,
|
||||
.board-widget--dragging .board-widget__bar,
|
||||
.board-widget--dragging .board-widget__resize-handle,
|
||||
.board-widget:has(.board-widget__menu[open]) .board-widget__bar,
|
||||
.board-widget:has(.board-widget__menu[open]) .board-widget__resize-handle {
|
||||
opacity: 1;
|
||||
/* Delay-only override: reveal flips visibility immediately while the base
|
||||
shorthand keeps the fade, and reduced-motion's transition: none stays
|
||||
authoritative because no transition-property is redeclared here. */
|
||||
transition-delay: 0s;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.board-widget__body {
|
||||
border-radius: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-widget__grant,
|
||||
.board-widget__error {
|
||||
align-content: center;
|
||||
@@ -622,6 +675,8 @@ openclaw-board-widget-cell {
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.board-widget,
|
||||
.board-widget__bar,
|
||||
.board-widget__resize-handle,
|
||||
.board-tabs__tab,
|
||||
.swarm-widget__dot--running {
|
||||
animation: none;
|
||||
|
||||
Reference in New Issue
Block a user