Skip to content

Commit 7433946

Browse files
authored
feat(FilterBar): introduce reorder filters feature (#5569)
Closes #5243
1 parent 51e9858 commit 7433946

File tree

11 files changed

+1081
-397
lines changed

11 files changed

+1081
-397
lines changed

packages/main/src/components/FilterBar/FilterBar.cy.tsx

+203-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import React from 'react';
2-
import { Input, MultiComboBox, Option, Select, Switch } from '../../webComponents/index.js';
1+
import { cypressPassThroughTestsFactory, mountWithCustomTagName } from '@/cypress/support/utils';
2+
import React, { useId, useState } from 'react';
3+
import {
4+
Input,
5+
MultiComboBox,
6+
MultiInput,
7+
Option,
8+
RatingIndicator,
9+
Select,
10+
StepInput,
11+
Switch,
12+
Token
13+
} from '../../webComponents/index.js';
314
import { FilterGroupItem } from '../FilterGroupItem';
415
import { VariantManagement } from '../VariantManagement';
516
import { VariantItem } from '../VariantManagement/VariantItem';
17+
import type { FilterBarPropTypes } from './index.js';
618
import { FilterBar } from './index.js';
7-
import { cypressPassThroughTestsFactory, mountWithCustomTagName } from '@/cypress/support/utils';
819

920
const variants = (
1021
<VariantManagement data-testid="variantManagement">
@@ -181,9 +192,11 @@ describe('FilterBar.cy.tsx', () => {
181192
cy.get('@restoreSpy').should('have.callCount', 0);
182193
cy.findByText(action).click();
183194
cy.closeUi5PopupWithEsc();
195+
cy.focused().should('have.attr', 'data-component-name', 'FilterBarDialogSaveBtn');
184196
cy.get('@restoreSpy').should('have.callCount', 0);
185197
cy.findByText(action).click();
186198
cy.get('[data-component-name="FilterBarDialogResetMessageBox"]').contains('OK').click();
199+
cy.focused().should('have.attr', 'data-component-name', 'FilterBarDialogSaveBtn');
187200
cy.findByText('OK').click();
188201
cy.get('@saveSpy').should('have.callCount', saveCallCount);
189202
saveCallCount++;
@@ -231,15 +244,13 @@ describe('FilterBar.cy.tsx', () => {
231244
cy.findAllByText('SWITCH').should('have.length', 2);
232245
cy.findAllByText('SELECT').should('have.length', 2);
233246

234-
cy.findByPlaceholderText('Search for filters').typeIntoUi5Input('S');
235-
cy.findByPlaceholderText('Search for filters').trigger('input');
247+
cy.findByPlaceholderText('Search for filters').typeIntoUi5Input('S{enter}');
236248

237249
cy.findAllByText('INPUT').should('have.length', 1);
238250
cy.findAllByText('SWITCH').should('have.length', 2);
239251
cy.findAllByText('SELECT').should('have.length', 2);
240252

241-
cy.findByPlaceholderText('Search for filters').typeIntoUi5Input('W', { force: true });
242-
cy.findByPlaceholderText('Search for filters').trigger('input');
253+
cy.findByPlaceholderText('Search for filters').typeIntoUi5Input('W{enter}', { force: true });
243254

244255
cy.findAllByText('INPUT').should('have.length', 1);
245256
cy.findAllByText('SWITCH').should('have.length', 2);
@@ -493,7 +504,192 @@ describe('FilterBar.cy.tsx', () => {
493504
cy.get('[ui5-multi-combobox]').should('have.length', 2);
494505
});
495506

507+
it('reorder', () => {
508+
const save = cy.spy().as('saveSpy');
509+
cy.mount(<FilterBarWithReordering onFiltersDialogSave={save} />);
510+
511+
cy.get('div[data-order-id]').eq(0).find('[ui5-label]').should('have.text', 'StepInput');
512+
cy.get('div[data-order-id]').eq(1).find('[ui5-label]').should('have.text', 'RatingIndicator');
513+
cy.get('div[data-order-id]').eq(2).find('[ui5-label]').should('have.text', 'MultiInput');
514+
cy.get('div[data-order-id]').eq(4).find('[ui5-label]').should('have.text', 'Switch');
515+
516+
cy.findByText('Filters').realClick();
517+
cy.get('[ui5-dialog]').should('have.attr', 'open');
518+
cy.wait(200);
519+
cy.get('[data-text="SELECT w/ initial selected"]').as('notSelected');
520+
cy.get('[data-text="MultiInput"]').as('multiInputRow');
521+
cy.get('[data-text="StepInput"]').as('stepInputRow');
522+
// active icon should be displayed if not hovered or focused
523+
cy.get('@multiInputRow').find('[name="circle-task-2"]').should('be.visible');
524+
// if no row was focused before, show reorder buttons on hover, but only for visible filters (selected rows)
525+
cy.get('@multiInputRow').shadow().find('tr').realHover();
526+
cy.get('@multiInputRow').find('[data-component-name="FilterBarDialogTableCellReorderBtns"]').should('be.visible');
527+
cy.get('@multiInputRow').find('[name="circle-task-2"]').should('not.be.visible');
528+
cy.get('@notSelected').shadow().find('tr').realHover();
529+
cy.get('@notSelected').find('[data-component-name="FilterBarDialogTableCellReorderBtns"]').should('not.be.visible');
530+
cy.get('@notSelected').find('[name="circle-task-2"]').should('not.exist');
531+
cy.realPress('Tab');
532+
cy.get('@multiInputRow').shadow().find('tr').realHover();
533+
// don't show reorder buttons if a row was focused before
534+
cy.get('@multiInputRow').find('[name="circle-task-2"]').should('be.visible');
535+
cy.get('@multiInputRow')
536+
.find('[data-component-name="FilterBarDialogTableCellReorderBtns"]')
537+
.should('not.be.visible');
538+
cy.focused().should('have.attr', 'data-text', 'StepInput');
539+
cy.focused().find('[data-component-name="FilterBarDialogTableCellReorderBtns"]').should('be.visible');
540+
541+
// reorder via keyboard
542+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'StepInput');
543+
cy.realPress(['Meta', 'ArrowDown']);
544+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'RatingIndicator');
545+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'StepInput');
546+
// for some reason, the focus is not set after moving a row in cypress
547+
cy.get('@stepInputRow').invoke('focus');
548+
cy.realPress(['Meta', 'End']);
549+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'RatingIndicator');
550+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'MultiInput');
551+
cy.get('[ui5-table-row]').eq(5).should('have.attr', 'data-text', 'StepInput');
552+
cy.realPress(['Meta', 'ArrowUp']);
553+
cy.get('[ui5-table-row]').eq(5).should('have.attr', 'data-text', 'SELECT w/ initial selected');
554+
cy.get('[ui5-table-row]').eq(4).should('have.attr', 'data-text', 'StepInput');
555+
cy.get('@stepInputRow').invoke('focus');
556+
cy.realPress(['Meta', 'Home']);
557+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'StepInput');
558+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'RatingIndicator');
559+
560+
// reorder via button click
561+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'StepInput');
562+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnTop"]').as('topBtn');
563+
cy.get('@topBtn').should('have.attr', 'disabled', 'disabled');
564+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnUp"]').as('upBtn');
565+
cy.get('@upBtn').should('have.attr', 'disabled', 'disabled');
566+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnDown"]').as('downBtn');
567+
cy.get('@downBtn').should('not.have.attr', 'disabled');
568+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnBottom"]').as('bottomBtn');
569+
cy.get('@bottomBtn').should('not.have.attr', 'disabled');
570+
571+
cy.get('@downBtn').realClick();
572+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'RatingIndicator');
573+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'StepInput');
574+
cy.get('@stepInputRow').realClick();
575+
cy.get('@bottomBtn').realClick();
576+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'RatingIndicator');
577+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'MultiInput');
578+
cy.get('[ui5-table-row]').eq(5).should('have.attr', 'data-text', 'StepInput');
579+
580+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnTop"]').as('topBtn');
581+
cy.get('@topBtn').should('not.have.attr', 'disabled');
582+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnUp"]').as('upBtn');
583+
cy.get('@upBtn').should('not.have.attr', 'disabled');
584+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnDown"]').as('downBtn');
585+
cy.get('@downBtn').should('have.attr', 'disabled', 'disabled');
586+
cy.get('@stepInputRow').find('[data-component-name="FilterBarDialogReorderBtnBottom"]').as('bottomBtn');
587+
cy.get('@bottomBtn').should('have.attr', 'disabled', 'disabled');
588+
589+
cy.get('@stepInputRow').realClick();
590+
cy.get('@upBtn').realClick();
591+
cy.get('[ui5-table-row]').eq(5).should('have.attr', 'data-text', 'SELECT w/ initial selected');
592+
cy.get('[ui5-table-row]').eq(4).should('have.attr', 'data-text', 'StepInput');
593+
cy.get('@stepInputRow').realClick();
594+
cy.get('@topBtn').realClick();
595+
cy.get('[ui5-table-row]').eq(0).should('have.attr', 'data-text', 'StepInput');
596+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'RatingIndicator');
597+
598+
// check if keyboard nav still works
599+
cy.wait(100);
600+
cy.realPress('End');
601+
cy.focused().should('have.attr', 'data-text', 'SELECT w/ initial selected');
602+
cy.realPress('ArrowUp');
603+
cy.focused().should('have.attr', 'data-text', 'Switch');
604+
cy.realPress('Home');
605+
cy.focused().should('have.attr', 'data-text', 'StepInput');
606+
cy.realPress('ArrowDown');
607+
cy.focused().should('have.attr', 'data-text', 'RatingIndicator');
608+
609+
// reset behavior
610+
cy.realPress(['Meta', 'End']);
611+
cy.get('[ui5-table-row]').eq(5).should('have.attr', 'data-text', 'RatingIndicator');
612+
cy.findByText('Reset').realClick();
613+
cy.realPress('Enter');
614+
cy.wait(100);
615+
cy.get('[ui5-table-row]').eq(1).should('have.attr', 'data-text', 'RatingIndicator');
616+
617+
// event
618+
cy.focused().should('have.attr', 'data-component-name', 'FilterBarDialogSaveBtn');
619+
cy.realPress(['Shift', 'Tab']);
620+
cy.focused().should('have.attr', 'data-text', 'RatingIndicator');
621+
cy.realPress(['Meta', 'End']);
622+
cy.get('[ui5-table-row]').eq(5).should('have.attr', 'data-text', 'RatingIndicator');
623+
cy.findByText('OK').realClick();
624+
cy.get('@saveSpy').should('have.been.calledOnce');
625+
626+
cy.get('div[data-order-id]').eq(0).find('[ui5-label]').should('have.text', 'StepInput');
627+
cy.get('div[data-order-id]').eq(1).find('[ui5-label]').should('have.text', 'MultiInput');
628+
cy.get('div[data-order-id]').eq(2).find('[ui5-label]').should('have.text', 'Input');
629+
cy.get('div[data-order-id]').eq(4).find('[ui5-label]').should('have.text', 'RatingIndicator');
630+
});
631+
496632
mountWithCustomTagName(FilterBar);
497633

