Skip to content

Commit 6eda2c9

Browse files
committed
feat/filter panel as dialog
1 parent c2fda56 commit 6eda2c9

8 files changed

Lines changed: 206 additions & 91 deletions

File tree

docs

Submodule docs updated 372 files

e2e/filtering.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,63 @@ test.describe('filtering', () => {
7878
await expect(filterPanel).not.toBeVisible();
7979
});
8080

81+
test('renders filter panel as a native dialog outside overflow clipping', async ({ page }) => {
82+
const source: SampleRow[] = [
83+
{ id: 501, name: 'Alice', role: 'Admin', city: 'Lisbon' },
84+
{ id: 502, name: 'Ben', role: 'Engineer', city: 'Porto' },
85+
];
86+
87+
const columns = buildColumns([
88+
{ prop: 'id', name: 'ID' },
89+
{ prop: 'name', name: 'Name' },
90+
{ prop: 'role', name: 'Role', filter: true, ...withHeaderTestId('dialog-filter-role') },
91+
{ prop: 'city', name: 'City' },
92+
]);
93+
94+
await mountGrid(page, { columns, source, filter: true, height: 72 });
95+
await page.locator('revo-grid').evaluate(grid => {
96+
const wrapper = grid.parentElement;
97+
if (!wrapper) {
98+
throw new Error('Grid wrapper was not found');
99+
}
100+
wrapper.style.overflow = 'hidden';
101+
});
102+
103+
await page
104+
.getByTestId('dialog-filter-role')
105+
.locator(SELECTORS.filterButton)
106+
.click();
107+
108+
const filterPanel = page.locator(SELECTORS.filterPanel);
109+
await expect(filterPanel).toBeVisible();
110+
await expect(filterPanel).toHaveJSProperty('open', true);
111+
112+
const { buttonBottom, buttonLeft, panelBottom, panelLeft, panelTop, wrapperBottom } = await page.evaluate(() => {
113+
const dialog = document.querySelector('revogr-filter-panel dialog');
114+
const button = document
115+
.querySelector('[data-testid="dialog-filter-role"]')
116+
?.querySelector('.rv-filter');
117+
const wrapper = document.querySelector('revo-grid')?.parentElement;
118+
if (!button || !dialog || !wrapper) {
119+
throw new Error('Filter button, dialog, or grid wrapper was not found');
120+
}
121+
const buttonRect = button.getBoundingClientRect();
122+
const panelRect = dialog.getBoundingClientRect();
123+
return {
124+
buttonBottom: buttonRect.bottom,
125+
buttonLeft: buttonRect.left,
126+
panelBottom: panelRect.bottom,
127+
panelLeft: panelRect.left,
128+
panelTop: panelRect.top,
129+
wrapperBottom: wrapper.getBoundingClientRect().bottom,
130+
};
131+
});
132+
133+
expect(panelLeft).toBeCloseTo(buttonLeft, 0);
134+
expect(panelTop).toBeCloseTo(buttonBottom, 0);
135+
expect(panelBottom).toBeGreaterThan(wrapperBottom);
136+
});
137+
81138
test('reapplies active filters after source replacement', async ({ page }) => {
82139
const source: SampleRow[] = [
83140
{ id: 501, name: 'Alice', role: 'Admin', city: 'Lisbon' },

e2e/helpers/selectors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export const SELECTORS = {
1010
pinnedTopRows: 'revogr-data[type="rowPinStart"] .rgRow',
1111
pinnedBottomRows: 'revogr-data[type="rowPinEnd"] .rgRow',
1212
filterButton: '.rv-filter',
13-
filterPanel: 'revogr-filter-panel',
14-
filterInput: 'revogr-filter-panel input[placeholder="Enter value..."]',
13+
filterPanel: 'revogr-filter-panel dialog',
14+
filterInput: 'revogr-filter-panel dialog input[placeholder="Enter value..."]',
1515
editInput: 'revo-grid revogr-edit input',
1616
focusedCell: 'revo-grid revogr-focus.focused-cell',
1717
selectedRange: '.selection-border-range',

src/plugins/filter/filter.panel.tsx

Lines changed: 126 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const FILTER_ID = 'add-filter';
4343
styleUrl: 'filter.style.scss',
4444
})
4545
export class FilterPanel {
46+
private dialog?: HTMLDialogElement;
4647
private filterCaptionsInternal: FilterCaptions = {
4748
title: 'Filter by',
4849
ok: 'Close',
@@ -187,22 +188,67 @@ export class FilterPanel {
187188
);
188189
}
189190

191+
componentDidRender() {
192+
this.syncDialog();
193+
}
194+
195+
private syncDialog() {
196+
if (!this.dialog) {
197+
return;
198+
}
199+
200+
if (!this.changes) {
201+
if (this.dialog.open) {
202+
this.dialog.close();
203+
}
204+
return;
205+
}
206+
207+
if (!this.dialog.open) {
208+
this.dialog.show();
209+
}
210+
211+
if (this.changes.autoCorrect !== false) {
212+
requestAnimationFrame(() => this.autoCorrect(this.dialog));
213+
}
214+
}
215+
190216
private autoCorrect(el?: HTMLElement | null) {
191217
if (!el) {
192218
return;
193219
}
194220

195-
const revoGrid = el.closest('revo-grid');
196-
if (!revoGrid) {
221+
const pos = el.getBoundingClientRect();
222+
const maxLeft = Math.max(0, window.innerWidth - pos.width);
223+
const maxTop = Math.max(0, window.innerHeight - pos.height);
224+
225+
if (pos.left > maxLeft) {
226+
el.style.left = `${maxLeft}px`;
227+
}
228+
229+
if (pos.top > maxTop) {
230+
el.style.top = `${maxTop}px`;
231+
}
232+
}
233+
234+
private onDialogMouseDown(e: MouseEvent) {
235+
if (
236+
!this.closeOnOutsideClick ||
237+
e.target !== this.dialog ||
238+
!this.dialog
239+
) {
197240
return;
198241
}
199242

200-
const pos = el.getBoundingClientRect();
201-
const gridPos = revoGrid.getBoundingClientRect();
202-
const maxLeft = gridPos.right - pos.width;
243+
const rect = this.dialog.getBoundingClientRect();
244+
const isInside =
245+
e.clientX >= rect.left &&
246+
e.clientX <= rect.right &&
247+
e.clientY >= rect.top &&
248+
e.clientY <= rect.bottom;
203249

204-
if (pos.left > maxLeft && el.offsetLeft) {
205-
el.style.left = `${maxLeft - (el.parentElement?.getBoundingClientRect().left ?? 0)}px`;
250+
if (!isInside) {
251+
this.onCancel();
206252
}
207253
}
208254

@@ -469,13 +515,9 @@ export class FilterPanel {
469515
}
470516

471517
render() {
472-
if (!this.changes) {
473-
return <Host style={{ display: 'none' }}></Host>;
474-
}
475518
const style = {
476-
display: 'block',
477-
left: `${this.changes.x}px`,
478-
top: `${this.changes.y}px`,
519+
left: `${this.changes?.x ?? 0}px`,
520+
top: `${this.changes?.y ?? 0}px`,
479521
};
480522

481523
const capts = Object.assign(
@@ -484,72 +526,78 @@ export class FilterPanel {
484526
);
485527

486528
return (
487-
<Host
488-
style={style}
489-
ref={el => {
490-
this.changes?.autoCorrect !== false && this.autoCorrect(el);
491-
}}
492-
>
493-
<slot slot="header" />
494-
{ this.changes.extraContent?.(this.changes) || '' }
495-
496-
{ this.changes?.hideDefaultFilters !== true && (
497-
[
498-
<label>{capts.title}</label>,
499-
<div class="filter-holder">{this.getFilterItemsList()}</div>,
500-
<div class="add-filter">
501-
<select
502-
id={FILTER_ID}
503-
class="select-css"
504-
onChange={e => this.onAddNewFilter(e)}
505-
>
506-
{this.renderSelectOptions(this.currentFilterType)}
507-
</select>
508-
</div>
509-
]
510-
)}
511-
512-
<slot />
513-
<div class="filter-actions">
514-
{this.disableDynamicFiltering && [
515-
<button
516-
id="revo-button-save"
517-
aria-label="save"
518-
class="revo-button green"
519-
onClick={() => this.onSave()}
520-
>
521-
{capts.save}
522-
</button>,
523-
<button
524-
id="revo-button-ok"
525-
aria-label="ok"
526-
class="revo-button green"
527-
onClick={() => this.onCancel()}
528-
>
529-
{capts.cancel}
530-
</button>,
531-
]}
532-
{!this.disableDynamicFiltering && [
533-
<button
534-
id="revo-button-ok"
535-
aria-label="ok"
536-
class="revo-button green"
537-
onClick={() => this.onCancel()}
538-
>
539-
{capts.ok}
540-
</button>,
541-
542-
<button
543-
id="revo-button-reset"
544-
aria-label="reset"
545-
class="revo-button outline"
546-
onClick={() => this.onReset()}
547-
>
548-
{capts.reset}
549-
</button>,
529+
<Host>
530+
<dialog
531+
class="filter-panel-dialog"
532+
style={style}
533+
ref={el => (this.dialog = el)}
534+
onCancel={e => {
535+
e.preventDefault();
536+
this.onCancel();
537+
}}
538+
onMouseDown={e => this.onDialogMouseDown(e)}
539+
>
540+
{this.changes && [
541+
<slot slot="header" />,
542+
this.changes.extraContent?.(this.changes) || '',
543+
544+
this.changes?.hideDefaultFilters !== true && [
545+
<label>{capts.title}</label>,
546+
<div class="filter-holder">{this.getFilterItemsList()}</div>,
547+
<div class="add-filter">
548+
<select
549+
id={FILTER_ID}
550+
class="select-css"
551+
onChange={e => this.onAddNewFilter(e)}
552+
>
553+
{this.renderSelectOptions(this.currentFilterType)}
554+
</select>
555+
</div>,
556+
],
557+
558+
<slot />,
559+
<div class="filter-actions">
560+
{this.disableDynamicFiltering && [
561+
<button
562+
id="revo-button-save"
563+
aria-label="save"
564+
class="revo-button green"
565+
onClick={() => this.onSave()}
566+
>
567+
{capts.save}
568+
</button>,
569+
<button
570+
id="revo-button-ok"
571+
aria-label="ok"
572+
class="revo-button green"
573+
onClick={() => this.onCancel()}
574+
>
575+
{capts.cancel}
576+
</button>,
577+
]}
578+
{!this.disableDynamicFiltering && [
579+
<button
580+
id="revo-button-ok"
581+
aria-label="ok"
582+
class="revo-button green"
583+
onClick={() => this.onCancel()}
584+
>
585+
{capts.ok}
586+
</button>,
587+
588+
<button
589+
id="revo-button-reset"
590+
aria-label="reset"
591+
class="revo-button outline"
592+
onClick={() => this.onReset()}
593+
>
594+
{capts.reset}
595+
</button>,
596+
]}
597+
</div>,
598+
<slot slot="footer" />,
550599
]}
551-
</div>
552-
<slot slot="footer" />
600+
</dialog>
553601
</Host>
554602
);
555603
}

src/plugins/filter/filter.plugin.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ export class FilterPlugin extends BasePlugin {
243243

244244
async headerclick(e: CustomEvent<ColumnRegular>) {
245245
const el = e.detail.originalEvent?.target as HTMLElement;
246-
if (!isFilterBtn(el)) {
246+
const filterButton = isFilterBtn(el);
247+
if (!filterButton) {
247248
return;
248249
}
249250
e.preventDefault();
@@ -259,14 +260,15 @@ export class FilterPlugin extends BasePlugin {
259260
}
260261

261262
// filter button clicked, open filter dialog
262-
const gridPos = this.revogrid.getBoundingClientRect();
263-
const buttonPos = el.getBoundingClientRect();
263+
const buttonPos = (
264+
filterButton instanceof HTMLElement ? filterButton : el
265+
).getBoundingClientRect();
264266

265267
const data: ShowData = {
266268
...e.detail,
267269
...this.filterCollection[prop],
268-
x: buttonPos.x - gridPos.x,
269-
y: buttonPos.y - gridPos.y + buttonPos.height,
270+
x: buttonPos.x,
271+
y: buttonPos.y + buttonPos.height,
270272
autoCorrect: true,
271273
filterTypes: this.getColumnFilter(e.detail.filter),
272274
filterItems: this.multiFilterItems,

0 commit comments

Comments
 (0)