Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/documentation/docs/assets/ListToolbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/documentation/docs/assets/ListToolbar0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 142 additions & 0 deletions docs/documentation/docs/controls/ListToolbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# ListToolbar control

This control renders a flexible toolbar for building list and data grid command bars. It is built with Fluent UI 9 components and supports item grouping, left/right aligned items, dividers, tooltips, automatic overflow menu, responsive design, loading states, and custom rendering.

Here is an example of the control in action:

![ListToolbar control](../assets/ListToolbar.png)
![ListToolbar control1](../assets/ListToolbar0.png)
![ListToolbar control](../assets/ListToolBarMultiple.png)
![ListToolbar control](../assets/ListToolBarMobile.png)

## How to use this control in your solutions

- 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.
- Import the following modules to your component:

```TypeScript
import { ListToolbar } from '@pnp/spfx-controls-react/lib/controls/ListToolbar';
import { IToolbarItem } from '@pnp/spfx-controls-react/lib/controls/ListToolbar';
```

- Use the `ListToolbar` control in your code as follows:

```TypeScript
<ListToolbar
items={[
{ key: 'new', label: 'New', icon: <AddRegular />, onClick: () => console.log('New') },
{ key: 'edit', label: 'Edit', icon: <EditRegular />, onClick: () => console.log('Edit') },
{ key: 'delete', label: 'Delete', icon: <DeleteRegular />, onClick: () => console.log('Delete') },
]}
context={this.props.context}
ariaLabel="Document toolbar"
/>
```

- With the `farItems` property you can add items that are aligned to the right side of the toolbar:

```TypeScript
<ListToolbar
items={[
{ key: 'new', label: 'New', icon: <AddRegular />, onClick: () => {} },
]}
farItems={[
{ key: 'filter', label: 'Filter', icon: <FilterRegular />, onClick: () => {} },
{ key: 'settings', icon: <SettingsRegular />, tooltip: 'Settings', onClick: () => {} },
]}
context={this.props.context}
/>
```

- Use the `group` property on items to visually group them with dividers:

```TypeScript
<ListToolbar
items={[
{ key: 'new', label: 'New', icon: <AddRegular />, group: 'file', onClick: () => {} },
{ key: 'edit', label: 'Edit', icon: <EditRegular />, group: 'file', onClick: () => {} },
{ key: 'copy', label: 'Copy', icon: <CopyRegular />, group: 'clipboard', onClick: () => {} },
{ key: 'paste', label: 'Paste', icon: <ClipboardPasteRegular />, group: 'clipboard', onClick: () => {} },
]}
showGroupDividers={true}
context={this.props.context}
/>
```

- Use the `totalCount` property to display a count badge in the toolbar:

```TypeScript
<ListToolbar
items={items}
totalCount={42}
context={this.props.context}
/>
```

- Use the `isLoading` property to disable all toolbar items during a loading state:

```TypeScript
<ListToolbar
items={items}
isLoading={true}
context={this.props.context}
/>
```

- Use the `onRender` property on an item for complete custom rendering:

```TypeScript
const farItems: IToolbarItem[] = [
{
key: 'search',
onRender: () => (
<SearchBox placeholder="Search..." onChange={(e, data) => onSearch(data.value)} />
),
},
];

<ListToolbar items={items} farItems={farItems} context={this.props.context} />
```

### Overflow & responsive behavior

When the toolbar is too narrow to show all left-side items, they automatically collapse into a **"..."** overflow menu. The overflow menu appears right next to the last visible item and lists every hidden action.

On small screens (≤ 768 px), far-item **labels are hidden** and only their icons are shown. The total-count badge is also hidden at that breakpoint.

## Implementation

The `ListToolbar` control can be configured with the following properties:

| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
| items | IToolbarItem[] | yes | Array of toolbar items to display on the left side. | |
| farItems | IToolbarItem[] | no | Items that appear on the right side of the toolbar. | `[]` |
| isLoading | boolean | no | When `true`, all items are disabled. | `false` |
| ariaLabel | string | no | Accessibility label for the toolbar. | `'Toolbar'` |
| totalCount | number | no | Displays a count badge in the toolbar. | |
| className | string | no | Additional CSS class name to apply to the toolbar. | |
| showGroupDividers | boolean | no | Whether to show dividers between item groups. | `true` |
| theme | Theme | no | Fluent UI v8 theme (auto-converted to v9 via `createV9Theme`). | |
| context | BaseComponentContext | no | SPFx component context. Enables automatic Teams theme detection (dark, high-contrast). | |

### IToolbarItem

| Property | Type | Required | Description | Default |
| ---- | ---- | ---- | ---- | ---- |
| key | string | yes | Unique identifier for the item. | |
| label | string | no | Button text label. | |
| tooltip | string | no | Tooltip content shown on hover. | |
| icon | ReactElement | no | Icon element to display. | |
| onClick | () => void | no | Click handler for the item. | |
| disabled | boolean | no | Whether the item is disabled. | `false` |
| visible | boolean | no | Whether to show or hide the item. | `true` |
| group | string | no | Group name — items with the same group are grouped together with dividers between groups. | `'default'` |
| isFarItem | boolean | no | Place the item on the right side of the toolbar. | `false` |
| appearance | ToolbarButtonProps['appearance'] | no | Button appearance style. | |
| onRender | () => ReactElement | no | Custom render function for complete control over item rendering. | |
| dividerAfter | boolean | no | Add a divider after this item. | `false` |
| dividerBefore | boolean | no | Add a divider before this item. | `false` |
| ariaLabel | string | no | Accessibility label override. | |

