Skip to content

Commit 28a40bb

Browse files
committed
feat/improved panel positioning
1 parent a3cd15b commit 28a40bb

7 files changed

Lines changed: 172 additions & 27 deletions

File tree

e2e/filtering.spec.ts

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,70 @@ test.describe('filtering', () => {
131131
};
132132
});
133133

134-
expect(panelLeft).toBeCloseTo(buttonLeft, 0);
135-
expect(panelTop).toBeCloseTo(buttonBottom, 0);
134+
expect(Math.abs(panelLeft - buttonLeft)).toBeLessThanOrEqual(2);
135+
expect(Math.abs(panelTop - buttonBottom)).toBeLessThanOrEqual(2);
136136
expect(panelBottom).toBeGreaterThan(wrapperBottom);
137137
});
138138

139+
test('flips filter dialog above the button near the viewport bottom', async ({ page }) => {
140+
await page.setViewportSize({ width: 900, height: 260 });
141+
142+
const source: SampleRow[] = [
143+
{ id: 501, name: 'Alice', role: 'Admin', city: 'Lisbon' },
144+
{ id: 502, name: 'Ben', role: 'Engineer', city: 'Porto' },
145+
];
146+
147+
const columns = buildColumns([
148+
{ prop: 'id', name: 'ID' },
149+
{ prop: 'name', name: 'Name' },
150+
{ prop: 'role', name: 'Role', filter: true, ...withHeaderTestId('bottom-filter-role') },
151+
{ prop: 'city', name: 'City' },
152+
]);
153+
154+
await mountGrid(page, { columns, source, filter: true, height: 160 });
155+
await page.locator('revo-grid').evaluate(grid => {
156+
const wrapper = grid.parentElement;
157+
if (!wrapper) {
158+
throw new Error('Grid wrapper was not found');
159+
}
160+
wrapper.style.marginTop = '160px';
161+
});
162+
163+
await page
164+
.getByTestId('bottom-filter-role')
165+
.locator(SELECTORS.filterButton)
166+
.click();
167+
168+
const filterPanel = page.locator(SELECTORS.filterPanel);
169+
await expect(filterPanel).toBeVisible();
170+
171+
const { actionBottom, buttonTop, panelBottom, panelTop, viewportBottom } = await page.evaluate(() => {
172+
const dialog = document.querySelector('revogr-filter-panel dialog');
173+
const button = document
174+
.querySelector('[data-testid="bottom-filter-role"]')
175+
?.querySelector('.rv-filter');
176+
const actions = dialog?.querySelector('.filter-actions');
177+
if (!button || !dialog || !actions) {
178+
throw new Error('Filter button, dialog, or filter actions were not found');
179+
}
180+
const buttonRect = button.getBoundingClientRect();
181+
const panelRect = dialog.getBoundingClientRect();
182+
const actionRect = actions.getBoundingClientRect();
183+
return {
184+
actionBottom: actionRect.bottom,
185+
buttonTop: buttonRect.top,
186+
panelBottom: panelRect.bottom,
187+
panelTop: panelRect.top,
188+
viewportBottom: window.innerHeight,
189+
};
190+
});
191+
192+
expect(panelTop).toBeGreaterThanOrEqual(8);
193+
expect(panelBottom).toBeLessThanOrEqual(buttonTop + 2);
194+
expect(panelBottom).toBeLessThanOrEqual(viewportBottom - 8);
195+
expect(Math.abs(actionBottom - panelBottom)).toBeLessThanOrEqual(2);
196+
});
197+
139198
test('renders filter condition actions outside select input on the same row', async ({ page }) => {
140199
const source: SampleRow[] = [
141200
{ id: 501, name: 'Alice', role: 'Admin', city: 'Lisbon' },
@@ -223,8 +282,8 @@ test.describe('filtering', () => {
223282
await filterPanel.evaluate((panel) => {
224283
const rows = Array.from(panel.querySelectorAll('.multi-filter-list-row'));
225284
const sourceHandle = rows[1].querySelector('.reorder-button');
226-
const targetHandle = rows[0].querySelector('.reorder-button');
227-
if (!sourceHandle || !targetHandle) {
285+
const targetRow = rows[0];
286+
if (!sourceHandle || !targetRow) {
228287
throw new Error('Filter reorder controls were not found');
229288
}
230289
const dataTransfer = new DataTransfer();
@@ -233,12 +292,25 @@ test.describe('filtering', () => {
233292
cancelable: true,
234293
dataTransfer,
235294
}));
236-
targetHandle.dispatchEvent(new DragEvent('dragover', {
295+
targetRow.dispatchEvent(new DragEvent('dragover', {
237296
bubbles: true,
238297
cancelable: true,
239298
dataTransfer,
240299
}));
241-
targetHandle.dispatchEvent(new DragEvent('drop', {
300+
});
301+
302+
await expect(filterPanel.locator('.multi-filter-list-row').nth(0)).toHaveClass(/filter-row-drag-over/);
303+
304+
await filterPanel.evaluate((panel) => {
305+
const rows = Array.from(panel.querySelectorAll('.multi-filter-list-row'));
306+
const sourceHandle = rows[1].querySelector('.reorder-button');
307+
const targetRow = rows[0];
308+
if (!sourceHandle || !targetRow) {
309+
throw new Error('Filter reorder controls were not found');
310+
}
311+
const dataTransfer = new DataTransfer();
312+
dataTransfer.setData('text/plain', '2');
313+
targetRow.dispatchEvent(new DragEvent('drop', {
242314
bubbles: true,
243315
cancelable: true,
244316
dataTransfer,
@@ -250,6 +322,7 @@ test.describe('filtering', () => {
250322
}));
251323
});
252324

325+
await expect(filterPanel.locator('.filter-row-drag-over')).toHaveCount(0);
253326
await expect(filterPanel.locator('.select-filter').nth(0)).toHaveValue('eq');
254327
await expect(filterPanel.locator('.select-filter').nth(1)).toHaveValue('contains');
255328
await expect(filterInputs.nth(0)).toHaveValue('Engineer');

playwright.local-3334.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expect } from '@playwright/test';
2+
import { matchers, createConfig } from '@stencil/playwright';
3+
4+
expect.extend(matchers);
5+
6+
export default createConfig({
7+
testDir: 'e2e',
8+
testMatch: ['**/*.e2e.ts', '**/*.spec.ts'],
9+
use: {
10+
baseURL: 'http://localhost:3334',
11+
},
12+
reporter: [['list']],
13+
});

src/plugins/filter/filter.panel.tsx

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const defaultType: FilterType = 'none';
3636
const FILTER_LIST_CLASS = 'multi-filter-list';
3737
const FILTER_LIST_CLASS_ACTION = 'multi-filter-list-action';
3838
const FILTER_ID = 'add-filter';
39+
const VIEWPORT_PADDING = 8;
3940

4041
/**
4142
* Filter panel for editing filters
@@ -170,19 +171,27 @@ export class FilterPanel {
170171
}
171172

172173
const extra = this.renderExtra(prop, index);
174+
const isDragging = this.draggedFilterId === filter.id;
175+
const isDragOver = this.dragOverFilterId === filter.id && !isDragging;
173176

174177
return (
175178
<div key={filter.id} class={FILTER_LIST_CLASS}>
176-
<div ref={el => el?.classList.add('multi-filter-list-row')}>
179+
<div
180+
class={{
181+
'multi-filter-list-row': true,
182+
'filter-row-dragging': isDragging,
183+
'filter-row-drag-over': isDragOver,
184+
}}
185+
onDragOver={e => this.onFilterDragOver(e, filter.id)}
186+
onDragLeave={() => this.onFilterDragLeave(filter.id)}
187+
onDrop={e => this.onFilterDrop(e, prop, filter.id)}
188+
>
177189
{visibleFilterCount > 1 ? (
178190
<ReorderButton
179-
dragging={this.draggedFilterId === filter.id}
180-
dragOver={this.dragOverFilterId === filter.id && this.draggedFilterId !== filter.id}
191+
dragging={isDragging}
192+
dragOver={isDragOver}
181193
onDragStart={e => this.onFilterDragStart(e, filter.id)}
182194
onDragEnd={() => this.onFilterDragEnd()}
183-
onDragOver={e => this.onFilterDragOver(e, filter.id)}
184-
onDragLeave={() => this.onFilterDragLeave(filter.id)}
185-
onDrop={e => this.onFilterDrop(e, prop, filter.id)}
186195
/>
187196
) : ''}
188197
<div class={{ 'select-input': true }}>
@@ -234,26 +243,49 @@ export class FilterPanel {
234243
}
235244

236245
if (this.changes.autoCorrect !== false) {
246+
this.autoCorrect(this.dialog);
237247
requestAnimationFrame(() => this.autoCorrect(this.dialog));
238248
}
239249
}
240250

241251
private autoCorrect(el?: HTMLElement | null) {
242-
if (!el) {
252+
if (!el || !this.changes) {
243253
return;
244254
}
245255

256+
el.style.maxHeight = '';
257+
el.style.left = `${this.changes.x}px`;
258+
el.style.top = `${this.changes.y}px`;
259+
246260
const pos = el.getBoundingClientRect();
247-
const maxLeft = Math.max(0, window.innerWidth - pos.width);
248-
const maxTop = Math.max(0, window.innerHeight - pos.height);
261+
const anchorTop = this.changes.anchorY ?? this.changes.y;
262+
const anchorBottom = this.changes.y;
263+
const spaceAbove = Math.max(0, anchorTop - VIEWPORT_PADDING);
264+
const spaceBelow = Math.max(0, window.innerHeight - anchorBottom - VIEWPORT_PADDING);
265+
const openAbove = pos.height > spaceBelow && spaceAbove > spaceBelow;
266+
const availableHeight = Math.max(
267+
VIEWPORT_PADDING,
268+
openAbove ? spaceAbove : spaceBelow,
269+
);
249270

250-
if (pos.left > maxLeft) {
251-
el.style.left = `${maxLeft}px`;
252-
}
271+
el.style.maxHeight = `${availableHeight}px`;
253272

254-
if (pos.top > maxTop) {
255-
el.style.top = `${maxTop}px`;
256-
}
273+
const adjustedPos = el.getBoundingClientRect();
274+
const maxLeft = Math.max(
275+
VIEWPORT_PADDING,
276+
window.innerWidth - adjustedPos.width - VIEWPORT_PADDING,
277+
);
278+
const maxTop = Math.max(
279+
VIEWPORT_PADDING,
280+
window.innerHeight - adjustedPos.height - VIEWPORT_PADDING,
281+
);
282+
const left = Math.min(Math.max(VIEWPORT_PADDING, this.changes.x), maxLeft);
283+
const top = openAbove
284+
? Math.min(Math.max(VIEWPORT_PADDING, anchorTop - adjustedPos.height), maxTop)
285+
: Math.min(Math.max(VIEWPORT_PADDING, anchorBottom), maxTop);
286+
287+
el.style.left = `${left}px`;
288+
el.style.top = `${top}px`;
257289
}
258290

259291
private onDialogMouseDown(e: MouseEvent) {

src/plugins/filter/filter.plugin.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ export class FilterPlugin extends BasePlugin {
270270
...this.filterCollection[prop],
271271
x: buttonPos.x,
272272
y: buttonPos.y + buttonPos.height,
273+
anchorY: buttonPos.y,
273274
autoCorrect: true,
274275
filterTypes: this.getColumnFilter(e.detail.filter),
275276
filterItems: this.multiFilterItems,

src/plugins/filter/filter.style.scss

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ revogr-filter-panel .filter-panel-dialog {
1818
border: 1px solid var(--revo-grid-filter-panel-border, #cecece);
1919
transform-origin: 62px 0px;
2020
box-shadow: 0 5px 18px -2px var(--revo-grid-filter-panel-shadow, rgba(0, 0, 0, 0.15));
21+
box-sizing: border-box;
2122
padding: 10px;
2223
border-radius: 8px;
2324
margin: 0;
@@ -56,8 +57,16 @@ revogr-filter-panel .filter-panel-dialog {
5657
}
5758

5859
.filter-actions {
60+
position: sticky;
61+
right: 0;
62+
bottom: -10px;
63+
left: 0;
64+
z-index: 1;
5965
text-align: right;
60-
margin-right: -5px;
66+
margin: 10px -10px -10px;
67+
padding: 0 5px 10px 10px;
68+
background: var(--revo-grid-filter-panel-bg, #fff);
69+
border-top: 1px solid var(--revo-grid-filter-panel-divider, #d9d9d9);
6170

6271
button {
6372
margin-top: 10px;
@@ -192,6 +201,23 @@ revogr-filter-panel .filter-panel-dialog {
192201
display: flex;
193202
align-items: center;
194203
gap: 6px;
204+
position: relative;
205+
206+
&.filter-row-dragging {
207+
opacity: 0.65;
208+
}
209+
210+
&.filter-row-drag-over::before {
211+
content: '';
212+
position: absolute;
213+
top: -4px;
214+
right: 0;
215+
left: 0;
216+
height: 2px;
217+
background: var(--revo-grid-filter-panel-reorder-accent, #007cb2);
218+
border-radius: 999px;
219+
box-shadow: 0 0 0 2px var(--revo-grid-filter-panel-bg, #fff);
220+
}
195221
}
196222

197223
.multi-filter-list-action {
@@ -234,10 +260,6 @@ revogr-filter-panel .filter-panel-dialog {
234260
transform: scaleX(0.8);
235261
width: 16px;
236262

237-
&.filter-row-dragging {
238-
opacity: 0.55;
239-
}
240-
241263
&.filter-row-drag-over {
242264
color: var(--revo-grid-filter-panel-reorder-accent, #007cb2);
243265
}

src/plugins/filter/filter.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ export interface MultiFilterItem {
160160
export interface ShowData extends FilterItem, Omit<ColumnRegular, 'filter'> {
161161
x: number;
162162
y: number;
163+
/**
164+
* Top viewport coordinate of the element the filter panel is anchored to.
165+
*/
166+
anchorY?: number;
163167
/**
164168
* Auto correct position if it is out of document bounds
165169
*/

0 commit comments

Comments
 (0)