498634
cypressPassThroughTestsFactory(FilterBar);
499635
});
636+
637+
function FilterBarWithReordering(props: Partial<FilterBarPropTypes>) {
638+
{
639+
const uniqueId = useId();
640+
const [orderedChildren, setOrderedChildren] = useState([
641+
<FilterGroupItem key={`${uniqueId}-0`} label="StepInput" required orderId={`${uniqueId}-0`}>
642+
<StepInput required />
643+
</FilterGroupItem>,
644+
<FilterGroupItem key={`${uniqueId}-1`} label="RatingIndicator" orderId={`${uniqueId}-1`}>
645+
<RatingIndicator />
646+
</FilterGroupItem>,
647+
<FilterGroupItem key={`${uniqueId}-2`} label="MultiInput" active orderId={`${uniqueId}-2`}>
648+
<MultiInput
649+
tokens={
650+
<>
651+
<Token text="Argentina" selected />
652+
<Token text="Bulgaria" />
653+
<Token text="England" />
654+
<Token text="Finland" />
655+
</>
656+
}
657+
/>
658+
</FilterGroupItem>,
659+
<FilterGroupItem key={`${uniqueId}-3`} label="Input" orderId={`${uniqueId}-3`}>
660+
<Input placeholder="Placeholder" />
661+
</FilterGroupItem>,
662+
<FilterGroupItem key={`${uniqueId}-4`} label="Switch" orderId={`${uniqueId}-4`}>
663+
<Switch />
664+
</FilterGroupItem>,
665+
<FilterGroupItem
666+
key={`${uniqueId}-5`}
667+
label="SELECT w/ initial selected"
668+
visibleInFilterBar={false}
669+
orderId={`${uniqueId}-5`}
670+
>
671+
<Select>
672+
<Option>Option 1</Option>
673+
<Option selected>Option 2</Option>
674+
<Option>Option 3</Option>
675+
<Option>Option 4</Option>
676+
</Select>
677+
</FilterGroupItem>
678+
]);
679+
680+
const handleFiltersDialogSave = (e) => {
681+
props.onFiltersDialogSave?.(e);
682+
setOrderedChildren((prev) => {
683+
return e.detail.orderIds.map((orderId) => {
684+
const obj = prev.find((item) => item.props.orderId === orderId);
685+
return { ...obj };
686+
});
687+
});
688+
};
689+
return (
690+
<FilterBar {...props} onFiltersDialogSave={handleFiltersDialogSave} enableReordering showResetButton>
691+
{orderedChildren}
692+
</FilterBar>
693+
);
694+
}
695+
}

