Skip to content

Commit 5790445

Browse files
chore: use html dialog element for file-picker (#3531)
1 parent b9a6e4e commit 5790445

File tree

6 files changed

+88
-63
lines changed

6 files changed

+88
-63
lines changed

packages/elements/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "3.4.0",
44
"description": "A suite of web components for a mutation testing report.",
55
"unpkg": "dist/mutation-test-elements.js",
6+
"browser": "dist/mutation-test-elements.js",
67
"main": "dist/index.cjs",
78
"module": "dist/index.js",
89
"types": "./dist-tsc/src/index.d.ts",

packages/elements/src/components/breadcrumb.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ export class MutationTestReportBreadcrumbComponent extends LitElement {
7878
}
7979

8080
#dispatchFilePickerOpenEvent() {
81+
// Move focus out of the button to the dialog
82+
// In Chrome we need to call on `this`, on Firefox we need to call on the button
83+
this.blur();
84+
this.renderRoot.querySelector('button')?.blur();
85+
8186
this.dispatchEvent(createCustomEvent('mte-file-picker-open', undefined));
8287
}
8388
}

packages/elements/src/components/file-picker/file-picker.component.ts

Lines changed: 47 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fuzzysort from 'fuzzysort';
22
import type { TemplateResult } from 'lit';
3-
import { html, LitElement, nothing, type PropertyValues } from 'lit';
4-
import { customElement, property, state } from 'lit/decorators.js';
3+
import { html, LitElement, type PropertyValues } from 'lit';
4+
import { customElement, property, query, state } from 'lit/decorators.js';
55
import { repeat } from 'lit/directives/repeat.js';
66
import type { FileUnderTestModel, Metrics, MetricsResult, MutationTestMetricsResult, TestMetrics } from 'mutation-testing-metrics';
77
import { TestFileModel } from 'mutation-testing-metrics';
@@ -27,20 +27,24 @@ export class MutationTestReportFilePickerComponent extends LitElement {
2727
@property({ attribute: false })
2828
public declare rootModel: MutationTestMetricsResult | undefined;
2929

30-
@state()
31-
public declare openPicker: boolean;
32-
3330
@state()
3431
public declare filteredFiles: (ModelEntry & { template?: (string | TemplateResult)[] })[];
3532

3633
@state()
3734
public declare fileIndex: number;
3835

36+
@query('dialog')
37+
private declare dialog: HTMLDialogElement;
38+
39+
get isOpen() {
40+
return this.dialog.open;
41+
}
42+
3943
constructor() {
4044
super();
4145

42-
this.openPicker = false;
4346
this.fileIndex = 0;
47+
this.filteredFiles = [];
4448
}
4549

4650
connectedCallback(): void {
@@ -64,38 +68,26 @@ export class MutationTestReportFilePickerComponent extends LitElement {
6468
}
6569
}
6670

67-
updated(changedProperties: PropertyValues<this>): void {
68-
if (changedProperties.has('openPicker')) {
69-
if (this.openPicker) {
70-
document.body.style.overflow = 'hidden';
71-
this.#focusInput();
72-
} else {
73-
document.body.style.overflow = this.#originalDocumentOverflow;
74-
}
75-
}
76-
}
71+
open = () => {
72+
this.dialog.showModal();
73+
};
7774

78-
open() {
79-
this.openPicker = true;
80-
}
75+
close = () => {
76+
this.dialog.close();
77+
};
8178

8279
render() {
83-
if (!this.openPicker) {
84-
return nothing;
85-
}
86-
8780
return html`
88-
<div
89-
id="backdrop"
90-
@click="${this.#closePicker}"
91-
class="fixed left-0 top-0 z-50 flex h-full w-full justify-center bg-gray-950/50 backdrop-blur-lg"
81+
<dialog
82+
@click="${this.close}"
83+
@close="${this.#handleClose}"
84+
@show="${this.#handleShow}"
85+
aria-labelledby="file-picker-label"
86+
class="my-4 max-w-[40rem] bg-transparent backdrop:bg-gray-950/50 backdrop:backdrop-blur-lg md:w-1/2"
9287
>
9388
<div
9489
@click="${(e: MouseEvent) => e.stopPropagation()}"
95-
role="dialog"
96-
aria-labelledby="file-picker-label"
97-
id="picker"
98-
class="m-4 flex h-fit max-h-[33rem] w-full max-w-[40rem] flex-col rounded-lg bg-gray-200/60 p-4 backdrop-blur-lg md:w-1/2"
90+
class="mx-auto flex h-fit max-h-[33rem] flex-col rounded-lg bg-gray-200/60 p-4 backdrop-blur-lg"
9991
>
10092
<div class="mb-3 flex items-center rounded bg-gray-200/60 p-2 text-gray-800 shadow-lg">
10193
<div class="mx-2 flex items-center">${searchIcon}</div>
@@ -113,20 +105,13 @@ export class MutationTestReportFilePickerComponent extends LitElement {
113105
</div>
114106
${this.#renderFoundFiles()}
115107
</div>
116-
</div>
108+
</dialog>
117109
`;
118110
}
119111

120112
#renderFoundFiles() {
121113
return html`
122-
<ul
123-
id="files"
124-
tabindex="-1"
125-
class="flex snap-y flex-col gap-2 overflow-auto"
126-
role="listbox"
127-
@focusout="${this.#focusInput}"
128-
aria-labelledby="file-picker-label"
129-
>
114+
<ul id="files" tabindex="-1" class="flex snap-y flex-col gap-2 overflow-auto" role="listbox" aria-labelledby="file-picker-label">
130115
${renderIf(this.filteredFiles.length === 0, () => html`<li class="text-gray-800">No files found</li>`)}
131116
${repeat(
132117
this.filteredFiles,
@@ -141,7 +126,7 @@ export class MutationTestReportFilePickerComponent extends LitElement {
141126
>
142127
<a
143128
tabindex="${index === this.fileIndex ? 0 : -1}"
144-
@click="${this.#closePicker}"
129+
@click="${this.close}"
145130
class="flex h-full flex-wrap items-center p-2 outline-none"
146131
@mousemove="${() => (this.fileIndex = index)}"
147132
href="${toAbsoluteUrl(view, name)}"
@@ -200,13 +185,15 @@ export class MutationTestReportFilePickerComponent extends LitElement {
200185
}
201186

202187
#handleKeyDown = (event: KeyboardEvent) => {
203-
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
188+
if (((event.ctrlKey || event.metaKey) && event.key === 'k') || (!this.isOpen && event.key === '/')) {
204189
this.#togglePicker(event);
205-
} else if (!this.openPicker && event.key === '/') {
206-
this.#togglePicker(event);
207-
} else if (event.key === 'Escape') {
208-
this.#closePicker();
209-
} else if (event.key === 'ArrowUp') {
190+
}
191+
192+
if (!this.isOpen) {
193+
return;
194+
}
195+
196+
if (event.key === 'ArrowUp') {
210197
this.#handleArrowUp();
211198
} else if (event.key === 'ArrowDown') {
212199
this.#handleArrowDown();
@@ -253,28 +240,32 @@ export class MutationTestReportFilePickerComponent extends LitElement {
253240

254241
const entry = this.filteredFiles[this.fileIndex];
255242
window.location.href = toAbsoluteUrl(this.#getView(entry.file), entry.name);
256-
this.#closePicker();
243+
this.close();
257244
}
258245

259246
#togglePicker = (event: KeyboardEvent | null = null) => {
260247
event?.preventDefault();
261248
event?.stopPropagation();
262249

263-
this.openPicker = !this.openPicker;
264-
};
265-
266-
#focusInput = () => {
267-
this.renderRoot.querySelector('input')?.focus();
250+
if (this.isOpen) {
251+
this.close();
252+
} else {
253+
this.open();
254+
}
268255
};
269256

270-
#closePicker = () => {
271-
this.openPicker = false;
257+
#handleClose = () => {
272258
this.fileIndex = 0;
273259
this.#filter('');
260+
document.body.style.overflow = this.#originalDocumentOverflow;
261+
};
262+
263+
#handleShow = () => {
264+
document.body.style.overflow = 'hidden';
274265
};
275266

276267
#handleSearch = (event: InputEvent) => {
277-
if (!this.openPicker) {
268+
if (!this.isOpen) {
278269
return;
279270
}
280271

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ReportPage } from './po/ReportPage.js';
2+
import { test, expect } from '@playwright/test';
3+
4+
test.describe('FilePicker', () => {
5+
let page: ReportPage;
6+
7+
test.beforeEach(async ({ page: p }) => {
8+
page = new ReportPage(p);
9+
await page.navigateTo('test-files-example/#mutant/deep-merge.ts');
10+
});
11+
12+
test('clicking search should open the file picker', async () => {
13+
const filePicker = await page.breadcrumb().openFilePicker();
14+
await expect(filePicker.picker()).toBeVisible();
15+
});
16+
17+
test('pressing enter should open the file and close the file picker', async ({ page: p }) => {
18+
const filePicker = await page.breadcrumb().openFilePicker();
19+
20+
await p.keyboard.press('Enter');
21+
await page.mutantView.waitForVisible();
22+
await expect(filePicker.picker()).not.toBeVisible();
23+
});
24+
});

packages/elements/test/integration/po/FilePicker.po.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ export class FilePicker extends PageObject {
88
public async results() {
99
return this.$$('#files li');
1010
}
11+
12+
public picker() {
13+
return this.$('dialog');
14+
}
1115
}

packages/elements/test/unit/components/file-picker.component.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe(MutationTestReportFilePickerComponent.name, () => {
2121
await sut.whenStable();
2222

2323
// Assert
24-
expect(getPicker()).not.toBeInTheDocument();
24+
expect(getPicker()).not.toBeVisible();
2525
});
2626

2727
it('should show the picker when keycombo is pressed', async () => {
@@ -85,7 +85,7 @@ describe(MutationTestReportFilePickerComponent.name, () => {
8585
await openPicker();
8686

8787
// Assert
88-
expect(getPicker()).not.toBeInTheDocument();
88+
expect(getPicker()).not.toBeVisible();
8989
});
9090

9191
it('should close the picker when the escape key is pressed', async () => {
@@ -94,17 +94,17 @@ describe(MutationTestReportFilePickerComponent.name, () => {
9494
await sut.whenStable();
9595

9696
// Assert
97-
expect(getPicker()).not.toBeInTheDocument();
97+
expect(getPicker()).not.toBeVisible();
9898
});
9999

100100
it('should close the picker when clicking outside the dialog', async () => {
101101
// Act
102-
const backdrop = sut.$('#backdrop');
102+
const backdrop = sut.$('dialog');
103103
backdrop.click();
104104
await sut.whenStable();
105105

106106
// Assert
107-
expect(getPicker()).not.toBeInTheDocument();
107+
expect(getPicker()).not.toBeVisible();
108108
});
109109

110110
describe('when not typing in the search box', () => {
@@ -226,14 +226,14 @@ describe(MutationTestReportFilePickerComponent.name, () => {
226226
}
227227

228228
function getPicker() {
229-
return sut.$('#picker');
229+
return sut.$<HTMLDialogElement>('dialog');
230230
}
231231

232232
function getActiveItem() {
233233
return sut.$<HTMLAnchorElement>('[aria-selected="true"] a');
234234
}
235235

236236
function getFilePickerInput() {
237-
return sut.$<HTMLInputElement>('#file-picker-input');
237+
return sut.$<HTMLInputElement>('input');
238238
}
239239
});

0 commit comments

Comments
 (0)