Skip to content

Commit

Permalink
feat(topbar): add popover to organization menu (#345)
Browse files Browse the repository at this point in the history
  • Loading branch information
thersee authored Nov 7, 2024
1 parent 038f81b commit a2fac3b
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 83 deletions.
223 changes: 141 additions & 82 deletions components/topbar/src/organization-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
Body2,
Button,
InputOnChangeData,
Menu,
MenuButton,
Expand All @@ -8,14 +10,19 @@ import {
MenuPopover,
MenuTrigger,
mergeClasses,
Popover,
PopoverSurface,
PositioningImperativeRef,
SearchBox,
SearchBoxChangeEvent,
Subtitle1,
} from "@fluentui/react-components";

import {
BuildingMultipleFilled,
BuildingMultipleRegular,
bundleIcon,
DismissRegular,
} from "@fluentui/react-icons";
import React, {
useCallback,
Expand All @@ -38,6 +45,7 @@ export const OrganizationMenu = ({
options,
value,
filter,
popoverInfo,
}: OrganizationMenuProps) => {
const styles = useStyles();

Expand Down Expand Up @@ -74,6 +82,8 @@ export const OrganizationMenu = ({
);

const menuListRef = useRef<HTMLInputElement>(null);
const orgMenuRef = React.useRef<HTMLDivElement>(null);
const popoverPositioningRef = React.useRef<PositioningImperativeRef>(null);
const [showSearch, setShowSearch] = React.useState(false);
const [menuOpen, setMenuOpen] = React.useState(false);

Expand Down Expand Up @@ -110,92 +120,141 @@ export const OrganizationMenu = ({
if (filterText.length === 0) updateShowSearchStatus();
}, [filterText]);

const [showMenuTriggerPopover, setShowMenuTriggerPopover] = useState(
!!popoverInfo
);

useEffect(() => {
setShowMenuTriggerPopover(!!popoverInfo);
}, [popoverInfo]);

useEffect(() => {
if (orgMenuRef.current) {
popoverPositioningRef.current?.setTarget(orgMenuRef.current);
}
}, [orgMenuRef, popoverPositioningRef]);

return (
<Menu
checkedValues={checkedValues}
positioning={"below-end"}
onOpenChange={(_, data) => {
setMenuOpen(data.open);
}}
>
<MenuTrigger>
<MenuButton
appearance="subtle"
className={mergeClasses(
noDropDownContent && styles.standalone,
styles.singleLine
)}
data-testid="organization-menu-button"
icon={<OrganizationIcon />}
menuIcon={noDropDownContent ? null : undefined}
onClick={() => setFilterText("")}
>
<span className={styles.organizationlabel}>
{currentOrganization?.label ?? value}
</span>
</MenuButton>
</MenuTrigger>
<MenuPopover>
{filter?.showFilter && showSearch && (
<>
<SearchBox
placeholder={filter.placeholderText}
ref={filterRef}
value={filterText}
onChange={onFilterChange}
appearance="filled-lighter"
className={styles.searchInput}
// To not get focus in search on open
tabIndex={-1}
// To keep focus on search when hovering menu items
onBlur={() => {
// Delay focus change one frame since SearchBox won't be able to react properly e.g.
// hide/show its X-button
window.requestAnimationFrame(() => {
filterRef.current?.focus();
});
}}
/>
<MenuDivider />
</>
)}
<MenuList>
{!onlyCustomContent && (
<div ref={menuListRef} className={styles.organizationSelection}>
{filteredOptions?.map(({ id, label }) => {
const match = label.toLowerCase().indexOf(filterText);
return (
<MenuItemRadio
data-testid={`organization-menu-item-${id}`}
key={id}
name="org"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onChange(id)}
value={id}
>
{
<span>
{label.substring(0, match)}
<span className={styles.bold}>
{label.substring(match, match + filterText.length)}
</span>
{label.substring(match + filterText.length)}
</span>
}
</MenuItemRadio>
);
})}
</div>
)}
{customContent !== undefined && (
<>
<Menu
checkedValues={checkedValues}
positioning={"below-end"}
onOpenChange={(_, data) => {
setMenuOpen(data.open);
}}
>
<MenuTrigger>
<MenuButton
appearance="subtle"
className={mergeClasses(
noDropDownContent && styles.standalone,
styles.singleLine
)}
data-testid="organization-menu-button"
icon={<OrganizationIcon />}
menuIcon={noDropDownContent ? null : undefined}
onClick={() => {
setFilterText("");
setShowMenuTriggerPopover(false);
}}
>
<span className={styles.organizationlabel}>
{currentOrganization?.label ?? value}
</span>
<div ref={orgMenuRef}></div>
</MenuButton>
</MenuTrigger>
<MenuPopover>
{filter?.showFilter && showSearch && (
<>
{!onlyCustomContent && <MenuDivider />}
{customContent}
<SearchBox
placeholder={filter.placeholderText}
ref={filterRef}
value={filterText}
onChange={onFilterChange}
appearance="filled-lighter"
className={styles.searchInput}
// To not get focus in search on open
tabIndex={-1}
// To keep focus on search when hovering menu items
onBlur={() => {
// Delay focus change one frame since SearchBox won't be able to react properly e.g.
// hide/show its X-button
window.requestAnimationFrame(() => {
filterRef.current?.focus();
});
}}
/>
<MenuDivider />
</>
)}
</MenuList>
</MenuPopover>
</Menu>
<MenuList>
{!onlyCustomContent && (
<div ref={menuListRef} className={styles.organizationSelection}>
{filteredOptions?.map(({ id, label }) => {
const match = label.toLowerCase().indexOf(filterText);
return (
<MenuItemRadio
data-testid={`organization-menu-item-${id}`}
key={id}
name="org"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onChange(id)}
value={id}
>
{
<span>
{label.substring(0, match)}
<span className={styles.bold}>
{label.substring(match, match + filterText.length)}
</span>
{label.substring(match + filterText.length)}
</span>
}
</MenuItemRadio>
);
})}
</div>
)}
{customContent !== undefined && (
<>
{!onlyCustomContent && <MenuDivider />}
{customContent}
</>
)}
</MenuList>
</MenuPopover>
</Menu>
<Popover
withArrow
positioning={{
positioningRef: popoverPositioningRef,
position: "below",
align: "end",
offset: {
mainAxis: 20,
crossAxis: 30,
},
}}
open={showMenuTriggerPopover}
unstable_disableAutoFocus
>
<PopoverSurface tabIndex={-1}>
<div className={styles.popoverRoot}>
<div className={styles.popoverTitle}>
<Subtitle1>{popoverInfo?.title}</Subtitle1>
<Button
size="small"
appearance={"transparent"}
icon={<DismissRegular />}
onClick={() => setShowMenuTriggerPopover(false)}
/>
</div>
<Body2>{popoverInfo?.body}</Body2>
</div>
</PopoverSurface>
</Popover>
</>
);
};

Expand Down
1 change: 1 addition & 0 deletions components/topbar/src/organization-menu.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export type OrganizationMenuProps = PropsWithChildren<{
readonly onChange: (id: string) => void;
readonly options?: OrganizationOption[];
readonly filter?: { showFilter: boolean; placeholderText: string };
readonly popoverInfo?: { title: string; body: string };
}>;
13 changes: 12 additions & 1 deletion components/topbar/src/organization.styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { makeStyles } from "@fluentui/react-components";
import { makeStyles, tokens } from "@fluentui/react-components";

export const useStyles = makeStyles({
standalone: {
Expand All @@ -25,4 +25,15 @@ export const useStyles = makeStyles({
bold: {
fontWeight: "bold",
},
popoverRoot: {
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalM,
maxWidth: "360px",
},
popoverTitle: {
display: "flex",
justifyContent: "space-between",
gap: tokens.spacingHorizontalL,
},
});
3 changes: 3 additions & 0 deletions examples/src/components/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ export const Navbar = () => {
showFilter: true,
placeholderText: "Search organization",
},
popoverInfo: currentOrganizationId === "2"
? { title: "PopOver", body: "Say something about the org item!" }
: undefined,
}}
profileMenu={{
// showCustomContentTopDivider: false,
Expand Down

0 comments on commit a2fac3b

Please sign in to comment.