Skip to content

Commit 1b5f20e

Browse files
authored
Merge pull request #1884 from wilecoyotegenius/filter-bar
Filter Bar
2 parents 91a4b7b + 73bc2df commit 1b5f20e

19 files changed

Lines changed: 531 additions & 2 deletions

File tree

15.7 KB
Loading
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# FilterBar
2+
3+
This control allows you to render a bar of filters that looks exactly the same as in modern lists.
4+
5+
Here is an example of the control in action:
6+
7+
![FilterBar control](../assets/FilterBar.png)
8+
9+
## How to use this control in your solutions
10+
11+
- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency.
12+
- In your component file, import the `FilterBar` control as follows:
13+
14+
```TypeScript
15+
import { FilterBar } from "@pnp/spfx-controls-react/lib/FilterBar";
16+
```
17+
18+
- Use the `FilterBar` control in your code as follows:
19+
20+
```TypeScript
21+
// Initial state
22+
this.state = {
23+
filters: [{
24+
label: "Title",
25+
value: "title 1"
26+
},
27+
{
28+
label: "Field1",
29+
value: "value 1"
30+
},
31+
{
32+
label: "Title",
33+
value: "title 2"
34+
}
35+
]
36+
}
37+
...
38+
...
39+
// Events
40+
private onClearFilters = () => {
41+
console.log("Cleared all filters");
42+
this.setState({ filters: []});
43+
}
44+
45+
private onRemoveFilter = (label: string, value: string) => {
46+
console.log(`Cleared ${label} ${value}`);
47+
const itm = this.state.filters.find(i => i.label === label && i.value === value);
48+
if (itm) {
49+
const index = this.state.filters.indexOf(itm);
50+
this.state.filters.splice(index, 1)
51+
52+
this.setState({
53+
filters: [...this.state.filters]
54+
});
55+
}
56+
}
57+
58+
...
59+
...
60+
61+
//Render the filter bar
62+
<FilterBar
63+
items={this.state.items}
64+
inlineItemCount={3}
65+
onClearFilters={this.onClearFilters}
66+
onRemoveFilter={this.onRemoveFilter}>
67+
</FilterBar>
68+
```
69+
70+
## Implementation
71+
72+
The `FilterBar` control can be configured with the following properties:
73+
74+
| Property | Type | Required | Description | Default |
75+
| ---- | ---- | ---- | ---- | ---- |
76+
| items | [IFilterBarItem[]](#ifilterbaritem) | yes | Filters to be displayed. Multiple filters with the same label are grouped together | |
77+
| inlineItemCount | number | no | Number of filters, after which filters start showing as overflow | 5 |
78+
| onClearFilters | () => void | no | Callback function called after the next item button is clicked. Not used when triggerPageEvent is specified. | |
79+
| onRemoveFilter | (label: string, value: string) => void | no | Callback function called after clicking a singular filter pill | |
80+
81+
## IFilterBarItem
82+
| Property | Type | Required | Description | Default |
83+
| ---- | ---- | ---- | ---- | ---- |
84+
| label | string | yes | Filter label | |
85+
| value | string | yes | Filter value | |
86+
87+
88+
![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/FilterBar)

docs/documentation/docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ The following controls are currently available:
8282
- [FieldPicker](./controls/FieldPicker) (control to pick one or multiple fields from a list or a site)
8383
- [FilePicker](./controls/FilePicker) (control that allows to browse and select a file from various places)
8484
- [FileTypeIcon](./controls/FileTypeIcon) (shows the icon of a specified file path or application)
85+
- [FilterBar](./controls/FilterBar) (control that renders filters in a similar way to modern lists)
8586
- [FolderExplorer](./controls/FolderExplorer) (control that allows to browse the folders and sub-folders from a root folder)
8687
- [FolderPicker](./controls/FolderPicker) (control that allows to browse and select a folder)
8788
- [GridLayout](./controls/GridLayout) (control that renders a responsive grid layout for your web parts)

docs/documentation/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ nav:
3636
- FieldPicker: 'controls/FieldPicker.md'
3737
- FilePicker: 'controls/FilePicker.md'
3838
- FileTypeIcon: 'controls/FileTypeIcon.md'
39+
- FilterBar: 'controls/FilterBar.md'
3940
- FolderExplorer: 'controls/FolderExplorer.md'
4041
- FolderPicker: 'controls/FolderPicker.md'
4142
- GridLayout: 'controls/GridLayout.md'

src/FilterBar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './controls/filterBar';
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
@import '~@fluentui/react/dist/sass/References.scss';
2+
3+
$--ms-semanticColors-listBackground: #ffffff;
4+
$--ms-effects-roundedCorner6: 6px;
5+
$--ms-effects-elevation4: 0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108);
6+
7+
.container {
8+
padding: 0 16px;
9+
align-items: center;
10+
background: $--ms-semanticColors-listBackground;
11+
position: relative;
12+
height: 42px;
13+
white-space: pre;
14+
display: flex;
15+
color: "[theme:neutralSecondary, default:#{$ms-color-neutralSecondary}]";
16+
overflow: hidden;
17+
border-radius: $--ms-effects-roundedCorner6 $--ms-effects-roundedCorner6 0 0;
18+
box-shadow: $--ms-effects-elevation4;
19+
}
20+
21+
.pillGroup {
22+
display: flex;
23+
align-items: center;
24+
}
25+
26+
html[dir=ltr] {
27+
.label {
28+
margin-right: 4px;
29+
// @extend .overflow;
30+
// margin-left: 0;
31+
}
32+
.overflow {
33+
.label {
34+
margin-left: 0;
35+
}
36+
}
37+
.pill {
38+
padding-left: 8px;
39+
}
40+
41+
.icon {
42+
margin-right: 0;
43+
}
44+
.clearAll {
45+
margin-right: 0;
46+
}
47+
.clearAll {
48+
margin-left: auto;
49+
}
50+
}
51+
html[dir=ltr] {
52+
.label {
53+
margin-left: 8px;
54+
}
55+
56+
57+
.pill {
58+
padding-right: 8px;
59+
}
60+
.icon {
61+
margin-left: 10px;
62+
}
63+
}
64+
65+
.pill {
66+
67+
background: "[theme:neutralLighter, default:#{$ms-color-neutralLighter}]";
68+
border-radius: 12px;
69+
border: none;
70+
padding: 0;
71+
display: flex;
72+
align-items: center;
73+
height: 24px;
74+
margin: 0 4px;
75+
cursor: pointer;
76+
z-index: 2;
77+
color: inherit;
78+
}
79+
80+
.pillText {
81+
max-width: 150px;
82+
overflow: hidden;
83+
text-overflow: ellipsis;
84+
padding-bottom: 2px;
85+
}
86+
87+
.icon {
88+
font-size: 12px;
89+
}
90+
.overflow {
91+
padding: 2px 8px;
92+
93+
.pillGroup {
94+
flex-direction: column;
95+
align-items: flex-start;
96+
}
97+
98+
.pill {
99+
margin: 6px 0;
100+
}
101+
}
102+
103+
.clearAll {
104+
background: 0 0;
105+
border: 1px solid "[theme:neutralSecondary, default:#{$ms-color-neutralSecondary}]";
106+
height: 22px;
107+
}
108+
109+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import * as React from 'react';
2+
import styles from "./FilterBar.module.scss";
3+
import { PillGroup } from './PillGroup';
4+
import { Pill } from './Pill';
5+
import { OverflowPill } from './OverflowPill';
6+
import { IFilterBarItem } from './IFilterBarItem';
7+
import { IFilterBarItemGroup } from './IFilterBarItemGroup';
8+
import * as strings from "ControlStrings";
9+
import { findLastIndex, uniq} from "lodash";
10+
import { ThemeProvider } from '@fluentui/react/lib/Theme';
11+
import { getTheme } from '@fluentui/react/lib/Styling';
12+
13+
export interface IFilterPillBarProps {
14+
/**
15+
Filters to be displayed. Multiple filters with the same label are grouped together
16+
*/
17+
items: IFilterBarItem[];
18+
/**
19+
Number of filters, after which filters start showing as overflow
20+
*/
21+
inlineItemCount?: number;
22+
/**
23+
Callback function called after clicking 'Clear filters' pill.
24+
*/
25+
onClearFilters?: () => void;
26+
/**
27+
Callback function called after clicking a singular filter pill
28+
*/
29+
onRemoveFilter?: (label: string, value: string) => void;
30+
}
31+
32+
export const FilterBar: React.FunctionComponent<IFilterPillBarProps> = (props: IFilterPillBarProps) => {
33+
34+
const orderedArray = (arr: IFilterBarItem[]) => {
35+
const ret: IFilterBarItem[] = [];
36+
arr.map(i => {
37+
const index = findLastIndex(ret, r => r.label === i.label);
38+
if (index > -1)
39+
{
40+
ret.splice(index + 1, 0, i);
41+
}
42+
else {
43+
ret.push(i);
44+
}
45+
});
46+
return ret;
47+
}
48+
49+
const groupItems = (itms: IFilterBarItem[]): IFilterBarItemGroup[] => itms.reduce((acc: IFilterBarItemGroup[], itm: IFilterBarItem) => {
50+
const label = itm.label;
51+
let obj = acc.find(i => i.label === label);
52+
if (!obj) {
53+
obj = {
54+
label: label,
55+
values: [itm.value]
56+
};
57+
acc.push(obj);
58+
}
59+
else {
60+
if (!obj.values.find(v => v === itm.value)) {
61+
obj.values.push(itm.value);
62+
}
63+
}
64+
65+
return acc;
66+
}, []);
67+
68+
const clearAll = () => {
69+
70+
if (props.onClearFilters) {
71+
props.onClearFilters();
72+
}
73+
}
74+
75+
const pillClick = (label?: string, value?: string) => {
76+
console.log(label, value);
77+
if (props.onRemoveFilter) {
78+
props.onRemoveFilter(label as string, value as string);
79+
}
80+
81+
}
82+
//const [items, setItems] = React.useState());
83+
const defaultInlineItemCount = 5;
84+
const [inlineCount, setInlineCount] = React.useState(defaultInlineItemCount);
85+
86+
const groupedItems: IFilterBarItemGroup[] = React.useMemo(() => groupItems(orderedArray(uniq(props.items))), [props.items]);
87+
88+
React.useEffect(() => {
89+
setInlineCount(props.inlineItemCount ?? defaultInlineItemCount);
90+
}, [props.inlineItemCount])
91+
92+
return (
93+
<>
94+
<ThemeProvider theme={getTheme()}>
95+
{
96+
groupedItems && groupedItems.length > 0 && (
97+
<div className={styles.container} aria-label={strings.AppliedFiltersAriaLabel} role='region'>
98+
{
99+
groupedItems.slice(0, inlineCount).map((i, index) => <PillGroup item={i} key={index} onRemoveFilter={pillClick} />)
100+
}
101+
{
102+
groupedItems.length > inlineCount && (
103+
<OverflowPill items={groupedItems.slice(inlineCount)} onClick={pillClick} />
104+
)
105+
}
106+
<Pill clearAll={true} onClick={clearAll} />
107+
</div>
108+
)
109+
}
110+
</ThemeProvider>
111+
</>
112+
);
113+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Public properties of the FilterBarItem
3+
*
4+
*/
5+
export interface IFilterBarItem {
6+
/**
7+
Label of the filter
8+
*/
9+
label: string;
10+
/**
11+
Value of the filter
12+
*/
13+
value: string;
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface IFilterBarItemGroup {
2+
label: string;
3+
values: string[];
4+
}

0 commit comments

Comments
 (0)