packages/main/src/components/FilterBar/FilterBar.mdx

+73
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,79 @@ const FilterBarComponent = (args) => {
190190

191191
</details>
192192

193+
## FilterBar with Reordering
194+
195+
To enable the reordering feature set `enableReordering` to `true` and add a unique `orderId` prop to each `FilterGroupItem`.
196+
197+
<Canvas of={ComponentStories.WithReordering} />
198+
199+
### Code
200+
201+
<details>
202+
203+
<summary>Show Code</summary>
204+
205+
```jsx
206+
function FilterBarWithReordering(props) {
207+
const uniqueId = useId();
208+
const [orderedChildren, setOrderedChildren] = useState([
209+
<FilterGroupItem key={`${uniqueId}-0`} label="StepInput" required orderId={`${uniqueId}-0`}>
210+
<StepInput required />
211+
</FilterGroupItem>,
212+
<FilterGroupItem key={`${uniqueId}-1`} label="RatingIndicator" orderId={`${uniqueId}-1`}>
213+
<RatingIndicator />
214+
</FilterGroupItem>,
215+
<FilterGroupItem key={`${uniqueId}-2`} label="MultiInput" active orderId={`${uniqueId}-2`}>
216+
<MultiInput
217+
tokens={
218+
<>
219+
<Token text="Argentina" selected />
220+
<Token text="Bulgaria" />
221+
<Token text="England" />
222+
<Token text="Finland" />
223+
</>
224+
}
225+
/>
226+
</FilterGroupItem>,
227+
<FilterGroupItem key={`${uniqueId}-3`} label="Input" orderId={`${uniqueId}-3`}>
228+
<Input placeholder="Placeholder" />
229+
</FilterGroupItem>,
230+
<FilterGroupItem key={`${uniqueId}-4`} label="Switch" orderId={`${uniqueId}-4`}>
231+
<Switch />
232+
</FilterGroupItem>,
233+
<FilterGroupItem
234+
key={`${uniqueId}-5`}
235+
label="SELECT w/ initial selected"
236+
visibleInFilterBar={false}
237+
orderId={`${uniqueId}-5`}
238+
>
239+
<Select>
240+
<Option>Option 1</Option>
241+
<Option selected>Option 2</Option>
242+
<Option>Option 3</Option>
243+
<Option>Option 4</Option>
244+
</Select>
245+
</FilterGroupItem>
246+
]);
247+
248+
const handleFiltersDialogSave = (e) => {
249+
setOrderedChildren((prev) => {
250+
return e.detail.reorderedItems.orderedIds.map((orderId) => {
251+
const obj = prev.find((item) => item.props.orderId === orderId);
252+
return { ...obj };
253+
});
254+
});
255+
};
256+
return (
257+
<FilterBar {...props} onFiltersDialogSave={handleFiltersDialogSave} enableReordering showResetButton>
258+
{orderedChildren}
259+
</FilterBar>
260+
);
261+
}
262+
```
263+
264+
</details>
265+
193266
## FilterBar in a DynamicPage
194267

195268
When a FilterBar is used inside e.g. a DynamicPage, `hideToolbar` should be set to `true`, since the `VariantManagement` of

packages/main/src/components/FilterBar/FilterBar.stories.tsx

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2-
import React, { useReducer, useRef } from 'react';
2+
import React, { useId, useReducer, useRef, useState } from 'react';
33
import { FlexBoxDirection } from '../../enums/index.js';
44
import {
55
ComboBox,
@@ -394,3 +394,62 @@ export const InDynamicPage: Story = {
394394
);
395395
}
396396
};
397+
398+
export const WithReordering: Story = {
399+
render(args) {
400+
const uniqueId = useId();
401+
const [orderedChildren, setOrderedChildren] = useState([
402+
<FilterGroupItem key={`${uniqueId}-0`} label="StepInput" required orderId={`${uniqueId}-0`}>
403+
<StepInput required />
404+
</FilterGroupItem>,
405+
<FilterGroupItem key={`${uniqueId}-1`} label="RatingIndicator" orderId={`${uniqueId}-1`}>
406+
<RatingIndicator />
407+
</FilterGroupItem>,
408+
<FilterGroupItem key={`${uniqueId}-2`} label="MultiInput" active orderId={`${uniqueId}-2`}>
409+
<MultiInput
410+
tokens={
411+
<>
412+
<Token text="Argentina" selected />
413+
<Token text="Bulgaria" />
414+
<Token text="England" />
415+
<Token text="Finland" />
416+
</>
417+
}
418+
/>
419+
</FilterGroupItem>,
420+
<FilterGroupItem key={`${uniqueId}-3`} label="Input" orderId={`${uniqueId}-3`}>
421+
<Input placeholder="Placeholder" />
422+
</FilterGroupItem>,
423+
<FilterGroupItem key={`${uniqueId}-4`} label="Switch" orderId={`${uniqueId}-4`}>
424+
<Switch />
425+
</FilterGroupItem>,
426+
<FilterGroupItem
427+
key={`${uniqueId}-5`}
428+
label="SELECT w/ initial selected"
429+
visibleInFilterBar={false}
430+
orderId={`${uniqueId}-5`}
431+
>
432+
<Select>
433+
<Option>Option 1</Option>
434+
<Option selected>Option 2</Option>
435+
<Option>Option 3</Option>
436+
<Option>Option 4</Option>
437+
</Select>
438+
</FilterGroupItem>
439+
]);
440+
441+
const handleFiltersDialogSave = (e) => {
442+
setOrderedChildren((prev) => {
443+
return e.detail.orderIds.map((orderId) => {
444+
const obj = prev.find((item) => item.props.orderId === orderId);
445+
return { ...obj };
446+
});
447+
});
448+
};
449+
return (
450+
<FilterBar {...args} onFiltersDialogSave={handleFiltersDialogSave} enableReordering showResetButton>
451+
{orderedChildren}
452+
</FilterBar>
453+
);
454+
}
455+
};

0 commit comments

Comments
 (0)