Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
15 changes: 14 additions & 1 deletion src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/e2e/multi-select.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Multi-select control harness</title>
<link rel="stylesheet" href="/src/styles.css" />
<style>
body { padding: 24px; background: var(--bg); color: var(--fg); font-family: var(--ui); }
</style>
</head>
<body data-theme="dark">
<main id="root" aria-label="Multi-select control"></main>
<button type="button" class="ms-btn ms-btn-primary" disabled>Disabled Apply</button>
<script type="module">
import { buildMultiSelectField } from '/src/ui/multi-select-field.js';

const field = buildMultiSelectField({
document, name: 'city', label: 'City', value: [], active: false,
options: [{ value: 'mad', label: 'Madrid' }],
onApply: () => {}, onFallbackCommit: () => {},
});
document.querySelector('#root').append(field.el);
window.__ready = true;
</script>
</body>
</html>
38 changes: 38 additions & 0 deletions tests/e2e/multi-select.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});
}
});
Loading