From 6b10c128a8cc2fbdbc10c43076fb0ed42e0d120c Mon Sep 17 00:00:00 2001 From: Boris Tyshkevich Date: Wed, 22 Jul 2026 19:39:13 +0200 Subject: [PATCH] fix(#386): strengthen multiselect Apply states Co-Authored-By: Codex Claude-Session: codex-ship-386 --- CHANGELOG.md | 6 ++++++ src/styles.css | 15 +++++++++++++- tests/e2e/multi-select.html | 26 +++++++++++++++++++++++ tests/e2e/multi-select.spec.js | 38 ++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/multi-select.html create mode 100644 tests/e2e/multi-select.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d113f1..eb3d15a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Make the Dashboard multi-select Apply button remain a clear primary action on hover, while preserving distinct disabled, keyboard-focus, and pressed states (#386). + All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/src/styles.css b/src/styles.css index 7328184..b47dddc 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1599,7 +1599,20 @@ body.detached-tab .graph-overlay-panel { .ms-btn:hover { background: var(--bg-hover); } .ms-btn-clear { margin-right: auto; } .ms-btn-primary { border: none; background: var(--accent); color: #fff; font-weight: 600; } -.ms-btn-primary:hover { filter: brightness(1.08); } +/* The generic `.ms-btn:hover` rule is deliberately overridden here: it is + more specific than the base primary rule and otherwise turns Apply into a + faint neutral surface on hover. Keep every action state explicit so the + primary affordance remains distinct in both themes. */ +.ms-btn-primary:hover:not(:disabled) { background: var(--accent-dim); } +.ms-btn-primary:focus-visible { + outline: 2px solid var(--accent); outline-offset: 2px; + box-shadow: 0 0 0 2px var(--bg-panel, var(--bg-editor)); +} +.ms-btn-primary:active:not(:disabled) { background: var(--accent-dim); transform: translateY(1px); } +.ms-btn-primary:disabled { + border: 1px solid var(--border); background: var(--bg-input); color: var(--fg-faint); + cursor: not-allowed; opacity: 1; +} /* Compound time-range control (#335, time-range-field.ts) — the SECOND consumer of the #364 dialog-popover pattern (openAnchoredDialog). The trigger reuses .var-input's sizing/border like .ms-trigger; the popover is diff --git a/tests/e2e/multi-select.html b/tests/e2e/multi-select.html new file mode 100644 index 0000000..63bb8d7 --- /dev/null +++ b/tests/e2e/multi-select.html @@ -0,0 +1,26 @@ + + + + + Multi-select control harness + + + + +
+ + + + diff --git a/tests/e2e/multi-select.spec.js b/tests/e2e/multi-select.spec.js new file mode 100644 index 0000000..38485ba --- /dev/null +++ b/tests/e2e/multi-select.spec.js @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Multi-select Apply action states (#386)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/tests/e2e/multi-select.html'); + await page.waitForFunction(() => window.__ready === true); + }); + + for (const theme of ['dark', 'light']) { + test(`keeps enabled, hover, disabled, focus, and pressed Apply states distinct in ${theme} theme`, async ({ page }) => { + await page.locator('body').evaluate((el, nextTheme) => { el.dataset.theme = nextTheme; }, theme); + await page.getByRole('button', { name: 'City filter, 0 selected' }).click(); + + const apply = page.getByRole('button', { name: 'Apply' }); + const disabled = page.getByRole('button', { name: 'Disabled Apply' }); + const styles = (target) => target.evaluate((el) => { + const css = getComputedStyle(el); + return { background: css.backgroundColor, color: css.color, outline: css.outlineStyle, transform: css.transform }; + }); + + const enabled = await styles(apply); + expect(enabled.background).not.toBe('rgba(0, 0, 0, 0)'); + await apply.hover(); + const hovered = await styles(apply); + expect(hovered.background).not.toBe(enabled.background); + expect(await styles(disabled)).not.toEqual(hovered); + + await apply.focus(); + expect((await styles(apply)).outline).not.toBe('none'); + + const box = await apply.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + expect((await styles(apply)).transform).not.toBe('none'); + await page.mouse.up(); + }); + } +});