@microsoft/fabric-visuals-extensibility
v4.0.0
Published
Extensibility components for Fabric Apps - Analytics
Readme
fabric-visuals-extensibility
Extensibility components for Fabric Apps - Analytics.
Components
VisualContainer
Wraps any children in a div with a card background and border, an optional
title/subtitle header, an optional hover/focus-revealed action toolbar, and a
right-click actions menu.
import { VisualContainer } from '@microsoft/fabric-visuals-extensibility';
import type { VisualContainerAction } from '@microsoft/fabric-visuals-extensibility';
const refresh: VisualContainerAction = {
id: 'refresh',
label: 'Refresh',
icon: <RefreshIcon />,
onClick: () => reload(),
};
function Example() {
return (
<VisualContainer header={{ title: 'Sales overview', subtitle: 'Last 12 months' }} customActions={[refresh]}>
<p>Anything rendered here is wrapped in a bordered container.</p>
</VisualContainer>
);
}Props
| Prop | Type | Description |
| ------------------- | ------------------------- | ------------------------------------------------------------------------------------ |
| children | ReactNode | Content rendered inside the bordered container. |
| header | HeaderProps | Optional header rendered above the content. |
| builtInActionIds | BuiltInActionId[] | Built-in actions rendered in the header toolbar by id (e.g. ['copyVisual']). |
| customActions | VisualContainerAction[] | Custom action buttons rendered on the trailing edge of the header. |
| maxVisibleButtons | number | Max toolbar buttons before extras collapse into a … overflow menu. Defaults to 2. |
| className | string | CSS class merged onto the container root, alongside the container's own class rather than in place of it. |
HeaderProps:
| Prop | Type | Description |
| ---------- | -------- | ------------------------------------------------------------ |
| title | string | Title rendered in the header above the content. |
| subtitle | string | Optional subtitle rendered beneath the title in the header. |
When no header is provided, no header is rendered and the actions move into a
floating toolbar that appears just above the container's top-trailing corner on
hover or keyboard focus. It sits outside the container's box, so it never covers
the content underneath and is never clipped by the container's own rounding.
Styling the container
className lands on the container root — the element that draws the card
background, border, radius, and padding. It is merged with the container's own class
(mergeClasses(styles.container, className)), so the built-in chrome stays in
place and your class is added on top of it. It does not reach the header, the
body region, or the toolbar.
<VisualContainer header={{ title: 'Revenue' }} className="app-card">
{visual}
</VisualContainer>Toolbar
customActions render as a toolbar on the trailing edge of the header,
opposite the title/subtitle. The toolbar is hover/focus-revealed: it
claims no width while idle, then expands in-flow on pointer hover over the container
or keyboard focus within it so the header reflows instead of overlaying the
title text.
When there are visible actions (built-in + custom, excluding hidden), right-clicking
inside the container opens a context menu (role="menu") with the same action list.
Selecting an item runs that action and closes the menu. The menu also closes on
outside click and Escape. If there are no visible actions, the container does
not intercept contextmenu, so the browser's native context menu opens as usual.
VisualContainerAction:
| Prop | Type | Description |
| ---------- | ----------------------------- | ---------------------------------------------------------------- |
| id | string | Stable id used as the React key. |
| label | string | Accessible label used as both the aria-label and the tooltip. |
| icon | ReactNode | Icon element rendered inside the button. |
| onClick | () => void \| Promise<void> | Invoked on activation. May be async. |
| disabled | boolean | When true, the button is rendered disabled. |
| hidden | boolean | When true, the button is removed from the toolbar completely. |
BuiltInActionId:
| Id | Label | Behavior |
| ------------ | ------ | ------------------------------------------------------------------------- |
| copyVisual | Copy | Captures the container as a PNG and writes it to the system clipboard, captioned with the header title and the time of the copy. |
Built-in actions are opt-in by id through builtInActionIds and render ahead of
customActions:
<VisualContainer header={{ title: 'Revenue' }} builtInActionIds={['copyVisual']}>{visual}</VisualContainer>Copy Visual — image capture & clipboard
The built-in copyVisual action and the imperative handle share one
capture-and-clipboard path:
Capture rasterizes the container to a PNG with
modern-screenshot. The library serializes a clone of the DOM, so the live UI stays perfectly still while the capture runs. Any node taggedvc-capture-exclude(exported asCAPTURE_EXCLUDE_ATTR) is dropped from the image — the toolbar is flagged this way so the buttons never land in a copied picture — and the resolved background color keeps exported PNGs opaque.Clipboard writes a single
ClipboardItemcarrying three MIME flavors of the same copy, so each target can take the richest one it understands:| Flavor | Payload | Typical target | | ------------ | -------------------------------------------------------------------------------------------------- | -------------------- | |
text/html| an<img>with the PNG inlined as adata:URL, then the title and copy time as two styled<div>s | Word, Outlook, Teams | |text/plain| those same two caption lines, newline separated | editors, chat boxes | |image/png| the capture on its own | image-only targets |A container with no header, or one given a React node as its header, has no title to caption with, so the caption is the timestamp alone. The write needs a secure context (https) and a user gesture — the toolbar click is one — and a cross-origin iframe host must also grant
allow="clipboard-write".
Both steps are also exported as standalone functions, for the case where there
is no VisualContainer at all — a bare visual rendered on its own still needs
a supported way to be captured and copied:
| Function | Description |
| --------------------------------------------------- | ------------------------------------------------------ |
| captureElementAsImage(element: HTMLElement) | Rasterizes any element to a PNG Blob. |
| writeImageToClipboard(blob: Blob, caption?: CopyVisualCaption) | Writes an image Blob to the system clipboard, captioned when a caption is given. |
CopyVisualCaption is the caption's lines exactly as they should read — the
timestamp is a string, so its locale and format are yours to choose:
| Prop | Type | Description |
| ----------- | -------- | -------------------------------------------------------------------------------- |
| title | string | The caption's lead line. |
| timestamp | string | Already-formatted moment of the copy, rendered beneath the title. |
Either line may be omitted; a caption with neither writes the image alone. The
container fills both in for you, stamping the copy time with the viewer's locale.
title is treated as plain text, so a title like P&L <Consolidated> pastes as
itself; timestamp is written to the HTML flavor as given, so pass formatted
text rather than markup.
import { useRef } from 'react';
import { captureElementAsImage, writeImageToClipboard } from '@microsoft/fabric-visuals-extensibility';
const ref = useRef<HTMLDivElement>(null);
async function copyChart() {
if (!ref.current) return;
await writeImageToClipboard(await captureElementAsImage(ref.current), {
title: 'Revenue',
timestamp: new Date().toLocaleString(),
});
}
<div ref={ref}>
<MyVisual />
</div>;captureElementAsImage accepts any HTMLElement and honors
CAPTURE_EXCLUDE_ATTR on its descendants, so you can drop your own chrome out of
the image exactly the way the container drops its toolbar. writeImageToClipboard
captions the copy the way the container does; omit the caption to write the image
alone. It carries the same secure-context and user-gesture requirements described
above — call it from an event handler, not from an effect.
This does not replace the container paths, and the hierarchy above still
holds. Inside a VisualContainer, builtInActionIds={['copyVisual']} remains the
right answer, and the imperative handle is next when you
need the capability without the built-in button. Reach for these functions only
when there is no container to ask.
Imperative handle
Prefer built-in actions. When a BuiltInActionId already does what you need,
opt into it with builtInActionIds: the container renders the button, wires the
behavior, and reports failures. That path needs no ref.
The handle exposes the same underlying capabilities the built-ins are implemented
with. Use a ref only when the behavior you need is not available as a built-in
action — either to build a custom action on top of a container capability, or to
drive that capability from outside the container:
| Goal | Use |
| ---------------------------------------------------------------- | -------------------------------------- |
| Behavior that matches an existing BuiltInActionId | builtInActionIds — no ref |
| A custom action needing a capability the container provides | ref + customActions |
| Driving a container capability from outside the container | ref |
Do not use a ref to reimplement a built-in as a custom action. If the
behavior is already a BuiltInActionId, opt into it instead — hand-rolling a
Copy action around copyImageToClipboard, for example, is strictly worse than
builtInActionIds={['copyVisual']}.
The example below adds a custom Download PNG action on top of captureAsImage.
Downloading has no built-in equivalent, so the handle is the right tool here —
while copy, which does have one, still comes from the built-in:
import { useRef } from 'react';
import { VisualContainer } from '@microsoft/fabric-visuals-extensibility';
import type { VisualContainerAction, VisualContainerHandle } from '@microsoft/fabric-visuals-extensibility';
const ref = useRef<VisualContainerHandle>(null);
const actions: VisualContainerAction[] = [{
id: 'download',
label: 'Download PNG',
icon: <DownloadIcon />,
onClick: async () => {
const blob = await ref.current?.captureAsImage();
if (!blob) return;
const url = URL.createObjectURL(blob);
Object.assign(document.createElement('a'), { href: url, download: 'chart.png' }).click();
URL.revokeObjectURL(url);
},
}];
// `copyVisual` still comes from the built-in — only the download needs the ref.
<VisualContainer
ref={ref}
header={{ title: 'Revenue' }}
builtInActionIds={['copyVisual']}
customActions={actions}
>
{visual}
</VisualContainer>;| Member | Type | Description |
| ---------------------- | ------------------------- | -------------------------------------------------- |
| element | HTMLElement \| null | The container's root (capture target) element. |
| captureAsImage | () => Promise<Blob> | Captures the container as a PNG Blob. |
| copyImageToClipboard | () => Promise<void> | Captures the container and writes it to the clipboard, captioned with the header title and the time of the copy. |
When the number of visible actions exceeds maxVisibleButtons, the first
maxVisibleButtons - 1 stay inline and the rest collapse into a … overflow
menu (role="menu"). The menu closes on outside click, Escape, focus moving
away, or activating an item.
The border and header are styled with the shared Fabric design tokens, so the container automatically follows the active theme:
| Aspect | Design token | Resolves to |
| ------------- | -------------------------- | ------------------ |
| Border color | --color-border | theme stroke color |
| Corner radius | --radius-md | 4px |
| Title text | --color-foreground | theme text color |
| Subtitle text | --color-muted-foreground | muted text color |
The border width is a standard 1px hairline.
Toast feedback
The built-in copyVisual action reports its own outcome — the container owns
that button, so it owns the feedback:
| Outcome | Toast | Role | Auto-dismiss |
| ------- | ------------------------------------------------------------------------ | -------- | ------------ |
| Success | Visual copied — The image is on your clipboard. | status | 4s |
| Failure | Couldn't copy this visual — a description that depends on why the copy failed: the clipboard being blocked (a sandboxed or cross-origin frame), unavailable (an insecure page or an unsupporting browser), or anything else. | alert | 10s |
The imperative handle stays silent. copyImageToClipboard() on a ref shows no toast, leaving ref callers
free to surface failures through their own notification system.
