Skip to content

Commit 5b3f7d0

Browse files
feat(topbar): add ability to search organizations (#294)
1 parent 34cfaac commit 5b3f7d0

File tree

4 files changed

+96
-8
lines changed

4 files changed

+96
-8
lines changed

components/topbar/src/organization-menu.tsx

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import {
2+
Button,
3+
Input,
4+
InputOnChangeData,
25
Menu,
36
MenuButton,
47
MenuDivider,
@@ -12,8 +15,10 @@ import {
1215
BuildingMultipleFilled,
1316
BuildingMultipleRegular,
1417
bundleIcon,
18+
Dismiss16Regular,
19+
Search16Regular,
1520
} from "@fluentui/react-icons";
16-
import React from "react";
21+
import React, { useCallback, useRef, useState } from "react";
1722
import { OrganizationMenuProps } from "./organization-menu.types";
1823
import { useStyles } from "./organization.styles";
1924

@@ -22,19 +27,49 @@ const OrganizationIcon = bundleIcon(
2227
BuildingMultipleRegular
2328
);
2429

25-
export const OrganizationMenu = (
26-
{ customContent, onChange, options, value }: OrganizationMenuProps
27-
) => {
30+
export const OrganizationMenu = ({
31+
customContent,
32+
onChange,
33+
options,
34+
value,
35+
filter,
36+
}: OrganizationMenuProps) => {
2837
const styles = useStyles();
2938

39+
const [filterText, setFilterText] = useState<string>("");
40+
const filterRef = useRef<HTMLInputElement>(null);
41+
3042
const currentOrganization = options?.find(({ id }) => id === value);
3143
const checkedValues = { org: [value] };
3244
const noDropDownContent = options?.length === 1 && !customContent
3345
&& currentOrganization;
3446
const onlyCustomContent = !options?.length && !!customContent;
3547

48+
const filteredOptions = options
49+
?.filter(
50+
(opt) =>
51+
filterText.length === 0
52+
|| opt.label.toLowerCase().indexOf(filterText) >= 0
53+
)
54+
.sort(
55+
(a, b) =>
56+
a.label.toLowerCase().indexOf(filterText)
57+
- b.label.toLowerCase().indexOf(filterText)
58+
);
59+
60+
const onFilterChange = useCallback(
61+
(_: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {
62+
if (data.value.length) {
63+
setFilterText(data.value.toLowerCase());
64+
} else {
65+
setFilterText("");
66+
}
67+
},
68+
[]
69+
);
70+
3671
return (
37-
<Menu checkedValues={checkedValues}>
72+
<Menu checkedValues={checkedValues} positioning={"below-end"}>
3873
<MenuTrigger>
3974
<MenuButton
4075
appearance="subtle"
@@ -45,17 +80,49 @@ export const OrganizationMenu = (
4580
data-testid="organization-menu-button"
4681
icon={<OrganizationIcon />}
4782
menuIcon={noDropDownContent ? null : undefined}
83+
onClick={() => setFilterText("")}
4884
>
4985
<span className={styles.organizationlabel}>
5086
{currentOrganization?.label ?? value}
5187
</span>
5288
</MenuButton>
5389
</MenuTrigger>
5490
<MenuPopover>
91+
{filter?.showFilter && (
92+
<>
93+
<Input
94+
ref={filterRef}
95+
contentBefore={<Search16Regular />}
96+
placeholder={filter.placeholderText}
97+
contentAfter={filterText.length
98+
? (
99+
<Button
100+
icon={<Dismiss16Regular />}
101+
appearance="transparent"
102+
onClick={() => {
103+
setFilterText("");
104+
filterRef.current?.focus();
105+
}}
106+
/>
107+
)
108+
: undefined}
109+
// To not get focus in search on open
110+
tabIndex={-1}
111+
// To keep focus on search when hovering menu items
112+
onBlur={() => filterRef.current?.focus()}
113+
className={styles.searchInput}
114+
appearance="filled-lighter"
115+
value={filterText}
116+
onChange={onFilterChange}
117+
/>
118+
<MenuDivider />
119+
</>
120+
)}
55121
<MenuList>
56122
{!onlyCustomContent && (
57123
<div className={styles.organizationSelection}>
58-
{options?.map(({ id, label }) => {
124+
{filteredOptions?.map(({ id, label }) => {
125+
const match = label.toLowerCase().indexOf(filterText);
59126
return (
60127
<MenuItemRadio
61128
data-testid={`organization-menu-item-${id}`}
@@ -65,7 +132,15 @@ export const OrganizationMenu = (
65132
onClick={() => onChange(id)}
66133
value={id}
67134
>
68-
{label}
135+
{
136+
<span>
137+
{label.substring(0, match)}
138+
<span className={styles.bold}>
139+
{label.substring(match, match + filterText.length)}
140+
</span>
141+
{label.substring(match + filterText.length)}
142+
</span>
143+
}
69144
</MenuItemRadio>
70145
);
71146
})}

components/topbar/src/organization-menu.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export type OrganizationMenuProps = PropsWithChildren<{
1010
readonly customContent?: JSX.Element;
1111
readonly onChange: (id: string) => void;
1212
readonly options?: OrganizationOption[];
13+
readonly filter?: { showFilter: boolean; placeholderText: string };
1314
}>;

components/topbar/src/organization.styles.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const useStyles = makeStyles({
77
organizationSelection: {
88
overflowY: "auto",
99
overflowX: "hidden",
10-
maxHeight: "30vh",
10+
height: "30vh",
11+
width: "290px",
1112
},
1213
singleLine: {
1314
overflowX: "hidden",
@@ -18,4 +19,10 @@ export const useStyles = makeStyles({
1819
display: "block",
1920
},
2021
},
22+
searchInput: {
23+
width: "100%",
24+
},
25+
bold: {
26+
fontWeight: "bold",
27+
},
2128
});

examples/src/components/top-bar.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export const Navbar = () => {
104104
const organizations: OrganizationOption[] = [
105105
{ id: "1", label: "organizationenn2" },
106106
{ id: "2", label: "organizationen AB" },
107+
{ id: "3", label: "A very long organization name AB" },
107108
...new Array(50).fill(0).map((_, i) => ({
108109
id: "extra" + i,
109110
label: "organization-" + i,
@@ -219,6 +220,10 @@ export const Navbar = () => {
219220
onChange: setCurrentOrganizationId,
220221
options: organizations,
221222
value: currentOrganizationId,
223+
filter: {
224+
showFilter: true,
225+
placeholderText: "Search organization",
226+
},
222227
}}
223228
profileMenu={{
224229
// showCustomContentTopDivider: false,

0 commit comments

Comments
 (0)