![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/ListToolbar)
11 changes: 11 additions & 0 deletions src/controls/ListToolbar/FlatItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IToolbarItem } from './IToolbarItem';

/* ------------------------------------------------------------------ */
/* FlatItem — a union type for rendered toolbar elements */
/* ------------------------------------------------------------------ */

export interface FlatItem {
type: 'item' | 'divider';
item?: IToolbarItem;
key: string;
}
28 changes: 28 additions & 0 deletions src/controls/ListToolbar/IListToolbarProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseComponentContext } from "@microsoft/sp-component-base";
import { IToolbarItem } from "./IToolbarItem";
import { Theme } from "@fluentui/react";

/**
* Props for the ListToolbar component
*/

export interface IListToolbarProps {
/** Array of toolbar items */
items: IToolbarItem[];
/** Far items that appear on the right side of the toolbar */
farItems?: IToolbarItem[];
/** Whether the toolbar is in a loading state */
isLoading?: boolean;
/** Aria label for the toolbar */
ariaLabel?: string;
/** Total count to display (optional) */
totalCount?: number;
/** Custom class name */
className?: string;
/** Whether to show dividers between groups */
showGroupDividers?: boolean;
/** Theme for the toolbar */
theme?: Theme;
/** Context for the web part */
context?: BaseComponentContext;
}
37 changes: 37 additions & 0 deletions src/controls/ListToolbar/IToolbarItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ToolbarButtonProps } from "@fluentui/react-components";
import * as React from "react";

/**
* Extended toolbar item interface
*/

export interface IToolbarItem {
/** Unique key for the item */
key: string;
/** Label text for the button */
label?: string;
/** Tooltip content */
tooltip?: string;
/** Icon element */
icon?: React.ReactElement;
/** Click handler */
onClick?: () => void;
/** Whether the item is disabled */
disabled?: boolean;
/** Whether to show the item */
visible?: boolean;
/** Group name - items with the same group will be grouped together */
group?: string;
/** Whether this is a far item (appears on the right side) */
isFarItem?: boolean;
/** Button appearance */
appearance?: ToolbarButtonProps["appearance"];
/** Custom render function for complete control */
onRender?: () => React.ReactElement;
/** Whether to add a divider after this item */
dividerAfter?: boolean;
/** Whether to add a divider before this item */
dividerBefore?: boolean;
/** Aria label override */
ariaLabel?: string;
}
13 changes: 13 additions & 0 deletions src/controls/ListToolbar/IToolbarItemRendererProps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IToolbarItem } from './IToolbarItem';


export interface IToolbarItemRendererProps {
/** The toolbar item to render */
item: IToolbarItem;
/** Whether the toolbar is in a loading state */
isLoading?: boolean;
/** CSS class applied to the button root */
itemClass?: string;
/** CSS class applied to the label text (e.g. to hide on mobile) */
labelClass?: string;
}
79 changes: 79 additions & 0 deletions src/controls/ListToolbar/ListToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as React from "react";

import { Provider } from "jotai";
import { has } from "lodash";

import {
FluentProvider,
IdPrefixProvider,
teamsDarkTheme,
teamsHighContrastTheme,
teamsLightTheme,
Theme,
} from "@fluentui/react-components";
import { createV9Theme } from "@fluentui/react-migration-v8-v9";
import { useTheme } from "@fluentui/react-theme-provider";
import { WebPartContext } from "@microsoft/sp-webpart-base";


import { IListToolbarProps } from "./IListToolbarProps";
import ListToolbarControl from "./ListToolbarControl";

export const ListToolbar: React.FunctionComponent<IListToolbarProps> = (
props: React.PropsWithChildren<IListToolbarProps >
) => {
const { theme: themeV8, context } = props;
const [theme, setTheme] = React.useState<Theme>();
const currentSPTheme = useTheme();
const [isInitialized, setIsInitialized] = React.useState<boolean>(false);


React.useEffect(() => {
(async () => {
try {
if (has(context, "sdks.microsoftTeams.teamsJs.app.getContext")) {
const teamsContext = await (context as WebPartContext).sdks.microsoftTeams?.teamsJs.app.getContext();
const teamsTheme = teamsContext.app.theme || "default";
switch (teamsTheme) {
case "dark":
setTheme(teamsDarkTheme);
break;
case "contrast":
setTheme(teamsHighContrastTheme);
break;
case "default":
setTheme(teamsLightTheme);
break;
default:
setTheme(teamsLightTheme);
break;
}
} else if (themeV8 || currentSPTheme) {
setTheme(createV9Theme(themeV8 ?? currentSPTheme));
} else {
setTheme(teamsLightTheme);
}
} catch (error) {
console.warn("ListToolbar: Failed to resolve theme, using default", error);
setTheme(teamsLightTheme);
}
setIsInitialized(true);
})();
}, [context, currentSPTheme, themeV8]);

if (!isInitialized) return <></>;

return (
<>
<IdPrefixProvider value="userPicker-">
<FluentProvider theme={theme}>
<Provider>
<ListToolbarControl {...props} />
</Provider>
</FluentProvider>
</IdPrefixProvider>
</>
);
};
Loading