Skip to content
Draft
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
2 changes: 2 additions & 0 deletions apps/public-docsite-v9-headless/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const parameters = {
dirSwitcher: true,
// headless components don't support theming
themePicker: false,
// CAP visual language only applies to @fluentui/react-components
visualLanguagePicker: false,
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import { buttonClassNames } from '@fluentui/react-button';
import { ButtonProps } from '@fluentui/react-button';
import { ButtonSlots } from '@fluentui/react-button';
import { ButtonState } from '@fluentui/react-button';
import { CAP_STYLE_HOOKS } from '@fluentui-contrib/react-cap-theme';
import { Caption1 } from '@fluentui/react-text';
import { caption1ClassNames } from '@fluentui/react-text';
import { Caption1Strong } from '@fluentui/react-text';
Expand Down Expand Up @@ -2294,6 +2295,8 @@ export { ButtonSlots }

export { ButtonState }

export { CAP_STYLE_HOOKS }

export { Caption1 }

export { caption1ClassNames }
Expand Down
3 changes: 2 additions & 1 deletion packages/react-components/react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
"@fluentui/react-motion": "^9.16.0",
"@fluentui/react-carousel": "^9.9.8",
"@fluentui/react-color-picker": "^9.2.17",
"@fluentui/react-nav": "^9.4.0"
"@fluentui/react-nav": "^9.4.0",
"@fluentui-contrib/react-cap-theme": "^0.4.2"
},
"peerDependencies": {
"@types/react": ">=16.14.0 <20.0.0",
Expand Down
5 changes: 5 additions & 0 deletions packages/react-components/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export type {
FluentProviderSlots,
FluentProviderState,
} from '@fluentui/react-provider';

// CAP theme overlay β€” pass to `<FluentProvider customStyleHooks_unstable={CAP_STYLE_HOOKS} />`
// to opt in to the CAP visual treatment without installing a separate package.
// See: docs/react-v9/contributing/rfcs/shared/cap-theme-in-fluent-v9.md
export { CAP_STYLE_HOOKS } from '@fluentui-contrib/react-cap-theme';
export {
createCustomFocusIndicatorStyle,
createFocusOutlineStyle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import * as React_2 from 'react';
import type { Renderer } from 'storybook/internal/types';
import type { StoryContext } from '@storybook/react-webpack5';

// @public (undocumented)
export const CAP_ID: "storybook_fluentui-react-addon_cap";

// @public (undocumented)
export const DIR_ID: "storybook_fluentui-react-addon_dir";

Expand All @@ -30,6 +33,8 @@ export type FluentDocsPageProps = {

// @public
export interface FluentGlobals extends Args {
// (undocumented)
[CAP_ID]?: boolean;
// (undocumented)
[DIR_ID]?: 'ltr' | 'rtl';
// (undocumented)
Expand All @@ -40,6 +45,8 @@ export interface FluentGlobals extends Args {

// @public
export interface FluentParameters extends Parameters_2 {
// (undocumented)
cap?: boolean;
// (undocumented)
dir?: 'ltr' | 'rtl';
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"@fluentui/react-spinner": "^9.8.3",
"@fluentui/react-toast": "^9.8.0",
"@griffel/react": "^1.5.32",
"@swc/helpers": "^0.5.1"
"@swc/helpers": "^0.5.1",
"@fluentui-contrib/react-cap-theme": "^0.4.2"
},
"peerDependencies": {
"@storybook/addon-docs": "^9.1.17",
Expand Down
15 changes: 15 additions & 0 deletions packages/react-components/react-storybook-addon/src/cap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* CAP visual-language option ids stored as a Storybook global.
*
* Modeled after `ThemeIds` so the value persists in the URL as a readable
* token (e.g. `storybook_fluentui-react-addon_cap:cap`) instead of a boolean
* (`storybook_fluentui-react-addon_cap:!false`).
*/
export type CapIds = 'base' | 'cap';

export const capOptions: ReadonlyArray<{ id: CapIds; label: string }> = [
{ id: 'base', label: 'Fluent' },
{ id: 'cap', label: 'CAP' },
];

export const defaultCap: { id: CapIds; label: string } = capOptions[0];
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react';
import { IconButton, TooltipLinkList, WithTooltip } from 'storybook/internal/components';
import { PaintBrushIcon } from '@storybook/icons';
import { useParameter } from 'storybook/manager-api';

import type { JSXElement } from '@fluentui/react-utilities';
import type { CapIds } from '../cap';
import { capOptions, defaultCap } from '../cap';
import { CAP_ID } from '../constants';
import type { FluentParameters } from '../hooks';
import { useGlobals } from '../hooks';

export interface CapSelectorItem {
id: string;
title: string;
onClick: () => void;
value: string;
active: boolean;
}

function createCapItems(
value: typeof capOptions,
changeCap: (id: CapIds) => void,
getCurrentCap: () => CapIds,
): CapSelectorItem[] {
return value.map(item => {
return {
id: item.id,
title: item.id === defaultCap.id ? `${item.label} (Default)` : item.label,
onClick: () => {
changeCap(item.id);
},
value: item.id,
active: getCurrentCap() === item.id,
};
});
}

/**
* Toolbar picker that selects the CAP visual-language overlay on top of the
* currently selected Fluent base theme. When `cap` is active, the
* FluentProvider decorator applies `CAP_STYLE_HOOKS` from
* `@fluentui-contrib/react-cap-theme`.
*/
export const VisualLanguagePicker = (): JSXElement => {
const [globals, updateGlobals] = useGlobals();
const capParameter: FluentParameters['cap'] = useParameter('cap');

const selectedCapId: CapIds = capParameter ?? globals[CAP_ID] ?? defaultCap.id;
const selectedCap = capOptions.find(entry => entry.id === selectedCapId);

const isActive = selectedCapId !== defaultCap.id;

const setCap = React.useCallback(
(id: CapIds) => {
updateGlobals({ [CAP_ID]: id });
},
[updateGlobals],
);

const renderTooltip = React.useCallback(
(props: { onHide: () => void }) => {
return (
<TooltipLinkList
links={createCapItems(
capOptions,
id => {
setCap(id);
props.onHide();
},
() => selectedCapId,
)}
/>
);
},
[selectedCapId, setCap],
);

return (
<WithTooltip placement="top" trigger="click" closeOnOutsideClick tooltip={renderTooltip}>
<IconButton key={CAP_ID} title="Change visual language" active={isActive}>
<PaintBrushIcon />
<span style={{ marginLeft: 5 }}>Visual Language: {selectedCap?.label}</span>
</IconButton>
</WithTooltip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const ADDON_ID = 'storybook_fluentui-react-addon';
export const DIR_ID = `${ADDON_ID}_dir` as const;
export const STRICT_MODE_ID = `${ADDON_ID}_strict-mode` as const;
export const THEME_ID = `${ADDON_ID}_theme` as const;
export const CAP_ID = `${ADDON_ID}_cap` as const;
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
webDarkTheme,
webLightTheme,
} from '@fluentui/react-theme';
import { CAP_STYLE_HOOKS } from '@fluentui-contrib/react-cap-theme';
import type { ThemeIds } from '../theme';
import { defaultTheme } from '../theme';
import { DIR_ID, THEME_ID } from '../constants';
import { CAP_ID, DIR_ID, THEME_ID } from '../constants';
import type { FluentStoryContext } from '../hooks';
import { isDecoratorDisabled } from '../utils/isDecoratorDisabled';

const themes: Record<ThemeIds, Theme> = {
'web-light': webLightTheme,
'web-dark': webDarkTheme,
Expand All @@ -42,12 +42,27 @@ export const withFluentProvider = (StoryFn: () => JSXElement, context: FluentSto

const isVrTest = mode === 'vr-test';
const dir = parameters.dir ?? globals[DIR_ID] ?? 'ltr';
const globalTheme = findTheme(globals[THEME_ID]);
const paramTheme = findTheme(parameters.fluentTheme);
const globalThemeId: ThemeIds | undefined = globals[THEME_ID];
const paramThemeId: ThemeIds | undefined = parameters.fluentTheme;
const globalTheme = findTheme(globalThemeId);
const paramTheme = findTheme(paramThemeId);
const theme = paramTheme ?? globalTheme ?? themes[defaultTheme.id];

// CAP is a visual-language overlay applied on top of any base Fluent theme.
// Toggle via the Storybook toolbar (CAP switch) or per-story via `parameters.cap`.
const capValue = parameters.cap ?? globals[CAP_ID];
const capEnabled = capValue === 'cap';
const customStyleHooks = capEnabled ? CAP_STYLE_HOOKS : undefined;

// CAP style hooks add their own React hook calls inside each Fluent component
// (e.g. CAP's `useButtonStyles` calls 9 hooks). Switching `customStyleHooks_unstable`
// between `undefined` and an object on a live tree changes the hook count, which
// violates the Rules of Hooks. Keying on `capEnabled` forces the subtree to
// remount so the hook order stays consistent within each mount.
const providerKey = capEnabled ? 'cap-on' : 'cap-off';

return (
<FluentProvider theme={theme} dir={dir}>
<FluentProvider key={providerKey} theme={theme} dir={dir} customStyleHooks_unstable={customStyleHooks}>
{isVrTest ? StoryFn() : <FluentExampleContainer theme={theme}>{StoryFn()}</FluentExampleContainer>}
</FluentProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import { makeStyles } from '@griffel/react';
import { InfoFilled } from '@fluentui/react-icons';
import type { JSXElement } from '@fluentui/react-utilities';

import { DIR_ID, THEME_ID } from '../constants';
import { CAP_ID, DIR_ID, THEME_ID } from '../constants';
import { themes } from '../theme';

import { getDocsPageConfig } from './utils';
import { VisualLanguagePicker } from './VisualLanguagePicker';
import { DirSwitch } from './DirSwitch';
import { ThemePicker } from './ThemePicker';
import { Toc, nameToHash } from './Toc';
Expand Down Expand Up @@ -371,6 +372,7 @@ export const FluentDocsPage = ({
assertStoryMetaValues(primaryStory);

const dir = primaryStoryContext.parameters?.dir ?? primaryStoryContext.globals?.[DIR_ID] ?? 'ltr';
const capValue = primaryStoryContext.parameters?.cap ?? primaryStoryContext.globals?.[CAP_ID];
const selectedTheme = themes.find(theme => theme.id === primaryStoryContext.globals![THEME_ID]);

const hideArgsTable = Boolean(primaryStoryContext.parameters?.docs?.hideArgsTable);
Expand Down Expand Up @@ -398,6 +400,7 @@ export const FluentDocsPage = ({
tableOfContents: showTableOfContents,
dirSwitcher: showDirSwitcher,
themePicker: showThemePicker,
visualLanguagePicker: showVisualLanguagePicker,
copyAsMarkdown: showCopyAsMarkdown,
argTable,
} = docsPageConfig;
Expand All @@ -419,9 +422,10 @@ export const FluentDocsPage = ({
<Title />
<div className={styles.wrapper}>
<div className={styles.container}>
{(showThemePicker || showDirSwitcher || showCopyAsMarkdown) && (
{(showThemePicker || showDirSwitcher || showVisualLanguagePicker || showCopyAsMarkdown) && (
<div className={styles.globalTogglesContainer}>
{showThemePicker && <ThemePicker selectedThemeId={selectedTheme?.id} />}
{showVisualLanguagePicker && <VisualLanguagePicker selectedCapId={capValue} />}
{showDirSwitcher && <DirSwitch dir={dir} />}
{showCopyAsMarkdown && <CopyAsMarkdownButton storyId={primaryStory.id} />}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import { addons } from 'storybook/preview-api';

import { Menu, MenuItemRadio, MenuList, MenuPopover, MenuTrigger } from '@fluentui/react-menu';
import type { MenuProps } from '@fluentui/react-menu';
import { MenuButton } from '@fluentui/react-button';
import { makeStyles } from '@griffel/react';

import type { CapIds } from '../cap';
import { capOptions, CAP_ID } from '..';

const useStyles = makeStyles({
menuButton: {
minWidth: '160px',
justifyContent: 'flex-start',
},

chevronIcon: {
marginLeft: 'auto',
},

menuPopover: {
minWidth: '160px',
},
});

/**
* CAP visual-language picker used in the react-components docs header.
*
* Mirrors `ThemePicker`: stores the selected option id (`'cap' | 'base'`)
* as a Storybook global so it persists in the URL as a readable value.
*/
export const VisualLanguagePicker: React.FC<{ selectedCapId?: CapIds }> = ({ selectedCapId }) => {
const styles = useStyles();
const [currentCapId, setCurrentCapId] = React.useState<CapIds | null>(selectedCapId ?? null);

const setGlobalCap = (capId: CapIds): void => {
addons.getChannel().emit('updateGlobals', { globals: { [CAP_ID]: capId } });
};
const onCheckedValueChange: MenuProps['onCheckedValueChange'] = (_e, data) => {
const newCapId = data.checkedItems[0] as CapIds;
setGlobalCap(newCapId);
setCurrentCapId(newCapId);
};

const selectedCap = capOptions.find(o => o.id === currentCapId);

return (
<Menu
// eslint-disable-next-line react/jsx-no-bind
onCheckedValueChange={onCheckedValueChange}
checkedValues={{ cap: selectedCapId ? [selectedCapId] : [] }}
positioning={{ autoSize: true }}
>
<MenuTrigger>
<MenuButton className={styles.menuButton} menuIcon={{ className: styles.chevronIcon }}>
{selectedCap?.label ?? 'Visual Language'}
</MenuButton>
</MenuTrigger>
<MenuPopover className={styles.menuPopover}>
<MenuList>
{capOptions.map(o => (
<MenuItemRadio name="cap" value={o.id} key={o.id}>
{o.label}
</MenuItemRadio>
))}
</MenuList>
</MenuPopover>
</Menu>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const docsDefaults = {
tableOfContents: true,
dirSwitcher: true,
themePicker: true,
visualLanguagePicker: true,
argTable: {
slotsApi: true,
nativePropsApi: true,
Expand Down Expand Up @@ -46,6 +47,7 @@ export function getDocsPageConfig(context: DocsContextProps): {
tableOfContents: boolean;
dirSwitcher: boolean;
themePicker: boolean;
visualLanguagePicker: boolean;
copyAsMarkdown: boolean;
argTable: {
slotsApi: boolean;
Expand All @@ -66,6 +68,7 @@ export function getDocsPageConfig(context: DocsContextProps): {
tableOfContents: docsConfig.tableOfContents !== false,
dirSwitcher: docsConfig.dirSwitcher !== false,
themePicker: docsConfig.themePicker !== false,
visualLanguagePicker: docsConfig.visualLanguagePicker !== false,
argTable: getArgTableConfig(docsConfig.argTable),
};
}
Expand Down
Loading
Loading