diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 519558c4fef05d..64f2760109733f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -331,6 +331,8 @@ packages/react-components/component-selector-preview/library @microsoft/teams-pr packages/react-components/component-selector-preview/stories @microsoft/teams-prg packages/react-components/react-menu-grid-preview/library @microsoft/teams-prg packages/react-components/react-menu-grid-preview/stories @microsoft/teams-prg +packages/react-components/react-headless-components-preview/library @microsoft/cxe-prg +packages/react-components/react-headless-components-preview/stories @microsoft/cxe-prg # <%= NX-CODEOWNER-PLACEHOLDER %> # Deprecated v9 packages - exposed as part of `/unstable` api diff --git a/apps/pr-deploy-site/just.config.ts b/apps/pr-deploy-site/just.config.ts index ff4ef576211db4..0aac32b650dde1 100644 --- a/apps/pr-deploy-site/just.config.ts +++ b/apps/pr-deploy-site/just.config.ts @@ -3,6 +3,10 @@ import path from 'path'; import { series, task, copyInstructionsTask, copyInstructions, cleanTask } from '@fluentui/scripts-tasks'; import { findGitRoot, getAllPackageInfo } from '@fluentui/scripts-monorepo'; +function getDeployDirectoryName(packageName: string) { + return packageName.replace(/^@[^/]+\//, ''); +} + task('clean', cleanTask()); const gitRoot = findGitRoot(); @@ -29,6 +33,7 @@ const dependencies = [ '@fluentui/public-docsite-v9', '@fluentui/perf-test-react-components', '@fluentui/theme-designer', + '@fluentui/react-headless-components-preview-stories', // web-components '@fluentui/web-components', // charting @@ -45,7 +50,10 @@ repoDeps.forEach(dep => { if (fs.existsSync(packageDist)) { instructions.push( - ...copyInstructions.copyFilesInDirectory(packageDist, path.join('dist', path.basename(dep.packagePath))), + ...copyInstructions.copyFilesInDirectory( + packageDist, + path.join('dist', getDeployDirectoryName(dep.packageJson.name)), + ), ); deployedPackages.add(dep.packageJson.name); } diff --git a/apps/pr-deploy-site/pr-deploy-site.js b/apps/pr-deploy-site/pr-deploy-site.js index 879dec42ed87fe..5b19c761885997 100644 --- a/apps/pr-deploy-site/pr-deploy-site.js +++ b/apps/pr-deploy-site/pr-deploy-site.js @@ -84,6 +84,12 @@ function main() { icon: 'CheckMark', title: 'Theme Designer v9', }, + { + package: '@fluentui/react-headless-components-preview-stories', + link: './react-headless-components-preview-stories/storybook/index.html', + icon: 'Code', + title: '@fluentui/react-headless-components-preview Storybook', + }, { package: '@fluentui/perf-test', link: './perf-test/index.html', diff --git a/packages/react-components/react-headless-components-preview/library/.swcrc b/packages/react-components/react-headless-components-preview/library/.swcrc new file mode 100644 index 00000000000000..6ae360015ce576 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/.swcrc @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "exclude": [ + "/testing", + "/**/*.cy.ts", + "/**/*.cy.tsx", + "/**/*.spec.ts", + "/**/*.spec.tsx", + "/**/*.test.ts", + "/**/*.test.tsx" + ], + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true, + "decorators": false, + "dynamicImport": false + }, + "externalHelpers": true, + "transform": { + "react": { + "runtime": "classic", + "useSpread": true + } + }, + "target": "es2022" + }, + "minify": false, + "sourceMaps": true +} diff --git a/packages/react-components/react-headless-components-preview/library/LICENSE b/packages/react-components/react-headless-components-preview/library/LICENSE new file mode 100644 index 00000000000000..2ba10ea0162426 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/LICENSE @@ -0,0 +1,15 @@ +@fluentui/react-headless-components-preview + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license diff --git a/packages/react-components/react-headless-components-preview/library/README.md b/packages/react-components/react-headless-components-preview/library/README.md new file mode 100644 index 00000000000000..b7b4ec1271421e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/README.md @@ -0,0 +1,7 @@ +# @fluentui/react-headless-components-preview + +**React Headless Components for [Fluent UI React](https://react.fluentui.dev/)** + +> [!WARNING] > **This package is in preview and not production-ready.** APIs may change without notice before final release. **Do not use in production.** +> +> This package exposes unstyled, headless Fluent UI v9 primitives for teams building custom design systems. For most teams, [`@fluentui/react-components`](https://www.npmjs.com/package/@fluentui/react-components) remains the recommended default. diff --git a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js new file mode 100644 index 00000000000000..f697eb476b514b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js @@ -0,0 +1,7 @@ +import * as rhc from '@fluentui/react-headless-components-preview'; + +console.log(rhc); + +export default { + name: 'react-headless-components-preview: entire library', +}; diff --git a/packages/react-components/react-headless-components-preview/library/config/api-extractor.json b/packages/react-components/react-headless-components-preview/library/config/api-extractor.json new file mode 100644 index 00000000000000..8d482156d10d53 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json", + "mainEntryPointFilePath": "/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts" +} diff --git a/packages/react-components/react-headless-components-preview/library/config/tests.js b/packages/react-components/react-headless-components-preview/library/config/tests.js new file mode 100644 index 00000000000000..c6c67de97059e8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/config/tests.js @@ -0,0 +1,3 @@ +/** Jest test setup file. */ + +require('@testing-library/jest-dom'); diff --git a/packages/react-components/react-headless-components-preview/library/docs/Spec.md b/packages/react-components/react-headless-components-preview/library/docs/Spec.md new file mode 100644 index 00000000000000..a0a32980547623 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/docs/Spec.md @@ -0,0 +1,201 @@ +# @fluentui/react-headless-components Spec + +## Background + +`@fluentui/react-headless-components` is an **advanced, opt-in** package that exposes the headless layer of Fluent UI v9 components: pure component logic, accessibility patterns, and semantic slot structure — without any styling opinions. + +It is intended for teams building custom design systems that significantly diverge from Fluent 2. For most teams, the default styled components in `@fluentui/react-components` remain the recommended path. + +**What this package provides:** + +- Unstyled primitive components (headless components) +- Component behavior, structure, and ARIA patterns +- Keyboard handling +- Semantic slot structure +- Building blocks for advanced composition: `use{Component}` and `render{Component}` +- Optional context-value hooks for compound components (for example, `useAccordionContextValues`) + +**What this package does NOT provide:** + +- Design props (`appearance`, `size`, `shape`, etc.) +- Style logic (Griffel, design tokens) +- Motion logic (animations, transitions) +- Default slot implementations (icons, components) + +> **Important:** Base hooks provide ARIA attributes and semantic structure, but not visual accessibility (e.g., focus indicators, sufficient contrast). Consumers are responsible for implementing these in their custom styles. + +## Prior Art + +- [RFC: Component Base State Hooks](../../../../../../docs/react-v9/contributing/rfcs/react-components/convergence/base-state-hooks.md) +- Fluent UI v9 `_unstable` hook convention used throughout `@fluentui/react-*` packages + +## Sample Code + +Building a fully custom button using base hooks: + +```tsx +import * as React from 'react'; +import { useButton, renderButton } from '@fluentui/react-headless-components'; +import type { ButtonProps, ButtonState } from '@fluentui/react-headless-components'; + +type CustomButtonProps = ButtonProps & { + variant?: 'primary' | 'secondary' | 'tertiary'; + tone?: 'neutral' | 'success' | 'warning' | 'danger'; +}; + +export const CustomButton = React.forwardRef( + ({ variant = 'primary', tone = 'neutral', ...props }, ref) => { + const state = useButton(props, ref); + + state.root.className = ['custom-btn', `custom-btn--${variant}`, `custom-btn--${tone}`, state.root.className] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = ['custom-btn__icon', state.icon.className].filter(Boolean).join(' '); + } + + return renderButton(state as ButtonState); + }, +); +``` + +## API + +### Naming Conventions + +| Artifact | Pattern | Example | +| ---------- | ------------------------ | -------------- | +| Primitive | `${ComponentName}` | `Button` | +| Hook | `use${ComponentName}` | `useButton` | +| Props type | `${ComponentName}Props` | `ButtonProps` | +| State type | `${ComponentName}State` | `ButtonState` | +| Render fn | `render${ComponentName}` | `renderButton` | + +Public exports in this package use stable names and wrap internal `_unstable` base hooks from individual component packages. + +### Type Hierarchy + +```tsx +// Package types are the headless/base component contracts +type ButtonProps = ComponentProps & { + disabled?: boolean; + disabledFocusable?: boolean; + iconPosition?: 'before' | 'after'; +}; + +type ButtonState = ComponentState & { + disabled: boolean; + disabledFocusable: boolean; + iconPosition: 'before' | 'after'; + iconOnly: boolean; +}; +``` + +### Exported Components + +Each exported component is available as an unstyled primitive component, with its low-level building blocks for advanced composition. + +#### Accordion family + +- `Accordion`, `AccordionItem`, `AccordionHeader`, `AccordionPanel` (unstyled primitives) +- `useAccordion`, `useAccordionItem`, `useAccordionHeader`, `useAccordionPanel` +- `renderAccordion`, `renderAccordionItem`, `renderAccordionHeader`, `renderAccordionPanel` +- Context hooks for advanced composition: `useAccordionContext`, `useAccordionContextValues` + +#### Button + +- `Button` (unstyled primitive) +- `useButton` +- `renderButton` + +#### Divider + +- `Divider` (unstyled primitive) +- `useDivider` +- `renderDivider` + +## Structure + +### Composition Layers + +```text +use{Component}Base_unstable (internal base state hook — logic + accessibility) + ↓ +use{Component} (public stable hook in this package) + ↓ +render{Component} (public render function in this package) + ↓ +{Component} (unstyled primitive component in this package) +``` + +This package exposes headless primitives and their building blocks. Styled components in `@fluentui/react-components` continue to compose on top of the same base logic. + +### Public API + +Every exported component exposes: + +- an unstyled primitive component (`Button`) +- a stable hook (`useButton`) +- a render function (`renderButton`) +- optional context-value hooks for compound patterns (`useAccordionContextValues`) + +### Internal + +Each component's base logic lives in its individual package (for example, `@fluentui/react-button`). This package re-exports stable wrappers and primitives on top of that base logic. + +## Migration + +This package is a new addition; there is no migration from v8 or v0. For teams currently using full Fluent UI components that want to adopt base hooks, the path is: + +1. Replace `useButton_unstable(props, ref)` with `useButton(props, ref)` from this package +2. Remove design props (`appearance`, `size`, `shape`) from your props type and use `ButtonProps` from this package +3. Apply your own class names or styles to the returned state slots before passing to the render function + +## Behaviors + +Headless hooks encapsulate interactive behavior inherited by the styled component layer: + +### Component States + +- **Disabled**: ARIA `disabled` and `aria-disabled` attributes set; keyboard events suppressed where appropriate +- **DisabledFocusable**: Element remains focusable while being semantically disabled (`aria-disabled="true"`) +- **Expanded / collapsed**: Accordion primitives manage disclosure state and relationships via ARIA + +### Interaction + +#### Keyboard + +Keyboard behavior is component-specific and follows WAI-ARIA authoring practices. Each hook applies the same keyboard handling as the corresponding styled component. + +#### Cursor + +No cursor styles are applied by base hooks. Consumers are responsible for setting appropriate cursor styles. + +#### Touch + +Touch events are handled via the same event handlers applied to root slots. + +#### Screen readers + +- ARIA roles, states, and properties are applied by the hooks + +## Accessibility + +Headless hooks and primitives provide the semantic foundation for accessibility, but consumers must ensure their custom styles maintain: + +- **Visible focus indicators** — base hooks do not apply focus ring styles +- **Sufficient color contrast** — base hooks do not apply colors or tokens +- **Appropriate visual feedback** for all interactive states (hover, active, disabled) + +### ARIA Patterns Applied + +Each component follows its corresponding WAI-ARIA authoring practice: + +| Component | ARIA pattern | +| --------- | --------------------- | +| Accordion | Accordion pattern | +| Button | Button / Link pattern | +| Divider | Separator pattern | + +> Keyboard navigation, focus management, and state announcements are delegated to the individual component packages and are identical to their styled counterparts. diff --git a/packages/react-components/react-headless-components-preview/library/eslint.config.js b/packages/react-components/react-headless-components-preview/library/eslint.config.js new file mode 100644 index 00000000000000..ec2e7cb1fc479f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [...fluentPlugin.configs['flat/react']]; diff --git a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md new file mode 100644 index 00000000000000..b20d9b2f1f00a8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md @@ -0,0 +1,153 @@ +## API Report File for "@fluentui/react-headless-components-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AccordionBaseProps } from '@fluentui/react-accordion'; +import { AccordionBaseState } from '@fluentui/react-accordion'; +import { AccordionContextValue } from '@fluentui/react-accordion'; +import { AccordionContextValues as AccordionContextValues_2 } from '@fluentui/react-accordion'; +import type { AccordionHeaderBaseProps } from '@fluentui/react-accordion'; +import { AccordionHeaderBaseState } from '@fluentui/react-accordion'; +import { AccordionHeaderContextValues } from '@fluentui/react-accordion'; +import type { AccordionHeaderSlots as AccordionHeaderSlots_2 } from '@fluentui/react-accordion'; +import { AccordionItemContextValues } from '@fluentui/react-accordion'; +import type { AccordionItemProps as AccordionItemProps_2 } from '@fluentui/react-accordion'; +import type { AccordionItemSlots as AccordionItemSlots_2 } from '@fluentui/react-accordion'; +import { AccordionItemState as AccordionItemState_2 } from '@fluentui/react-accordion'; +import type { AccordionPanelBaseProps } from '@fluentui/react-accordion'; +import type { AccordionPanelBaseState } from '@fluentui/react-accordion'; +import type { AccordionPanelSlots as AccordionPanelSlots_2 } from '@fluentui/react-accordion'; +import type { AccordionSlots as AccordionSlots_2 } from '@fluentui/react-accordion'; +import type { ButtonBaseProps } from '@fluentui/react-button'; +import { ButtonBaseState } from '@fluentui/react-button'; +import type { ButtonSlots as ButtonSlots_2 } from '@fluentui/react-button'; +import { ContextSelector } from '@fluentui/react-context-selector'; +import type { DividerBaseProps } from '@fluentui/react-divider'; +import { DividerBaseState } from '@fluentui/react-divider'; +import type { DividerSlots as DividerSlots_2 } from '@fluentui/react-divider'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { JSXElement } from '@fluentui/react-utilities'; +import type * as React_2 from 'react'; + +// @public +export const Accordion: ForwardRefComponent; + +// @public (undocumented) +export type AccordionContextValues = AccordionContextValues_2; + +// @public +export const AccordionHeader: ForwardRefComponent; + +// @public (undocumented) +export type AccordionHeaderProps = AccordionHeaderBaseProps; + +// @public (undocumented) +export type AccordionHeaderSlots = AccordionHeaderSlots_2; + +// @public (undocumented) +export type AccordionHeaderState = AccordionHeaderBaseState; + +// @public +export const AccordionItem: ForwardRefComponent; + +// @public (undocumented) +export type AccordionItemProps = AccordionItemProps_2; + +// @public (undocumented) +export type AccordionItemSlots = AccordionItemSlots_2; + +// @public (undocumented) +export type AccordionItemState = AccordionItemState_2; + +// @public +export const AccordionPanel: ForwardRefComponent; + +// @public (undocumented) +export type AccordionPanelProps = AccordionPanelBaseProps; + +// @public (undocumented) +export type AccordionPanelSlots = AccordionPanelSlots_2; + +// @public (undocumented) +export type AccordionPanelState = AccordionPanelBaseState; + +// @public (undocumented) +export type AccordionProps = AccordionBaseProps; + +// @public (undocumented) +export type AccordionSlots = AccordionSlots_2; + +// @public (undocumented) +export type AccordionState = AccordionBaseState; + +// @public +export const Button: ForwardRefComponent; + +// @public +export type ButtonProps = ButtonBaseProps; + +// @public +export type ButtonSlots = ButtonSlots_2; + +// @public +export type ButtonState = ButtonBaseState; + +// @public +export const Divider: ForwardRefComponent; + +// @public (undocumented) +export type DividerProps = DividerBaseProps; + +// @public (undocumented) +export type DividerSlots = DividerSlots_2; + +// @public (undocumented) +export type DividerState = DividerBaseState; + +// @public +export const renderAccordion: (state: AccordionBaseState, contextValues: AccordionContextValues_2) => JSXElement; + +// @public +export const renderAccordionHeader: (state: AccordionHeaderBaseState, contextValues: AccordionHeaderContextValues) => JSXElement; + +// @public +export const renderAccordionItem: (state: AccordionItemState_2, contextValues: AccordionItemContextValues) => JSXElement; + +// @public +export const renderAccordionPanel: (state: AccordionPanelState) => JSXElement; + +// @public +export const renderButton: (state: ButtonBaseState) => JSXElement; + +// @public +export const renderDivider: (state: DividerBaseState) => JSXElement; + +// @public +export const useAccordion: (props: AccordionProps, ref: React_2.Ref) => AccordionState; + +// @public +export const useAccordionContext: (selector: ContextSelector) => T; + +// @public +export const useAccordionContextValues: (state: AccordionState) => AccordionContextValues; + +// @public +export const useAccordionHeader: (props: AccordionHeaderProps, ref: React_2.Ref) => AccordionHeaderState; + +// @public +export const useAccordionItem: (props: AccordionItemProps, ref: React_2.Ref) => AccordionItemState; + +// @public +export const useAccordionPanel: (props: AccordionPanelProps, ref: React_2.Ref) => AccordionPanelState; + +// @public +export const useButton: (props: ButtonProps, ref: React_2.Ref) => ButtonState; + +// @public +export const useDivider: (props: DividerProps, ref: React_2.Ref) => DividerState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-headless-components-preview/library/jest.config.js b/packages/react-components/react-headless-components-preview/library/jest.config.js new file mode 100644 index 00000000000000..eafebaf8130b29 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/jest.config.js @@ -0,0 +1,33 @@ +// @ts-check +/* eslint-disable */ + +const { readFileSync } = require('node:fs'); +const { join } = require('node:path'); + +// Reading the SWC compilation config and remove the "exclude" +// for the test files to be compiled by SWC +const { exclude: _, ...swcJestConfig } = JSON.parse(readFileSync(join(__dirname, '.swcrc'), 'utf-8')); + +// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. +// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" +if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; +} + +// Uncomment if using global setup/teardown files being transformed via swc +// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries +// jest needs EsModule Interop to find the default exported setup/teardown functions +// swcJestConfig.module.noInterop = false; + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-headless-components-preview', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], +}; diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json new file mode 100644 index 00000000000000..5de707fdcb7339 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -0,0 +1,51 @@ +{ + "name": "@fluentui/react-headless-components-preview", + "version": "0.0.0", + "private": true, + "description": "Fluent UI React Headless Components", + "main": "lib-commonjs/index.js", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "*.md", + "dist/*.d.ts", + "lib", + "lib-commonjs" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "dependencies": { + "@fluentui/react-accordion": "^9.10.0", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-utilities": "^9.26.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./lib-commonjs/index.js", + "import": "./lib/index.js", + "require": "./lib-commonjs/index.js" + }, + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/react-headless-components-preview/library/project.json b/packages/react-components/react-headless-components-preview/library/project.json new file mode 100644 index 00000000000000..e18bfdf5c2fd80 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-headless-components-preview", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-headless-components-preview/library/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-headless-components-preview/library/src/Accordion.ts b/packages/react-components/react-headless-components-preview/library/src/Accordion.ts new file mode 100644 index 00000000000000..5b711472ec5cf5 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Accordion.ts @@ -0,0 +1,17 @@ +export { + Accordion, + renderAccordion, + useAccordion, + useAccordionContext, + useAccordionContextValues, +} from './components/Accordion'; +export type { AccordionSlots, AccordionProps, AccordionState, AccordionContextValues } from './components/Accordion'; + +export { AccordionHeader, renderAccordionHeader, useAccordionHeader } from './components/Accordion'; +export type { AccordionHeaderSlots, AccordionHeaderProps, AccordionHeaderState } from './components/Accordion'; + +export { AccordionItem, renderAccordionItem, useAccordionItem } from './components/Accordion'; +export type { AccordionItemSlots, AccordionItemProps, AccordionItemState } from './components/Accordion'; + +export { AccordionPanel, renderAccordionPanel, useAccordionPanel } from './components/Accordion'; +export type { AccordionPanelSlots, AccordionPanelProps, AccordionPanelState } from './components/Accordion'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Button.ts b/packages/react-components/react-headless-components-preview/library/src/Button.ts new file mode 100644 index 00000000000000..6b768102f1256b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Button.ts @@ -0,0 +1,2 @@ +export { Button, renderButton, useButton } from './components/Button'; +export type { ButtonSlots, ButtonProps, ButtonState } from './components/Button'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Divider.ts b/packages/react-components/react-headless-components-preview/library/src/Divider.ts new file mode 100644 index 00000000000000..91bc4060979e59 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Divider.ts @@ -0,0 +1,2 @@ +export { Divider, renderDivider, useDivider } from './components/Divider'; +export type { DividerSlots, DividerProps, DividerState } from './components/Divider'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx new file mode 100644 index 00000000000000..a97ab0ac256a39 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Accordion } from './Accordion'; +import { AccordionHeader } from './AccordionHeader/AccordionHeader'; +import { AccordionItem } from './AccordionItem/AccordionItem'; +import { AccordionPanel } from './AccordionPanel/AccordionPanel'; + +describe('Accordion', () => { + isConformant({ + Component: Accordion, + displayName: 'Accordion', + }); + + const items = [ + { + id: 'item-1', + header: 'Item #1 header', + panel: 'Item #1 Panel', + }, + { + id: 'item-2', + header: 'Item #2 header', + panel: 'Item #2 Panel', + }, + ]; + + it('renders a default state', () => { + const { container } = render( + + {items.map(item => ( + + {item.header} + {item.panel} + + ))} + , + ); + + expect(container.firstChild).toMatchInlineSnapshot(` +
+
+
+ +
+
+ Item #1 Panel +
+
+
+
+ +
+
+ Item #2 Panel +
+
+
+ `); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.tsx new file mode 100644 index 00000000000000..042eff76c6ddbf --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { AccordionProps } from './Accordion.types'; +import { useAccordion, useAccordionContextValues } from './useAccordion'; +import { renderAccordion } from './renderAccordion'; + +/** + * Represents a set of collapsible panels with headings. + */ +export const Accordion: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useAccordion(props, ref); + const contextValues = useAccordionContextValues(state); + + return renderAccordion(state, contextValues); +}); + +Accordion.displayName = 'Accordion'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts new file mode 100644 index 00000000000000..f70377ba162011 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts @@ -0,0 +1,14 @@ +import type { + AccordionSlots as AccordionBaseSlots, + AccordionBaseProps, + AccordionContextValues as AccordionBaseContextValues, + AccordionBaseState, +} from '@fluentui/react-accordion'; + +export type AccordionSlots = AccordionBaseSlots; + +export type AccordionProps = AccordionBaseProps; + +export type AccordionState = AccordionBaseState; + +export type AccordionContextValues = AccordionBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.tsx new file mode 100644 index 00000000000000..39ee6d28f7382a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { AccordionHeaderProps } from './AccordionHeader.types'; +import { useAccordionHeader, useAccordionHeaderContextValues } from './useAccordionHeader'; +import { renderAccordionHeader } from './renderAccordionHeader'; + +/** + * Represents the heading of an accordion item, containing a button that toggles the panel open or closed. + */ +export const AccordionHeader: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useAccordionHeader(props, ref); + const contextValues = useAccordionHeaderContextValues(state); + + return renderAccordionHeader(state, contextValues); +}); + +AccordionHeader.displayName = 'AccordionHeader'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts new file mode 100644 index 00000000000000..0b945771795df4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts @@ -0,0 +1,14 @@ +import type { + AccordionHeaderSlots as AccordionHeaderBaseSlots, + AccordionHeaderBaseProps, + AccordionHeaderContextValues as AccordionHeaderBaseContextValues, + AccordionHeaderBaseState, +} from '@fluentui/react-accordion'; + +export type AccordionHeaderSlots = AccordionHeaderBaseSlots; + +export type AccordionHeaderProps = AccordionHeaderBaseProps; + +export type AccordionHeaderState = AccordionHeaderBaseState; + +export type AccordionHeaderContextValues = AccordionHeaderBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/index.ts new file mode 100644 index 00000000000000..663bc7aa2f6b6c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/index.ts @@ -0,0 +1,4 @@ +export { AccordionHeader } from './AccordionHeader'; +export { renderAccordionHeader } from './renderAccordionHeader'; +export { useAccordionHeader } from './useAccordionHeader'; +export type { AccordionHeaderSlots, AccordionHeaderProps, AccordionHeaderState } from './AccordionHeader.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/renderAccordionHeader.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/renderAccordionHeader.ts new file mode 100644 index 00000000000000..e6231e1dbea5a1 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/renderAccordionHeader.ts @@ -0,0 +1,6 @@ +import { renderAccordionHeader_unstable } from '@fluentui/react-accordion'; + +/** + * Renders the final JSX of the AccordionHeader component, given the state and context values. + */ +export const renderAccordionHeader = renderAccordionHeader_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts new file mode 100644 index 00000000000000..f1964035d97bb8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts @@ -0,0 +1,32 @@ +'use client'; + +import type * as React from 'react'; +import { + useAccordionHeaderBase_unstable, + useAccordionHeaderContext_unstable, + useAccordionHeaderContextValues_unstable, +} from '@fluentui/react-accordion'; + +import type { AccordionHeaderProps, AccordionHeaderState, AccordionHeaderContextValues } from './AccordionHeader.types'; + +/** + * Returns the state for an AccordionHeader component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderAccordionHeader`. + */ +export const useAccordionHeader = (props: AccordionHeaderProps, ref: React.Ref): AccordionHeaderState => { + const state = useAccordionHeaderBase_unstable(props, ref); + + return state; +}; + +/** + * Returns the context values provided by the nearest AccordionHeader, enabling child components to read header-level state. + */ +export const useAccordionHeaderContext = useAccordionHeaderContext_unstable; + +/** + * Maps AccordionHeader state to the context values passed down to child components. + */ +export const useAccordionHeaderContextValues = useAccordionHeaderContextValues_unstable as ( + state: AccordionHeaderState, +) => AccordionHeaderContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.tsx new file mode 100644 index 00000000000000..9f86bd2f6b1800 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { AccordionItemProps } from './AccordionItem.types'; +import { useAccordionItem, useAccordionItemContextValues } from './useAccordionItem'; +import { renderAccordionItem } from './renderAccordionItem'; + +/** + * Represents a single collapsible section within an Accordion, containing a header and a panel. + */ +export const AccordionItem: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useAccordionItem(props, ref); + const contextValues = useAccordionItemContextValues(state); + + return renderAccordionItem(state, contextValues); +}); + +AccordionItem.displayName = 'AccordionItem'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts new file mode 100644 index 00000000000000..a5b064e93f5898 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts @@ -0,0 +1,14 @@ +import type { + AccordionItemSlots as AccordionItemBaseSlots, + AccordionItemProps as AccordionItemBaseProps, + AccordionItemContextValues as AccordionItemBaseContextValues, + AccordionItemState as AccordionItemBaseState, +} from '@fluentui/react-accordion'; + +export type AccordionItemSlots = AccordionItemBaseSlots; + +export type AccordionItemProps = AccordionItemBaseProps; + +export type AccordionItemState = AccordionItemBaseState; + +export type AccordionItemContextValues = AccordionItemBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/index.ts new file mode 100644 index 00000000000000..43078c8433ce0b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/index.ts @@ -0,0 +1,4 @@ +export { AccordionItem } from './AccordionItem'; +export { renderAccordionItem } from './renderAccordionItem'; +export { useAccordionItem } from './useAccordionItem'; +export type { AccordionItemSlots, AccordionItemProps, AccordionItemState } from './AccordionItem.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/renderAccordionItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/renderAccordionItem.ts new file mode 100644 index 00000000000000..cb81057c244f5c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/renderAccordionItem.ts @@ -0,0 +1,6 @@ +import { renderAccordionItem_unstable } from '@fluentui/react-accordion'; + +/** + * Renders the final JSX of the AccordionItem component, given the state and context values. + */ +export const renderAccordionItem = renderAccordionItem_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts new file mode 100644 index 00000000000000..58010b1a5b4903 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts @@ -0,0 +1,38 @@ +'use client'; + +import type * as React from 'react'; +import { + useAccordionItem_unstable, + useAccordionItemContext_unstable, + useAccordionItemContextValues_unstable, +} from '@fluentui/react-accordion'; + +import type { AccordionItemProps, AccordionItemState, AccordionItemContextValues } from './AccordionItem.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for an AccordionItem component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderAccordionItem`. + */ +export const useAccordionItem = (props: AccordionItemProps, ref: React.Ref): AccordionItemState => { + const state = useAccordionItem_unstable(props, ref); + + Object.assign(state.root, { + 'data-disabled': stringifyDataAttribute(state.disabled), + 'data-open': stringifyDataAttribute(state.open), + }); + + return state; +}; + +/** + * Returns the context values provided by the nearest AccordionItem, enabling child components to read item-level state such as whether the item is open or disabled. + */ +export const useAccordionItemContext = useAccordionItemContext_unstable; + +/** + * Maps AccordionItem state to the context values passed down to child components. + */ +export const useAccordionItemContextValues = useAccordionItemContextValues_unstable as ( + state: AccordionItemState, +) => AccordionItemContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.tsx new file mode 100644 index 00000000000000..468dddc21d5e00 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { AccordionPanelProps } from './AccordionPanel.types'; +import { useAccordionPanel } from './useAccordionPanel'; +import { renderAccordionPanel } from './renderAccordionPanel'; + +/** + * Represents the content area of an accordion item, revealed when the associated header is toggled open. + */ +export const AccordionPanel: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useAccordionPanel(props, ref); + + return renderAccordionPanel(state); +}); + +AccordionPanel.displayName = 'AccordionPanel'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts new file mode 100644 index 00000000000000..311319fc5847f7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts @@ -0,0 +1,11 @@ +import type { + AccordionPanelSlots as AccordionPanelBaseSlots, + AccordionPanelBaseProps, + AccordionPanelBaseState, +} from '@fluentui/react-accordion'; + +export type AccordionPanelSlots = AccordionPanelBaseSlots; + +export type AccordionPanelProps = AccordionPanelBaseProps; + +export type AccordionPanelState = AccordionPanelBaseState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/index.ts new file mode 100644 index 00000000000000..e508778aa42c28 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/index.ts @@ -0,0 +1,4 @@ +export { AccordionPanel } from './AccordionPanel'; +export { renderAccordionPanel } from './renderAccordionPanel'; +export { useAccordionPanel } from './useAccordionPanel'; +export type { AccordionPanelSlots, AccordionPanelProps, AccordionPanelState } from './AccordionPanel.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/renderAccordionPanel.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/renderAccordionPanel.ts new file mode 100644 index 00000000000000..595a7178740991 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/renderAccordionPanel.ts @@ -0,0 +1,12 @@ +import { renderAccordionPanel_unstable } from '@fluentui/react-accordion'; +import type { AccordionPanelState as FluentAccordionPanelState } from '@fluentui/react-accordion'; +import type { JSXElement } from '@fluentui/react-utilities'; + +import type { AccordionPanelState } from './AccordionPanel.types'; + +/** + * Renders the final JSX of the AccordionPanel component, given the state. + */ +export const renderAccordionPanel = (state: AccordionPanelState): JSXElement => { + return renderAccordionPanel_unstable(state as FluentAccordionPanelState); +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts new file mode 100644 index 00000000000000..1f664cd2a85212 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useAccordionPanelBase_unstable } from '@fluentui/react-accordion'; + +import type { AccordionPanelProps, AccordionPanelState } from './AccordionPanel.types'; + +/** + * Returns the state for an AccordionPanel component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderAccordionPanel`. + */ +export const useAccordionPanel = (props: AccordionPanelProps, ref: React.Ref): AccordionPanelState => { + const state = useAccordionPanelBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/index.ts new file mode 100644 index 00000000000000..ea48f85d51af07 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/index.ts @@ -0,0 +1,13 @@ +export { Accordion } from './Accordion'; +export { renderAccordion } from './renderAccordion'; +export { useAccordion, useAccordionContext, useAccordionContextValues } from './useAccordion'; +export type { AccordionSlots, AccordionProps, AccordionState, AccordionContextValues } from './Accordion.types'; + +export { AccordionHeader, renderAccordionHeader, useAccordionHeader } from './AccordionHeader'; +export type { AccordionHeaderSlots, AccordionHeaderProps, AccordionHeaderState } from './AccordionHeader'; + +export { AccordionItem, renderAccordionItem, useAccordionItem } from './AccordionItem'; +export type { AccordionItemSlots, AccordionItemProps, AccordionItemState } from './AccordionItem'; + +export { AccordionPanel, renderAccordionPanel, useAccordionPanel } from './AccordionPanel'; +export type { AccordionPanelSlots, AccordionPanelProps, AccordionPanelState } from './AccordionPanel'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/renderAccordion.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/renderAccordion.ts new file mode 100644 index 00000000000000..ee5add11b966bd --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/renderAccordion.ts @@ -0,0 +1,6 @@ +import { renderAccordion_unstable } from '@fluentui/react-accordion'; + +/** + * Renders the final JSX of the Accordion component, given the state and context values. + */ +export const renderAccordion = renderAccordion_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts new file mode 100644 index 00000000000000..8c734230b7f00c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts @@ -0,0 +1,38 @@ +'use client'; + +import type * as React from 'react'; +import { + useAccordionBase_unstable, + useAccordionContext_unstable, + useAccordionContextValues_unstable, +} from '@fluentui/react-accordion'; + +import type { AccordionProps, AccordionState, AccordionContextValues } from './Accordion.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for an Accordion component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderAccordion`. + */ +export const useAccordion = (props: AccordionProps, ref: React.Ref): AccordionState => { + const state = useAccordionBase_unstable(props, ref); + + Object.assign(state.root, { + 'data-collapsible': stringifyDataAttribute(state.collapsible), + 'data-multiple': stringifyDataAttribute(state.multiple), + }); + + return state; +}; + +/** + * Returns the context of the accordion, which is used to pass information about the accordion to its children. This is used when a child component needs to know about the state of the accordion, such as whether it is collapsible or allows multiple items to be expanded. + */ +export const useAccordionContext = useAccordionContext_unstable; + +/** + * Maps the state of the accordion to the values that are passed through context to its children. This is used when a child component needs to know about the state of the accordion, such as whether it is collapsible or allows multiple items to be expanded. + */ +export const useAccordionContextValues = useAccordionContextValues_unstable as ( + state: AccordionState, +) => AccordionContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx new file mode 100644 index 00000000000000..35e2562c89dd8c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Button } from './Button'; + +describe('Button', () => { + isConformant({ + Component: Button, + displayName: 'Button', + }); + + it('renders a default state', () => { + const result = render(); + const button = result.getByRole('button', { name: 'Default Button' }); + expect(button).toBeInTheDocument(); + expect(button).toMatchInlineSnapshot(` + + `); + }); + + it('renders an anchor when "as" prop is set to "a"', () => { + const result = render( + , + ); + + const link = result.getByRole('link', { name: 'Link Button' }); + + expect(link).toBeInTheDocument(); + expect(link).toMatchInlineSnapshot(` + + Link Button + + `); + }); + + it('renders with state data attributes', () => { + const result = render( + , + ); + const button = result.getByRole('button', { name: 'Disabled Button' }); + + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('data-disabled'); + expect(button).toHaveAttribute('data-disabled-focusable'); + }); + + it('renders + + + +); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Button/ButtonDescription.md b/packages/react-components/react-headless-components-preview/stories/src/Button/ButtonDescription.md new file mode 100644 index 00000000000000..a037553bd7d68d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Button/ButtonDescription.md @@ -0,0 +1,3 @@ +A button triggers a single action or event. + +Use buttons for important actions like submitting a response, committing a change, or moving to the next step. For navigating to another place, try a link instead. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Button/index.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Button/index.stories.tsx new file mode 100644 index 00000000000000..7e295efb739bdd --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Button/index.stories.tsx @@ -0,0 +1,17 @@ +import { Button } from '@fluentui/react-headless-components-preview'; + +import descriptionMd from './ButtonDescription.md'; + +export { Default } from './ButtonDefault.stories'; + +export default { + title: 'Headless Components/Button', + component: Button, + parameters: { + docs: { + description: { + component: descriptionMd, + }, + }, + }, +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerDefault.stories.tsx new file mode 100644 index 00000000000000..e093a6c3f3033e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerDefault.stories.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { Divider } from '@fluentui/react-headless-components-preview'; + +export const Default = (): React.ReactNode => ( +
+

Content above the divider

+ +

Content below the divider

+
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerDescription.md b/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerDescription.md new file mode 100644 index 00000000000000..82b5b53b65fdb8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerDescription.md @@ -0,0 +1 @@ +A divider groups sections of content to create visual rhythm and hierarchy. Use dividers along with spacing and headers to organize content in your layout. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerVertical.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerVertical.stories.tsx new file mode 100644 index 00000000000000..5c50ff33ed8b63 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Divider/DividerVertical.stories.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { Divider } from '@fluentui/react-headless-components-preview'; + +export const Vertical = (): React.ReactNode => ( +
+ Link 1 + + Link 2 +
+); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Divider/index.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Divider/index.stories.tsx new file mode 100644 index 00000000000000..21ab7e6f17c2db --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Divider/index.stories.tsx @@ -0,0 +1,18 @@ +import { Divider } from '@fluentui/react-headless-components-preview'; + +import descriptionMd from './DividerDescription.md'; + +export { Default } from './DividerDefault.stories'; +export { Vertical } from './DividerVertical.stories'; + +export default { + title: 'Headless Components/Divider', + component: Divider, + parameters: { + docs: { + description: { + component: descriptionMd, + }, + }, + }, +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/index.ts b/packages/react-components/react-headless-components-preview/stories/src/index.ts new file mode 100644 index 00000000000000..cb0ff5c3b541f6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-components/react-headless-components-preview/stories/tsconfig.json b/packages/react-components/react-headless-components-preview/stories/tsconfig.json new file mode 100644 index 00000000000000..efc50169d1df18 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json b/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json new file mode 100644 index 00000000000000..9486b224643d9f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "lib": ["ES2019", "dom"], + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index 9b9756df7a1a05..a4f0f24a8c3871 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -122,6 +122,9 @@ "@fluentui/react-field": ["packages/react-components/react-field/library/src/index.ts"], "@fluentui/react-field-stories": ["packages/react-components/react-field/stories/src/index.ts"], "@fluentui/react-focus-management": ["packages/react-focus-management/src/index.ts"], + "@fluentui/react-headless-components-preview": [ + "packages/react-components/react-headless-components-preview/library/src/index.ts" + ], "@fluentui/react-icons-compat": ["packages/react-components/react-icons-compat/library/src/index.ts"], "@fluentui/react-icons-compat-stories": ["packages/react-components/react-icons-compat/stories/src/index.ts"], "@fluentui/react-image": ["packages/react-components/react-image/library/src/index.ts"], @@ -254,7 +257,10 @@ "@fluentui/tokens": ["packages/tokens/src/index.ts"], "@fluentui/visual-regression-assert": ["tools/visual-regression-assert/src/index.ts"], "@fluentui/visual-regression-utilities": ["tools/visual-regression-utilities/src/index.ts"], - "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] + "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"], + "@fluentui/react-headless-components-preview-stories": [ + "packages/react-components/react-headless-components-preview/stories/src/index.ts" + ] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index ded6aaaa1f2528..04f2c0b87a9b22 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -77,6 +77,12 @@ "@fluentui/react-field": ["packages/react-components/react-field/library/src/index.ts"], "@fluentui/react-field-stories": ["packages/react-components/react-field/stories/src/index.ts"], "@fluentui/react-focus-management": ["packages/react-focus-management/src/index.ts"], + "@fluentui/react-headless-components-preview": [ + "packages/react-components/react-headless-components-preview/library/src/index.ts" + ], + "@fluentui/react-headless-components-preview-stories": [ + "packages/react-components/react-headless-components-preview/stories/src/index.ts" + ], "@fluentui/react-icons-compat": ["packages/react-components/react-icons-compat/library/src/index.ts"], "@fluentui/react-icons-compat-stories": ["packages/react-components/react-icons-compat/stories/src/index.ts"], "@fluentui/react-image": ["packages/react-components/react-image/library/src/index.ts"],