From bfa4a0341d282c2023ba1a3c65c01d6f64dd842a Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Tue, 31 Mar 2026 17:53:17 +0200 Subject: [PATCH 01/13] feat(react-base-components): introduce new package for creating base components --- .github/CODEOWNERS | 2 + ...-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json | 7 + .../library/.babelrc.json | 4 + .../react-base-components/library/.swcrc | 30 ++ .../react-base-components/library/LICENSE | 15 + .../react-base-components/library/README.md | 5 + .../bundle-size/ReactComponents.fixture.js | 7 + .../library/config/api-extractor.json | 5 + .../library/config/tests.js | 3 + .../library/docs/Spec.md | 63 +++ .../library/eslint.config.js | 5 + .../library/etc/react-base-components.api.md | 34 ++ .../library/jest.config.js | 34 ++ .../library/package.json | 91 ++++ .../library/project.json | 8 + .../src/components/Button/Button.test.tsx | 64 +++ .../library/src/components/Button/Button.ts | 13 + .../library/src/index.ts | 5 + .../library/src/testing/isConformant.ts | 15 + .../composeComponent/composeComponent.tsx | 187 ++++++++ .../composeComponents.test.tsx | 401 ++++++++++++++++++ .../src/utils/composeComponent/index.ts | 2 + .../library/tsconfig.json | 22 + .../library/tsconfig.lib.json | 22 + .../library/tsconfig.spec.json | 17 + .../stories/.storybook/main.js | 14 + .../stories/.storybook/preview.js | 9 + .../stories/.storybook/tsconfig.json | 10 + .../react-base-components/stories/README.md | 17 + .../stories/eslint.config.js | 10 + .../stories/package.json | 6 + .../stories/project.json | 8 + .../stories/src/.gitkeep | 0 .../stories/src/index.ts | 1 + .../stories/tsconfig.json | 22 + .../stories/tsconfig.lib.json | 10 + tsconfig.base.all.json | 6 +- tsconfig.base.json | 4 + 38 files changed, 1177 insertions(+), 1 deletion(-) create mode 100644 change/@fluentui-react-base-components-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json create mode 100644 packages/react-components/react-base-components/library/.babelrc.json create mode 100644 packages/react-components/react-base-components/library/.swcrc create mode 100644 packages/react-components/react-base-components/library/LICENSE create mode 100644 packages/react-components/react-base-components/library/README.md create mode 100644 packages/react-components/react-base-components/library/bundle-size/ReactComponents.fixture.js create mode 100644 packages/react-components/react-base-components/library/config/api-extractor.json create mode 100644 packages/react-components/react-base-components/library/config/tests.js create mode 100644 packages/react-components/react-base-components/library/docs/Spec.md create mode 100644 packages/react-components/react-base-components/library/eslint.config.js create mode 100644 packages/react-components/react-base-components/library/etc/react-base-components.api.md create mode 100644 packages/react-components/react-base-components/library/jest.config.js create mode 100644 packages/react-components/react-base-components/library/package.json create mode 100644 packages/react-components/react-base-components/library/project.json create mode 100644 packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx create mode 100644 packages/react-components/react-base-components/library/src/components/Button/Button.ts create mode 100644 packages/react-components/react-base-components/library/src/index.ts create mode 100644 packages/react-components/react-base-components/library/src/testing/isConformant.ts create mode 100644 packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponent.tsx create mode 100644 packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponents.test.tsx create mode 100644 packages/react-components/react-base-components/library/src/utils/composeComponent/index.ts create mode 100644 packages/react-components/react-base-components/library/tsconfig.json create mode 100644 packages/react-components/react-base-components/library/tsconfig.lib.json create mode 100644 packages/react-components/react-base-components/library/tsconfig.spec.json create mode 100644 packages/react-components/react-base-components/stories/.storybook/main.js create mode 100644 packages/react-components/react-base-components/stories/.storybook/preview.js create mode 100644 packages/react-components/react-base-components/stories/.storybook/tsconfig.json create mode 100644 packages/react-components/react-base-components/stories/README.md create mode 100644 packages/react-components/react-base-components/stories/eslint.config.js create mode 100644 packages/react-components/react-base-components/stories/package.json create mode 100644 packages/react-components/react-base-components/stories/project.json create mode 100644 packages/react-components/react-base-components/stories/src/.gitkeep create mode 100644 packages/react-components/react-base-components/stories/src/index.ts create mode 100644 packages/react-components/react-base-components/stories/tsconfig.json create mode 100644 packages/react-components/react-base-components/stories/tsconfig.lib.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 519558c4fef05..6d96c06cd9d5f 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-base-components/library @microsoft/cxe-prg +packages/react-components/react-base-components/stories @microsoft/cxe-prg # <%= NX-CODEOWNER-PLACEHOLDER %> # Deprecated v9 packages - exposed as part of `/unstable` api diff --git a/change/@fluentui-react-base-components-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json b/change/@fluentui-react-base-components-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json new file mode 100644 index 0000000000000..d24402bebce57 --- /dev/null +++ b/change/@fluentui-react-base-components-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: introduce new package for creating base components", + "packageName": "@fluentui/react-base-components", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-base-components/library/.babelrc.json b/packages/react-components/react-base-components/library/.babelrc.json new file mode 100644 index 0000000000000..630deaf765c49 --- /dev/null +++ b/packages/react-components/react-base-components/library/.babelrc.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../.babelrc-v9.json", + "plugins": ["annotate-pure-calls", "@babel/transform-react-pure-annotations"] +} diff --git a/packages/react-components/react-base-components/library/.swcrc b/packages/react-components/react-base-components/library/.swcrc new file mode 100644 index 0000000000000..b4ffa86dee306 --- /dev/null +++ b/packages/react-components/react-base-components/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": "es2019" + }, + "minify": false, + "sourceMaps": true +} diff --git a/packages/react-components/react-base-components/library/LICENSE b/packages/react-components/react-base-components/library/LICENSE new file mode 100644 index 0000000000000..d1d5d1e12ad3d --- /dev/null +++ b/packages/react-components/react-base-components/library/LICENSE @@ -0,0 +1,15 @@ +@fluentui/react-base-components + +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-base-components/library/README.md b/packages/react-components/react-base-components/library/README.md new file mode 100644 index 0000000000000..37d3a0eeb8a36 --- /dev/null +++ b/packages/react-components/react-base-components/library/README.md @@ -0,0 +1,5 @@ +# @fluentui/react-base-components + +**React Base Components components for [Fluent UI React](https://react.fluentui.dev/)** + +These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. diff --git a/packages/react-components/react-base-components/library/bundle-size/ReactComponents.fixture.js b/packages/react-components/react-base-components/library/bundle-size/ReactComponents.fixture.js new file mode 100644 index 0000000000000..bca62175ee2e9 --- /dev/null +++ b/packages/react-components/react-base-components/library/bundle-size/ReactComponents.fixture.js @@ -0,0 +1,7 @@ +import * as rbc from '@fluentui/react-base-components'; + +console.log(rbc); + +export default { + name: 'react-base-components: entire library', +}; diff --git a/packages/react-components/react-base-components/library/config/api-extractor.json b/packages/react-components/react-base-components/library/config/api-extractor.json new file mode 100644 index 0000000000000..8d482156d10d5 --- /dev/null +++ b/packages/react-components/react-base-components/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-base-components/library/config/tests.js b/packages/react-components/react-base-components/library/config/tests.js new file mode 100644 index 0000000000000..c6c67de97059e --- /dev/null +++ b/packages/react-components/react-base-components/library/config/tests.js @@ -0,0 +1,3 @@ +/** Jest test setup file. */ + +require('@testing-library/jest-dom'); diff --git a/packages/react-components/react-base-components/library/docs/Spec.md b/packages/react-components/react-base-components/library/docs/Spec.md new file mode 100644 index 0000000000000..94c0b1427584e --- /dev/null +++ b/packages/react-components/react-base-components/library/docs/Spec.md @@ -0,0 +1,63 @@ +# @fluentui/react-base-components Spec + +## Background + +_Description and use cases of this component_ + +## Prior Art + +_Include background research done for this component_ + +- _Link to Open UI research_ +- _Link to comparison of v7 and v0_ +- _Link to GitHub epic issue for the converged component_ + +## Sample Code + +_Provide some representative example code that uses the proposed API for the component_ + +## Variants + +_Describe visual or functional variants of this control, if applicable. For example, a slider could have a 2D variant._ + +## API + +_List the **Props** and **Slots** proposed for the component. Ideally this would just be a link to the component's `.types.ts` file_ + +## Structure + +- _**Public**_ +- _**Internal**_ +- _**DOM** - how the component will be rendered as HTML elements_ + +## Migration + +_Describe what will need to be done to upgrade from the existing implementations:_ + +- _Migration from v8_ +- _Migration from v0_ + +## Behaviors + +_Explain how the component will behave in use, including:_ + +- _Component States_ +- _Interaction_ + - _Keyboard_ + - _Cursor_ + - _Touch_ + - _Screen readers_ + +## Accessibility + +Base accessibility information is included in the design document. After the spec is filled and review, outcomes from it need to be communicated to design and incorporated in the design document. + +- Decide whether to use **native element** or folow **ARIA** and provide reasons +- Identify the **[ARIA](https://www.w3.org/TR/wai-aria-practices-1.2/) pattern** and, if the component is listed there, follow its specification as possible. +- Identify accessibility **variants**, the `role` ([ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#role_definitions)) of the component, its `slots` and `aria-*` props. +- Describe the **keyboard navigation**: Tab Oder and Arrow Key Navigation. Describe any other keyboard **shortcuts** used +- Specify texts for **state change announcements** - [ARIA live regions + ](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) (number of available items in dropdown, error messages, confirmations, ...) +- Identify UI parts that appear on **hover or focus** and specify keyboard and screen reader interaction with them +- List cases when **focus** needs to be **trapped** in sections of the UI (for dialogs and popups or for hierarchical navigation) +- List cases when **focus** needs to be **moved programatically** (if parts of the UI are appearing/disappearing or other cases) diff --git a/packages/react-components/react-base-components/library/eslint.config.js b/packages/react-components/react-base-components/library/eslint.config.js new file mode 100644 index 0000000000000..ec2e7cb1fc479 --- /dev/null +++ b/packages/react-components/react-base-components/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-base-components/library/etc/react-base-components.api.md b/packages/react-components/react-base-components/library/etc/react-base-components.api.md new file mode 100644 index 0000000000000..e48d926e571ba --- /dev/null +++ b/packages/react-components/react-base-components/library/etc/react-base-components.api.md @@ -0,0 +1,34 @@ +## API Report File for "@fluentui/react-base-components" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ButtonBaseProps as ButtonProps } from '@fluentui/react-button'; +import { ButtonSlots } from '@fluentui/react-button'; +import { ButtonBaseState as ButtonState } from '@fluentui/react-button'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities'; +import * as React_2 from 'react'; +import { useButtonBase_unstable as useButton } from '@fluentui/react-button'; + +export { ButtonProps } + +export { ButtonSlots } + +export { ButtonState } + +// @public +export function composeComponent(options: ComposeComponentOptions): ForwardRefComponent; + +// @public +export type ComposeComponentOptions = [ContextValues] extends [never] ? ComposeComponentOptionsWithoutContext : ComposeComponentOptionsWithContext; + +// @public (undocumented) +export const renderButton: (state: ButtonState) => JSXElement; + +export { useButton } + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-base-components/library/jest.config.js b/packages/react-components/react-base-components/library/jest.config.js new file mode 100644 index 0000000000000..18fbb05cb9e43 --- /dev/null +++ b/packages/react-components/react-base-components/library/jest.config.js @@ -0,0 +1,34 @@ +// @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-base-components', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-base-components/library/package.json b/packages/react-components/react-base-components/library/package.json new file mode 100644 index 0000000000000..7bcc759345a87 --- /dev/null +++ b/packages/react-components/react-base-components/library/package.json @@ -0,0 +1,91 @@ +{ + "name": "@fluentui/react-base-components", + "version": "0.0.1", + "description": "Compose custom React components using Fluent UI's base 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-avatar": "^9.10.4", + "@fluentui/react-badge": "^9.5.1", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-card": "^9.6.0", + "@fluentui/react-checkbox": "^9.5.17", + "@fluentui/react-combobox": "^9.16.18", + "@fluentui/react-dialog": "^9.17.3", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-drawer": "^9.11.6", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-image": "^9.4.0", + "@fluentui/react-infolabel": "^9.4.18", + "@fluentui/react-input": "^9.8.0", + "@fluentui/react-label": "^9.3.15", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-list": "^9.6.12", + "@fluentui/react-menu": "^9.23.1", + "@fluentui/react-overflow": "^9.7.1", + "@fluentui/react-persona": "^9.7.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-progress": "^9.4.17", + "@fluentui/react-provider": "^9.22.15", + "@fluentui/react-radio": "^9.6.0", + "@fluentui/react-select": "^9.4.16", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-skeleton": "^9.7.0", + "@fluentui/react-slider": "^9.6.0", + "@fluentui/react-spinbutton": "^9.6.0", + "@fluentui/react-spinner": "^9.8.0", + "@fluentui/react-swatch-picker": "^9.5.0", + "@fluentui/react-switch": "^9.7.0", + "@fluentui/react-table": "^9.19.13", + "@fluentui/react-tabs": "^9.11.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-tags": "^9.7.19", + "@fluentui/react-textarea": "^9.7.0", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-toast": "^9.7.16", + "@fluentui/react-toolbar": "^9.7.6", + "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-utilities": "^9.26.2", + "@fluentui/react-text": "^9.6.15", + "@fluentui/react-tree": "^9.15.15", + "@griffel/react": "^1.5.32", + "@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-base-components/library/project.json b/packages/react-components/react-base-components/library/project.json new file mode 100644 index 0000000000000..749e2a8b4d551 --- /dev/null +++ b/packages/react-components/react-base-components/library/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-base-components", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-base-components/library/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx b/packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx new file mode 100644 index 0000000000000..49b10e492654e --- /dev/null +++ b/packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { useButton, renderButton } from './Button'; +import { composeComponent } from '../../utils/composeComponent/composeComponent'; + +const Button = composeComponent({ + displayName: 'Button', + useState: useButton, + useStyles(state) { + state.root.className = [ + 'btn', + state.disabled && `btn--disabled`, + state.iconOnly && 'btn--icon', + state.disabledFocusable && `btn--disabledFocusable`, + ] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = 'btn__icon'; + } + }, + render: renderButton, +}); + +describe('Button', () => { + it('renders a button element by default', () => { + const { getByRole } = render( + , + ); + + const button = getByRole('button'); + expect(button).toMatchInlineSnapshot(` + + `); + }); + + it('renders an anchor element when href is provided', () => { + const { getByRole } = render( + , + ); + + const link = getByRole('link'); + expect(link).toMatchInlineSnapshot(` + + Visit example.com + + `); + }); +}); diff --git a/packages/react-components/react-base-components/library/src/components/Button/Button.ts b/packages/react-components/react-base-components/library/src/components/Button/Button.ts new file mode 100644 index 0000000000000..6b8c38529eb72 --- /dev/null +++ b/packages/react-components/react-base-components/library/src/components/Button/Button.ts @@ -0,0 +1,13 @@ +import { type ButtonBaseState, renderButton_unstable } from '@fluentui/react-button'; +import type { JSXElement } from '@fluentui/react-utilities'; + +export type { + ButtonSlots, + ButtonBaseState as ButtonState, + ButtonBaseProps as ButtonProps, +} from '@fluentui/react-button'; + +export { useButtonBase_unstable as useButton } from '@fluentui/react-button'; + +// TODO: Remove the cast once the renderButton_unstable is updated to use the ButtonBaseState instead of ButtonState +export const renderButton = renderButton_unstable as (state: ButtonBaseState) => JSXElement; diff --git a/packages/react-components/react-base-components/library/src/index.ts b/packages/react-components/react-base-components/library/src/index.ts new file mode 100644 index 0000000000000..1a65f67dc8818 --- /dev/null +++ b/packages/react-components/react-base-components/library/src/index.ts @@ -0,0 +1,5 @@ +export type { ButtonProps, ButtonState, ButtonSlots } from './components/Button/Button'; +export { useButton, renderButton } from './components/Button/Button'; + +export { composeComponent } from './utils/composeComponent'; +export type { ComposeComponentOptions } from './utils/composeComponent'; diff --git a/packages/react-components/react-base-components/library/src/testing/isConformant.ts b/packages/react-components/react-base-components/library/src/testing/isConformant.ts new file mode 100644 index 0000000000000..8ed2da0f92513 --- /dev/null +++ b/packages/react-components/react-base-components/library/src/testing/isConformant.ts @@ -0,0 +1,15 @@ +import { isConformant as baseIsConformant } from '@fluentui/react-conformance'; +import type { IsConformantOptions, TestObject } from '@fluentui/react-conformance'; +import griffelTests from '@fluentui/react-conformance-griffel'; + +export function isConformant( + testInfo: Omit, 'componentPath'> & { componentPath?: string }, +): void { + const defaultOptions: Partial> = { + tsConfig: { configName: 'tsconfig.spec.json' }, + componentPath: require.main?.filename.replace('.test', ''), + extraTests: griffelTests as TestObject, + }; + + baseIsConformant(defaultOptions, testInfo); +} diff --git a/packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponent.tsx b/packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponent.tsx new file mode 100644 index 0000000000000..14641fe4861fb --- /dev/null +++ b/packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponent.tsx @@ -0,0 +1,187 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent, JSXElement } from '@fluentui/react-utilities'; + +type BaseComposeComponentOptions = { + /** + * The `displayName` set on the resulting React component — visible in React DevTools + * and error messages. + */ + displayName: string; + + /** + * Hook that derives internal `State` from the component's public `Props` and forwarded `ref`. + * Called on every render. Must follow the Rules of Hooks. + * + * @param props - The props passed to the component. + * @param ref - The forwarded ref. + * @returns The resolved internal state object. + */ + useState: (props: Props, ref: React.Ref) => State; + + /** + * Optional hook that applies CSS-in-JS styles by mutating the state in place + * (e.g. setting `className` on slot objects). + * Called after `useState` on every render. Must follow the Rules of Hooks. + * When omitted, no styles are applied. + * + * @param state - The state returned by `useState`. + */ + useStyles?: (state: State) => void; +}; + +type ComposeComponentOptionsWithoutContext = BaseComposeComponentOptions< + Element, + Props, + State +> & { + /** + * Pure render function that converts `State` into JSX. + * Must NOT call hooks — all hook calls belong in `useState` or `useStyles`. + * + * @param state - The state returned by `useState`. + * @returns The rendered JSX element, or `null` to render nothing. + */ + render: (state: State) => JSXElement | null; +}; + +type ComposeComponentOptionsWithContext = BaseComposeComponentOptions< + Element, + Props, + State +> & { + /** + * Hook that derives context values from state and passes them to + * child components via React context providers set up inside `render`. + * Called after `useStyles` on every render. Must follow the Rules of Hooks. + * + * @param state - The state returned by `useState`. + * @returns An object whose values are distributed to context consumers. + */ + useContextValues: (state: State) => ContextValues; + + /** + * Pure render function that converts `State` and `ContextValues` into JSX. + * Must NOT call hooks — all hook calls belong in `useState`, `useStyles`, or `useContextValues`. + * + * @param state - The state returned by `useState`. + * @param contextValues - The values returned by `useContextValues`. + * @returns The rendered JSX element, or `null` to render nothing. + */ + render: (state: State, contextValues: ContextValues) => JSXElement | null; +}; + +/** + * Options for creating a composed component via {@link composeComponent}. + * + * @template Element + * @template Props + * @template State + * @template ContextValues + * `Element` is the underlying DOM element type (for example, `HTMLButtonElement`). + * `Props` is the public props accepted by the component. + * `State` is the internal state derived from props by `useState`. + * `ContextValues` is the shape of values passed via React context. It defaults to `never` + * (no context) when omitted. + * + * @example + * ```tsx + * type BadgeOptions = ComposeComponentOptions< + * HTMLSpanElement, + * BadgeProps, + * BadgeState + * >; + * ``` + */ +export type ComposeComponentOptions = [ContextValues] extends [never] + ? ComposeComponentOptionsWithoutContext + : ComposeComponentOptionsWithContext; + +/** + * Creates a strongly-typed, `forwardRef`-wrapped React component from a set of + * composable hooks and a render function. + * + * The component lifecycle runs in this order on every render: + * 1. `useState` — derives internal state from props and the forwarded ref. + * 2. `useContextValues` — derives context values from state *(optional)*. + * 3. `useStyles` — applies styles by mutating the state *(optional)*. + * 4. `render` — converts state and context values into JSX. + * + * @template Element + * @template Props + * @template State + * @template ContextValues + * `Element` is the underlying DOM element type the ref will point to. + * `Props` is the public prop surface of the component. + * `State` is the internal state shape produced by `useState`. + * `ContextValues` contains values passed down via React context and defaults to `never`. + * + * @param options - {@link ComposeComponentOptions} that define the component's behavior. + * @returns A `ForwardRefComponent` with `displayName` set. + * + * @example Basic usage — unstyled, no context values + * ```tsx + * import { useBadgeBase_unstable, renderBadge_unstable } from '@fluentui/react-components'; + * + * const UnstyledBadge = composeComponent({ + * displayName: 'UnstyledBadge', + * useState: useBadgeBase_unstable, + * render: renderBadge_unstable, + * }); + * ``` + * + * @example With custom styles + * ```tsx + * import { useBadgeBase_unstable, useBadgeStyles_unstable, renderBadge_unstable } from '@fluentui/react-components'; + * + * const CustomBadge = composeComponent({ + * displayName: 'CustomBadge', + * useState: useBadgeBase_unstable, + * useStyles(state) { + * state.root.className = `CustomBadge CustomBadge--${state.variant}`; + * }, + * render: renderBadge_unstable, + * }); + * ``` + * + * @example With context values passed to child slots + * ```tsx + * import { useMenuBase_unstable, useMenuStyles_unstable, useMenuContextValues_unstable, renderMenu_unstable } from '@fluentui/react-components'; + * + * const Menu = composeComponent({ + * displayName: 'Menu', + * useState: useMenuBase_unstable, + * useContextValues: useMenuContextValues_unstable, + * useStyles: useMenuStyles_unstable, + * render: renderMenu_unstable, + * }); + * ``` + */ +export function composeComponent( + options: ComposeComponentOptions, +): ForwardRefComponent { + const { + displayName, + useState, + // eslint-disable-next-line @typescript-eslint/no-empty-function + useStyles = () => {}, + render, + } = options; + + const useContextValues: (state: State) => ContextValues = + 'useContextValues' in options ? options.useContextValues! : () => ({} as ContextValues); + + const Component = React.forwardRef((props, ref) => { + const state = useState(props as Props, ref as React.Ref); + const contextValues = useContextValues(state); + + useStyles(state); + + return render(state, contextValues); + }); + + Component.displayName = displayName; + + return Component as ForwardRefComponent; +} diff --git a/packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponents.test.tsx b/packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponents.test.tsx new file mode 100644 index 0000000000000..191554c5a6b6b --- /dev/null +++ b/packages/react-components/react-base-components/library/src/utils/composeComponent/composeComponents.test.tsx @@ -0,0 +1,401 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { composeComponent } from './composeComponent'; +import { assertSlots, slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, RefAttributes, Slot } from '@fluentui/react-utilities'; + +type TestForwardRefComponent = React.ForwardRefExoticComponent< + React.PropsWithoutRef & RefAttributes +>; + +describe('composeComponent', () => { + it('sets displayName on the returned component', () => { + const Comp = composeComponent({ + displayName: 'TestComp', + useState: () => ({}), + render: () => null, + }); + + expect(Comp.displayName).toBe('TestComp'); + }); + + it('calls useState with props and the forwarded ref', () => { + const ref = React.createRef(); + const mockUseState = jest.fn((_props: { id?: string }, _ref: React.Ref) => ({})); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: mockUseState, + render: () => null, + }); + + // ForwardRefComponent<{}> infers ref type as never via InferredElementRefType, + // so we bypass the JSX type-check with createElement + cast. + render(React.createElement(Comp as TestForwardRefComponent<{ id?: string }, HTMLSpanElement>, { id: 'test', ref })); + + expect(mockUseState).toHaveBeenCalledTimes(1); + expect(mockUseState).toHaveBeenCalledWith({ id: 'test' }, ref); + }); + + it('passes state returned by useState to render', () => { + const state = { tag: 'sentinel' }; + const mockRender = jest.fn(() => null); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => state, + render: mockRender, + }); + + render(); + + expect(mockRender).toHaveBeenCalledWith(state, expect.anything()); + }); + + it('forwards ref to the root DOM element', () => { + const ref = React.createRef(); + + const Comp = composeComponent }>({ + displayName: 'Comp', + useState: (_props, _ref) => ({ elRef: _ref }), + render: state => React.createElement('span', { ref: state.elRef }), + }); + + render(React.createElement(Comp as TestForwardRefComponent<{}, HTMLSpanElement>, { ref })); + + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + }); + + // ------------------------------------------------------------------------- + // useStyles + // ------------------------------------------------------------------------- + + describe('useStyles', () => { + it('calls useStyles with state after useState', () => { + const state = { tag: 'sentinel' }; + const mockUseStyles = jest.fn(); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => state, + useStyles: mockUseStyles, + render: () => null, + }); + + render(); + + expect(mockUseStyles).toHaveBeenCalledTimes(1); + expect(mockUseStyles).toHaveBeenCalledWith(state); + }); + + it('calls useStyles again on every re-render with the latest state', () => { + const mockUseStyles = jest.fn(); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: props => ({ value: props.value }), + useStyles: mockUseStyles, + render: () => null, + }); + + const { rerender } = render(); + + expect(mockUseStyles).toHaveBeenCalledTimes(1); + expect(mockUseStyles).toHaveBeenLastCalledWith({ value: 'first' }); + + rerender(); + + expect(mockUseStyles).toHaveBeenCalledTimes(2); + expect(mockUseStyles).toHaveBeenLastCalledWith({ value: 'second' }); + }); + + it('state mutations from useStyles are visible in the render output', () => { + type State = { className: string }; + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({ className: '' }), + useStyles: state => { + state.className = 'applied'; + }, + render: state => React.createElement('span', { 'data-testid': 'el', className: state.className }), + }); + + const { getByTestId } = render(); + + expect((getByTestId('el') as HTMLElement).getAttribute('class')).toBe('applied'); + }); + + it('does not crash when useStyles is omitted', () => { + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({}), + render: () => React.createElement('span', { 'data-testid': 'el' }), + }); + + const { getByTestId } = render(); + + expect(getByTestId('el')).toBeTruthy(); + }); + }); + + // ------------------------------------------------------------------------- + // useContextValues + // ------------------------------------------------------------------------- + + describe('useContextValues', () => { + // Shared fixture: a minimal Menu that exposes `open` via React context. + const Ctx = React.createContext<{ open: boolean } | undefined>(undefined); + + type MenuProps = { open: boolean; children?: React.ReactNode }; + type MenuState = { open: boolean; children?: React.ReactNode }; + + const Menu = composeComponent({ + displayName: 'Menu', + useState: props => ({ open: props.open, children: props.children }), + useContextValues: state => ({ menu: { open: state.open } }), + render: (state, contextValues) => + React.createElement(Ctx.Provider, { value: contextValues!.menu }, state.children), + }); + + // React.createElement avoids native JSX in this file, which uses @fluentui/react-jsx-runtime + // that does not declare JSX.IntrinsicElements. + const Consumer: React.FC = () => { + const context = React.useContext(Ctx); + + if (!context) { + throw new Error('Menu context is missing'); + } + + const { open } = context; + return React.createElement('span', { 'data-testid': 'consumer' }, open ? 'open' : 'closed'); + }; + + it('passes context values returned by useContextValues to render', () => { + const contextValues = { flag: true }; + const mockRender = jest.fn(() => null); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({}), + useContextValues: () => contextValues, + render: mockRender, + }); + + render(); + + expect(mockRender).toHaveBeenCalledWith({}, contextValues); + }); + + it('makes context values derived from state available to child consumers', () => { + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('consumer').textContent).toBe('open'); + }); + + it('re-derives context values when props change', () => { + const { getByTestId, rerender } = render( + + + , + ); + + expect(getByTestId('consumer').textContent).toBe('closed'); + + rerender( + + + , + ); + + expect(getByTestId('consumer').textContent).toBe('open'); + }); + + it('passes an empty object to render when useContextValues is omitted', () => { + const mockRender = jest.fn(() => null); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({}), + render: mockRender, + }); + + render(); + + expect(mockRender).toHaveBeenCalledWith({}, {}); + }); + }); + + // ------------------------------------------------------------------------- + // Generic type parameters + // ------------------------------------------------------------------------- + + describe('generic type parameters', () => { + it('accepts explicit Element, Props, and State type parameters', () => { + type Props = { label: string }; + type State = { text: string }; + + // Exercises the internal `props as Props` and `ref as React.Ref` casts. + const Comp = composeComponent({ + displayName: 'TypedComp', + useState: props => ({ text: props.label }), + render: state => React.createElement('span', { 'data-testid': 'el' }, state.text), + }); + + const { getByTestId } = render(); + + expect(getByTestId('el').textContent).toBe('hello'); + }); + + it('sets displayName when generic type parameters are explicit', () => { + const Comp = composeComponent({ + displayName: 'TypedComp', + useState: props => ({ text: props.label }), + render: () => null, + }); + + expect(Comp.displayName).toBe('TypedComp'); + }); + + it('forwards the ref when generic element type is explicit', () => { + const ref = React.createRef(); + + const Comp = composeComponent }>({ + displayName: 'TypedComp', + useState: (_props, _ref) => ({ elRef: _ref }), + render: state => React.createElement('span', { ref: state.elRef }), + }); + + render(React.createElement(Comp as TestForwardRefComponent<{}, HTMLSpanElement>, { ref })); + + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + }); + + it('supports generic component props by casting the return value', () => { + type ListProps = { items: T[]; renderItem: (item: T, index: number) => React.ReactElement }; + type ListState = Pick, 'items' | 'renderItem'>; + type GenericListComponent = ( + props: ListProps & RefAttributes, + ) => React.ReactElement | null; + + const List = composeComponent, ListState>({ + displayName: 'List', + useState: ({ items = [], renderItem }) => ({ items, renderItem }), + render: state => React.createElement('ul', { 'data-testid': 'list' }, state.items.map(state.renderItem)), + }) as GenericListComponent; + + const { getByTestId } = render( + + items={['a', 'b', 'c']} + renderItem={(item: string, i: number) => React.createElement('li', { key: i }, item)} + />, + ); + + expect(getByTestId('list').children).toHaveLength(3); + }); + }); + + // ------------------------------------------------------------------------- + // Integration: styled and unstyled component variants + // + // Demonstrates the primary use-case of composeComponent: sharing the same + // useState + render between a styled variant (with useStyles) and an unstyled + // variant (without useStyles), mirroring the real Fluent UI component pattern. + // ------------------------------------------------------------------------- + + describe('styled and unstyled component variants', () => { + type BadgeSlots = { root: NonNullable> }; + type BadgeBaseProps = ComponentProps; + type BadgeProps = BadgeBaseProps & { variant?: 'primary' | 'secondary' }; + type BadgeBaseState = ComponentState; + type BadgeState = BadgeBaseState & Required>; + + /** + * Base state hook, provides state slots and ARIA attributes, but no styles. + */ + const useBadgeBase = (props: BadgeBaseProps, ref: React.Ref): BadgeBaseState => { + return { + components: { root: 'span' }, + root: slot.always>(props, { + defaultProps: { ref }, + elementType: 'span', + }), + }; + }; + + /** + * State hook for the styled variant, adds style-related props to the base state. + */ + const useBadge = (props: BadgeProps, ref: React.Ref): BadgeState => { + const { variant = 'primary', ...rest } = props; + return { + ...useBadgeBase(rest, ref), + variant, + }; + }; + + /** + * Style hook that applies a className based on the variant + */ + const useBadgeStyles = (state: BadgeState) => { + state.root.className = `Fui-Badge Fui-Badge--${state.variant}`; + }; + + /** + * Render function shared by both styled and unstyled variants, asserts the presence of slots and renders the root slot as a span. + */ + const renderBadge = (state: BadgeBaseState) => { + assertSlots(state); + return ; + }; + + /** + * Styled component variant that uses the full state and styles. + */ + const Badge = composeComponent({ + displayName: 'Badge', + useState: useBadge, + useStyles: useBadgeStyles, + render: renderBadge, + }); + + /** + * Unstyled component variant that uses the base state without styles. + */ + const BadgeUnstyled = composeComponent({ + displayName: 'BadgeUnstyled', + useState: useBadgeBase, + render: renderBadge, + }); + + it('styled variant applies className via useStyles', () => { + const { container } = render(Label); + const el = container.firstChild as HTMLElement; + + expect(el.tagName).toBe('SPAN'); + expect(el.getAttribute('class')).toBe('Fui-Badge Fui-Badge--secondary'); + }); + + it('styled variant uses the default prop value when variant is omitted', () => { + const { container } = render(Label); + + expect((container.firstChild as HTMLElement).getAttribute('class')).toBe('Fui-Badge Fui-Badge--primary'); + }); + + it('unstyled variant renders the same structure without any className', () => { + const { container } = render(Label); + const el = container.firstChild as HTMLElement; + + expect(el.tagName).toBe('SPAN'); + expect(el.getAttribute('class')).toBeNull(); + }); + }); +}); diff --git a/packages/react-components/react-base-components/library/src/utils/composeComponent/index.ts b/packages/react-components/react-base-components/library/src/utils/composeComponent/index.ts new file mode 100644 index 0000000000000..0dfbaf5838586 --- /dev/null +++ b/packages/react-components/react-base-components/library/src/utils/composeComponent/index.ts @@ -0,0 +1,2 @@ +export { composeComponent } from './composeComponent'; +export type { ComposeComponentOptions } from './composeComponent'; diff --git a/packages/react-components/react-base-components/library/tsconfig.json b/packages/react-components/react-base-components/library/tsconfig.json new file mode 100644 index 0000000000000..32bdbdf1ac26f --- /dev/null +++ b/packages/react-components/react-base-components/library/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": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/react-components/react-base-components/library/tsconfig.lib.json b/packages/react-components/react-base-components/library/tsconfig.lib.json new file mode 100644 index 0000000000000..53066fdd11fff --- /dev/null +++ b/packages/react-components/react-base-components/library/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "dom"], + "declaration": true, + "declarationDir": "../../../../dist/out-tsc/types", + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "exclude": [ + "./src/testing/**", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.ts", + "**/*.stories.tsx" + ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/react-base-components/library/tsconfig.spec.json b/packages/react-components/react-base-components/library/tsconfig.spec.json new file mode 100644 index 0000000000000..0e881941843de --- /dev/null +++ b/packages/react-components/react-base-components/library/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/packages/react-components/react-base-components/stories/.storybook/main.js b/packages/react-components/react-base-components/stories/.storybook/main.js new file mode 100644 index 0000000000000..67905c6bfe15f --- /dev/null +++ b/packages/react-components/react-base-components/stories/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../src/**/*.mdx', '../src/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/react-base-components/stories/.storybook/preview.js b/packages/react-components/react-base-components/stories/.storybook/preview.js new file mode 100644 index 0000000000000..98274ed0b8095 --- /dev/null +++ b/packages/react-components/react-base-components/stories/.storybook/preview.js @@ -0,0 +1,9 @@ +import * as rootPreview from '../../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; + +export const tags = ['autodocs']; diff --git a/packages/react-components/react-base-components/stories/.storybook/tsconfig.json b/packages/react-components/react-base-components/stories/.storybook/tsconfig.json new file mode 100644 index 0000000000000..4cdd1ce9d006f --- /dev/null +++ b/packages/react-components/react-base-components/stories/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment"] + }, + "include": ["*.js"] +} diff --git a/packages/react-components/react-base-components/stories/README.md b/packages/react-components/react-base-components/stories/README.md new file mode 100644 index 0000000000000..f432b55248779 --- /dev/null +++ b/packages/react-components/react-base-components/stories/README.md @@ -0,0 +1,17 @@ +# @fluentui/react-base-components-stories + +Storybook stories for packages/react-components/react-base-components + +## Usage + +To include within storybook specify stories globs: + +\`\`\`js +module.exports = { +stories: ['../packages/react-components/react-base-components/stories/src/**/*.mdx', '../packages/react-components/react-base-components/stories/src/**/index.stories.@(ts|tsx)'], +} +\`\`\` + +## API + +no public API available diff --git a/packages/react-components/react-base-components/stories/eslint.config.js b/packages/react-components/react-base-components/stories/eslint.config.js new file mode 100644 index 0000000000000..f8362c3e41303 --- /dev/null +++ b/packages/react-components/react-base-components/stories/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [ + ...fluentPlugin.configs['flat/react'], + { + rules: {}, + }, +]; diff --git a/packages/react-components/react-base-components/stories/package.json b/packages/react-components/react-base-components/stories/package.json new file mode 100644 index 0000000000000..d1fe53f3f5d39 --- /dev/null +++ b/packages/react-components/react-base-components/stories/package.json @@ -0,0 +1,6 @@ +{ + "name": "@fluentui/react-base-components-stories", + "version": "0.0.0", + "private": true, + "devDependencies": {} +} diff --git a/packages/react-components/react-base-components/stories/project.json b/packages/react-components/react-base-components/stories/project.json new file mode 100644 index 0000000000000..5a196cf9b1d17 --- /dev/null +++ b/packages/react-components/react-base-components/stories/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-base-components-stories", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-base-components/stories/src", + "tags": ["vNext", "platform:web", "type:stories"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-base-components/stories/src/.gitkeep b/packages/react-components/react-base-components/stories/src/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/react-components/react-base-components/stories/src/index.ts b/packages/react-components/react-base-components/stories/src/index.ts new file mode 100644 index 0000000000000..cb0ff5c3b541f --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-components/react-base-components/stories/tsconfig.json b/packages/react-components/react-base-components/stories/tsconfig.json new file mode 100644 index 0000000000000..efc50169d1df1 --- /dev/null +++ b/packages/react-components/react-base-components/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-base-components/stories/tsconfig.lib.json b/packages/react-components/react-base-components/stories/tsconfig.lib.json new file mode 100644 index 0000000000000..9486b224643d9 --- /dev/null +++ b/packages/react-components/react-base-components/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 9b9756df7a1a0..483238c91e27e 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -254,7 +254,11 @@ "@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-base-components-stories": [ + "packages/react-components/react-base-components/stories/src/index.ts" + ], + "@fluentui/react-base-components": ["packages/react-components/react-base-components/library/src/index.ts"] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index ded6aaaa1f252..dc9f0abb642a8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -38,6 +38,10 @@ "@fluentui/react-avatar-stories": ["packages/react-components/react-avatar/stories/src/index.ts"], "@fluentui/react-badge": ["packages/react-components/react-badge/library/src/index.ts"], "@fluentui/react-badge-stories": ["packages/react-components/react-badge/stories/src/index.ts"], + "@fluentui/react-base-components": ["packages/react-components/react-base-components/library/src/index.ts"], + "@fluentui/react-base-components-stories": [ + "packages/react-components/react-base-components/stories/src/index.ts" + ], "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/library/src/index.ts"], "@fluentui/react-breadcrumb-stories": ["packages/react-components/react-breadcrumb/stories/src/index.ts"], "@fluentui/react-button": ["packages/react-components/react-button/library/src/index.ts"], From d8a971ed449a668cf29145fca558d8ae46714d18 Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Wed, 1 Apr 2026 15:04:15 +0200 Subject: [PATCH 02/13] base components stories and update types for components --- ...-59f990c7-d36b-4b22-b659-4152e463b83e.json | 7 + ...-6561155a-5cdf-4b9a-ae91-69fa32f9650d.json | 7 + ...-8eae8004-fa9d-4754-bc51-fd06d5eb2c06.json | 7 + ...-13fe61b7-e8e0-479f-87f7-566a49be4b26.json | 7 + .../library/etc/react-base-components.api.md | 698 +++++++++++++++++- .../library/package.json | 1 - .../src/components/Button/Button.test.tsx | 64 -- .../library/src/components/Button/Button.ts | 13 - .../library/src/index.ts | 440 ++++++++++- .../library/src/testing/isConformant.ts | 15 - .../stories/src/.gitkeep | 0 .../Accordion/ExampleAccordion.stories.tsx | 133 ++++ .../stories/src/Accordion/index.stories.tsx | 20 + .../src/Badge/ExampleBadge.stories.tsx | 69 ++ .../Badge/ExamplePresenceBadge.stories.tsx | 43 ++ .../stories/src/Badge/index.stories.tsx | 21 + .../Breadcrumb/ExampleBreadcrumb.stories.tsx | 101 +++ .../stories/src/Breadcrumb/index.stories.tsx | 20 + .../src/Button/ExampleButton.stories.tsx | 97 +++ .../stories/src/Button/index.stories.tsx | 20 + .../stories/src/Card/ExampleCard.stories.tsx | 106 +++ .../stories/src/Card/index.stories.tsx | 20 + .../src/Divider/ExampleDivider.stories.tsx | 50 ++ .../stories/src/Divider/index.stories.tsx | 20 + .../src/Image/ExampleImage.stories.tsx | 50 ++ .../stories/src/Image/index.stories.tsx | 20 + .../src/Input/ExampleInput.stories.tsx | 60 ++ .../stories/src/Input/index.stories.tsx | 20 + .../stories/src/Link/ExampleLink.stories.tsx | 50 ++ .../stories/src/Link/index.stories.tsx | 20 + .../src/Persona/ExamplePersona.stories.tsx | 82 ++ .../stories/src/Persona/index.stories.tsx | 20 + .../src/Radio/ExampleRadio.stories.tsx | 75 ++ .../stories/src/Radio/index.stories.tsx | 20 + .../src/Rating/ExampleRating.stories.tsx | 65 ++ .../stories/src/Rating/index.stories.tsx | 20 + .../SearchBox/ExampleSearchBox.stories.tsx | 55 ++ .../stories/src/SearchBox/index.stories.tsx | 20 + .../src/Skeleton/ExampleSkeleton.stories.tsx | 77 ++ .../stories/src/Skeleton/index.stories.tsx | 20 + .../src/Slider/ExampleSlider.stories.tsx | 76 ++ .../stories/src/Slider/index.stories.tsx | 20 + .../SpinButton/ExampleSpinButton.stories.tsx | 65 ++ .../stories/src/SpinButton/index.stories.tsx | 20 + .../src/Spinner/ExampleSpinner.stories.tsx | 65 ++ .../stories/src/Spinner/index.stories.tsx | 20 + .../src/Switch/ExampleSwitch.stories.tsx | 80 ++ .../stories/src/Switch/index.stories.tsx | 20 + .../src/Textarea/ExampleTextarea.stories.tsx | 48 ++ .../stories/src/Textarea/index.stories.tsx | 20 + .../ExampleToggleButton.stories.tsx | 53 ++ .../src/ToggleButton/index.stories.tsx | 20 + .../library/etc/react-breadcrumb.api.md | 8 +- .../Breadcrumb/renderBreadcrumb.tsx | 4 +- .../renderBreadcrumbButton.tsx | 4 +- .../renderBreadcrumbDivider.tsx | 4 +- .../BreadcrumbItem/renderBreadcrumbItem.tsx | 4 +- .../library/etc/react-button.api.md | 2 +- .../src/components/Button/renderButton.tsx | 4 +- .../library/etc/react-skeleton.api.md | 10 +- .../components/Skeleton/renderSkeleton.tsx | 4 +- .../react-skeleton/library/src/index.ts | 10 +- .../library/etc/react-textarea.api.md | 2 +- .../components/Textarea/renderTextarea.tsx | 4 +- 64 files changed, 3100 insertions(+), 120 deletions(-) create mode 100644 change/@fluentui-react-breadcrumb-59f990c7-d36b-4b22-b659-4152e463b83e.json create mode 100644 change/@fluentui-react-button-6561155a-5cdf-4b9a-ae91-69fa32f9650d.json create mode 100644 change/@fluentui-react-skeleton-8eae8004-fa9d-4754-bc51-fd06d5eb2c06.json create mode 100644 change/@fluentui-react-textarea-13fe61b7-e8e0-479f-87f7-566a49be4b26.json delete mode 100644 packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx delete mode 100644 packages/react-components/react-base-components/library/src/components/Button/Button.ts delete mode 100644 packages/react-components/react-base-components/library/src/testing/isConformant.ts delete mode 100644 packages/react-components/react-base-components/stories/src/.gitkeep create mode 100644 packages/react-components/react-base-components/stories/src/Accordion/ExampleAccordion.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Accordion/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Badge/ExampleBadge.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Badge/ExamplePresenceBadge.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Badge/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Breadcrumb/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Button/ExampleButton.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Button/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Card/ExampleCard.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Card/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Divider/ExampleDivider.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Divider/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Image/ExampleImage.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Image/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Input/ExampleInput.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Input/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Link/ExampleLink.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Link/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Persona/ExamplePersona.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Persona/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Radio/ExampleRadio.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Radio/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Rating/ExampleRating.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Rating/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/SearchBox/ExampleSearchBox.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/SearchBox/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Skeleton/ExampleSkeleton.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Skeleton/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Slider/ExampleSlider.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Slider/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/SpinButton/ExampleSpinButton.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/SpinButton/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Spinner/ExampleSpinner.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Spinner/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Switch/ExampleSwitch.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Switch/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Textarea/ExampleTextarea.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/Textarea/index.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/ToggleButton/ExampleToggleButton.stories.tsx create mode 100644 packages/react-components/react-base-components/stories/src/ToggleButton/index.stories.tsx diff --git a/change/@fluentui-react-breadcrumb-59f990c7-d36b-4b22-b659-4152e463b83e.json b/change/@fluentui-react-breadcrumb-59f990c7-d36b-4b22-b659-4152e463b83e.json new file mode 100644 index 0000000000000..76ba873d105bc --- /dev/null +++ b/change/@fluentui-react-breadcrumb-59f990c7-d36b-4b22-b659-4152e463b83e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update types for render function", + "packageName": "@fluentui/react-breadcrumb", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-button-6561155a-5cdf-4b9a-ae91-69fa32f9650d.json b/change/@fluentui-react-button-6561155a-5cdf-4b9a-ae91-69fa32f9650d.json new file mode 100644 index 0000000000000..358cf1fba67b2 --- /dev/null +++ b/change/@fluentui-react-button-6561155a-5cdf-4b9a-ae91-69fa32f9650d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update types for render function", + "packageName": "@fluentui/react-button", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-skeleton-8eae8004-fa9d-4754-bc51-fd06d5eb2c06.json b/change/@fluentui-react-skeleton-8eae8004-fa9d-4754-bc51-fd06d5eb2c06.json new file mode 100644 index 0000000000000..bc3c232bc37cb --- /dev/null +++ b/change/@fluentui-react-skeleton-8eae8004-fa9d-4754-bc51-fd06d5eb2c06.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update types for render function", + "packageName": "@fluentui/react-skeleton", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-textarea-13fe61b7-e8e0-479f-87f7-566a49be4b26.json b/change/@fluentui-react-textarea-13fe61b7-e8e0-479f-87f7-566a49be4b26.json new file mode 100644 index 0000000000000..744b9a06fbfa8 --- /dev/null +++ b/change/@fluentui-react-textarea-13fe61b7-e8e0-479f-87f7-566a49be4b26.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update types for render function", + "packageName": "@fluentui/react-textarea", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-base-components/library/etc/react-base-components.api.md b/packages/react-components/react-base-components/library/etc/react-base-components.api.md index e48d926e571ba..ba6135faa574b 100644 --- a/packages/react-components/react-base-components/library/etc/react-base-components.api.md +++ b/packages/react-components/react-base-components/library/etc/react-base-components.api.md @@ -4,13 +4,307 @@ ```ts +import { AccordionContextValue } from '@fluentui/react-accordion'; +import { AccordionContextValues } from '@fluentui/react-accordion'; +import { AccordionHeaderContextValues } from '@fluentui/react-accordion'; +import { AccordionHeaderBaseProps as AccordionHeaderProps } from '@fluentui/react-accordion'; +import { AccordionHeaderSlots } from '@fluentui/react-accordion'; +import { AccordionHeaderBaseState as AccordionHeaderState } from '@fluentui/react-accordion'; +import { AccordionItemContextValue } from '@fluentui/react-accordion'; +import { AccordionItemContextValues } from '@fluentui/react-accordion'; +import { AccordionItemProps } from '@fluentui/react-accordion'; +import { AccordionItemSlots } from '@fluentui/react-accordion'; +import { AccordionItemState } from '@fluentui/react-accordion'; +import { AccordionPanelBaseProps as AccordionPanelProps } from '@fluentui/react-accordion'; +import { AccordionPanelSlots } from '@fluentui/react-accordion'; +import { AccordionPanelBaseState as AccordionPanelState } from '@fluentui/react-accordion'; +import { AccordionBaseProps as AccordionProps } from '@fluentui/react-accordion'; +import { AccordionSlots } from '@fluentui/react-accordion'; +import { AccordionBaseState as AccordionState } from '@fluentui/react-accordion'; +import { AnnounceContextValue } from '@fluentui/react-shared-contexts'; +import { AnnounceProvider } from '@fluentui/react-shared-contexts'; +import { assertSlots } from '@fluentui/react-utilities'; +import { BadgeBaseProps as BadgeProps } from '@fluentui/react-badge'; +import { BadgeSlots } from '@fluentui/react-badge'; +import { BadgeBaseState as BadgeState } from '@fluentui/react-badge'; +import { BreadcrumbButtonBaseProps as BreadcrumbButtonProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbButtonSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbButtonBaseState as BreadcrumbButtonState } from '@fluentui/react-breadcrumb'; +import { BreadcrumbContextValues } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerBaseProps as BreadcrumbDividerProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerBaseState as BreadcrumbDividerState } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemBaseProps as BreadcrumbItemProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemBaseState as BreadcrumbItemState } from '@fluentui/react-breadcrumb'; +import { BreadcrumbBaseProps as BreadcrumbProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbBaseState as BreadcrumbState } from '@fluentui/react-breadcrumb'; import { ButtonBaseProps as ButtonProps } from '@fluentui/react-button'; import { ButtonSlots } from '@fluentui/react-button'; import { ButtonBaseState as ButtonState } from '@fluentui/react-button'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import type { JSXElement } from '@fluentui/react-utilities'; +import { CardContextValue } from '@fluentui/react-card'; +import { CardFooterBaseProps as CardFooterProps } from '@fluentui/react-card'; +import { CardFooterSlots } from '@fluentui/react-card'; +import { CardFooterBaseState as CardFooterState } from '@fluentui/react-card'; +import { CardHeaderBaseProps as CardHeaderProps } from '@fluentui/react-card'; +import { CardHeaderSlots } from '@fluentui/react-card'; +import { CardHeaderBaseState as CardHeaderState } from '@fluentui/react-card'; +import { CardPreviewBaseProps as CardPreviewProps } from '@fluentui/react-card'; +import { CardPreviewSlots } from '@fluentui/react-card'; +import { CardPreviewBaseState as CardPreviewState } from '@fluentui/react-card'; +import { CardBaseProps as CardProps } from '@fluentui/react-card'; +import { CardSlots } from '@fluentui/react-card'; +import { CardBaseState as CardState } from '@fluentui/react-card'; +import { ComponentProps } from '@fluentui/react-utilities'; +import { ComponentState } from '@fluentui/react-utilities'; +import { CounterBadgeBaseProps as CounterBadgeProps } from '@fluentui/react-badge'; +import { CounterBadgeBaseState as CounterBadgeState } from '@fluentui/react-badge'; +import { DividerBaseProps as DividerProps } from '@fluentui/react-divider'; +import { DividerSlots } from '@fluentui/react-divider'; +import { DividerBaseState as DividerState } from '@fluentui/react-divider'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getPartitionedNativeProps } from '@fluentui/react-utilities'; +import { getSlotClassNameProp_unstable } from '@fluentui/react-utilities'; +import { getSlots } from '@fluentui/react-utilities'; +import { IdPrefixProvider } from '@fluentui/react-utilities'; +import { ImageBaseProps as ImageProps } from '@fluentui/react-image'; +import { ImageSlots } from '@fluentui/react-image'; +import { ImageBaseState as ImageState } from '@fluentui/react-image'; +import { InputBaseProps as InputProps } from '@fluentui/react-input'; +import { InputSlots } from '@fluentui/react-input'; +import { InputBaseState as InputState } from '@fluentui/react-input'; +import { isHTMLElement } from '@fluentui/react-utilities'; +import { JSXElement } from '@fluentui/react-utilities'; +import { JSXIntrinsicElement } from '@fluentui/react-utilities'; +import { JSXIntrinsicElementKeys } from '@fluentui/react-utilities'; +import { LinkBaseProps as LinkProps } from '@fluentui/react-link'; +import { LinkSlots } from '@fluentui/react-link'; +import { LinkBaseState as LinkState } from '@fluentui/react-link'; +import { mergeCallbacks } from '@fluentui/react-utilities'; +import { OnSelectionChangeCallback } from '@fluentui/react-utilities'; +import { OnSelectionChangeData } from '@fluentui/react-utilities'; +import { PersonaBaseProps as PersonaProps } from '@fluentui/react-persona'; +import { PersonaSlots } from '@fluentui/react-persona'; +import { PersonaBaseState as PersonaState } from '@fluentui/react-persona'; +import { PortalMountNodeProvider } from '@fluentui/react-shared-contexts'; +import { PresenceBadgeBaseProps as PresenceBadgeProps } from '@fluentui/react-badge'; +import { PresenceBadgeBaseState as PresenceBadgeState } from '@fluentui/react-badge'; +import { RadioGroupContextValue } from '@fluentui/react-radio'; +import { RadioGroupContextValues } from '@fluentui/react-radio'; +import { RadioGroupBaseProps as RadioGroupProps } from '@fluentui/react-radio'; +import { RadioGroupSlots } from '@fluentui/react-radio'; +import { RadioGroupBaseState as RadioGroupState } from '@fluentui/react-radio'; +import { RadioBaseProps as RadioProps } from '@fluentui/react-radio'; +import { RadioSlots } from '@fluentui/react-radio'; +import { RadioBaseState as RadioState } from '@fluentui/react-radio'; +import { RatingContextValues } from '@fluentui/react-rating'; +import { RatingDisplayContextValues } from '@fluentui/react-rating'; +import { RatingDisplayBaseProps as RatingDisplayProps } from '@fluentui/react-rating'; +import { RatingDisplayBaseState as RatingDisplayState } from '@fluentui/react-rating'; +import { RatingItemBaseProps as RatingItemProps } from '@fluentui/react-rating'; +import { RatingItemSlots } from '@fluentui/react-rating'; +import { RatingItemBaseState as RatingItemState } from '@fluentui/react-rating'; +import { RatingBaseProps as RatingProps } from '@fluentui/react-rating'; +import { RatingBaseState as RatingState } from '@fluentui/react-rating'; import * as React_2 from 'react'; +import { renderAccordion_unstable as renderAccordion } from '@fluentui/react-accordion'; +import { renderAccordionHeader_unstable as renderAccordionHeader } from '@fluentui/react-accordion'; +import { renderAccordionItem_unstable as renderAccordionItem } from '@fluentui/react-accordion'; +import { renderBadge_unstable as renderBadge } from '@fluentui/react-badge'; +import { renderBreadcrumb_unstable as renderBreadcrumb } from '@fluentui/react-breadcrumb'; +import { renderBreadcrumbButton_unstable as renderBreadcrumbButton } from '@fluentui/react-breadcrumb'; +import { renderBreadcrumbDivider_unstable as renderBreadcrumbDivider } from '@fluentui/react-breadcrumb'; +import { renderBreadcrumbItem_unstable as renderBreadcrumbItem } from '@fluentui/react-breadcrumb'; +import { renderButton_unstable as renderButton } from '@fluentui/react-button'; +import { renderCard_unstable as renderCard } from '@fluentui/react-card'; +import { renderCardFooter_unstable as renderCardFooter } from '@fluentui/react-card'; +import { renderCardHeader_unstable as renderCardHeader } from '@fluentui/react-card'; +import { renderCardPreview_unstable as renderCardPreview } from '@fluentui/react-card'; +import { renderDivider_unstable as renderDivider } from '@fluentui/react-divider'; +import { renderImage_unstable as renderImage } from '@fluentui/react-image'; +import { renderInput_unstable as renderInput } from '@fluentui/react-input'; +import { renderLink_unstable as renderLink } from '@fluentui/react-link'; +import { renderRadio_unstable as renderRadio } from '@fluentui/react-radio'; +import { renderRadioGroup_unstable as renderRadioGroup } from '@fluentui/react-radio'; +import { renderRating_unstable as renderRating } from '@fluentui/react-rating'; +import { renderRatingDisplay_unstable as renderRatingDisplay } from '@fluentui/react-rating'; +import { renderSearchBox_unstable as renderSearchBox } from '@fluentui/react-search'; +import { renderSkeleton_unstable as renderSkeleton } from '@fluentui/react-skeleton'; +import { renderSkeletonItem_unstable as renderSkeletonItem } from '@fluentui/react-skeleton'; +import { renderSlider_unstable as renderSlider } from '@fluentui/react-slider'; +import { renderSpinButton_unstable as renderSpinButton } from '@fluentui/react-spinbutton'; +import { renderSpinner_unstable as renderSpinner } from '@fluentui/react-spinner'; +import { renderSwitch_unstable as renderSwitch } from '@fluentui/react-switch'; +import { renderTextarea_unstable as renderTextarea } from '@fluentui/react-textarea'; +import { renderToggleButton_unstable as renderToggleButton } from '@fluentui/react-button'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { resolveShorthand } from '@fluentui/react-utilities'; +import { ResolveShorthandFunction } from '@fluentui/react-utilities'; +import { ResolveShorthandOptions } from '@fluentui/react-utilities'; +import { SearchBoxBaseProps as SearchBoxProps } from '@fluentui/react-search'; +import { SearchBoxSlots } from '@fluentui/react-search'; +import { SearchBoxBaseState as SearchBoxState } from '@fluentui/react-search'; +import { SelectionHookParams } from '@fluentui/react-utilities'; +import { SelectionItemId } from '@fluentui/react-utilities'; +import { SelectionMethods } from '@fluentui/react-utilities'; +import { SelectionMode as SelectionMode_2 } from '@fluentui/react-utilities'; +import { SkeletonContextValues } from '@fluentui/react-skeleton'; +import { SkeletonItemBaseProps as SkeletonItemProps } from '@fluentui/react-skeleton'; +import { SkeletonItemSlots } from '@fluentui/react-skeleton'; +import { SkeletonItemBaseState as SkeletonItemState } from '@fluentui/react-skeleton'; +import { SkeletonBaseProps as SkeletonProps } from '@fluentui/react-skeleton'; +import { SkeletonBaseState as SkeletonState } from '@fluentui/react-skeleton'; +import { SliderBaseProps as SliderProps } from '@fluentui/react-slider'; +import { SliderSlots } from '@fluentui/react-slider'; +import { SliderBaseState as SliderState } from '@fluentui/react-slider'; +import { Slot } from '@fluentui/react-utilities'; +import { slot } from '@fluentui/react-utilities'; +import { SlotClassNames } from '@fluentui/react-utilities'; +import { SlotComponentType } from '@fluentui/react-utilities'; +import { SlotOptions } from '@fluentui/react-utilities'; +import { SlotPropsRecord } from '@fluentui/react-utilities'; +import { SlotRenderFunction } from '@fluentui/react-utilities'; +import { SpinButtonBaseProps as SpinButtonProps } from '@fluentui/react-spinbutton'; +import { SpinButtonSlots } from '@fluentui/react-spinbutton'; +import { SpinButtonBaseState as SpinButtonState } from '@fluentui/react-spinbutton'; +import { SpinnerBaseProps as SpinnerProps } from '@fluentui/react-spinner'; +import { SpinnerSlots } from '@fluentui/react-spinner'; +import { SpinnerBaseState as SpinnerState } from '@fluentui/react-spinner'; +import { SSRProvider } from '@fluentui/react-utilities'; +import { SwitchBaseProps as SwitchProps } from '@fluentui/react-switch'; +import { SwitchSlots } from '@fluentui/react-switch'; +import { SwitchBaseState as SwitchState } from '@fluentui/react-switch'; +import { TextareaBaseProps as TextareaProps } from '@fluentui/react-textarea'; +import { TextareaSlots } from '@fluentui/react-textarea'; +import { TextareaBaseState as TextareaState } from '@fluentui/react-textarea'; +import { ToggleButtonBaseProps as ToggleButtonProps } from '@fluentui/react-button'; +import { ToggleButtonBaseState as ToggleButtonState } from '@fluentui/react-button'; +import { useAccordionBase_unstable as useAccordion } from '@fluentui/react-accordion'; +import { useAccordionHeaderBase_unstable as useAccordionHeader } from '@fluentui/react-accordion'; +import { useAccordionItem_unstable as useAccordionItem } from '@fluentui/react-accordion'; +import { useAccordionItemContext_unstable as useAccordionItemContext } from '@fluentui/react-accordion'; +import { useAccordionItemContextValues_unstable as useAccordionItemContextValues } from '@fluentui/react-accordion'; +import { useAccordionPanelBase_unstable as useAccordionPanel } from '@fluentui/react-accordion'; +import { useAnimationFrame } from '@fluentui/react-utilities'; +import { useAnnounce } from '@fluentui/react-shared-contexts'; +import { useApplyScrollbarWidth } from '@fluentui/react-utilities'; +import { useBadgeBase_unstable as useBadge } from '@fluentui/react-badge'; +import { useBreadcrumbBase_unstable as useBreadcrumb } from '@fluentui/react-breadcrumb'; +import { useBreadcrumbButtonBase_unstable as useBreadcrumbButton } from '@fluentui/react-breadcrumb'; +import { useBreadcrumbDividerBase_unstable as useBreadcrumbDivider } from '@fluentui/react-breadcrumb'; +import { useBreadcrumbItemBase_unstable as useBreadcrumbItem } from '@fluentui/react-breadcrumb'; import { useButtonBase_unstable as useButton } from '@fluentui/react-button'; +import { useCardBase_unstable as useCard } from '@fluentui/react-card'; +import { useCardFooterBase_unstable as useCardFooter } from '@fluentui/react-card'; +import { useCardHeaderBase_unstable as useCardHeader } from '@fluentui/react-card'; +import { useCardPreviewBase_unstable as useCardPreview } from '@fluentui/react-card'; +import { useCounterBadgeBase_unstable as useCounterBadge } from '@fluentui/react-badge'; +import { useDividerBase_unstable as useDivider } from '@fluentui/react-divider'; +import { useEventCallback } from '@fluentui/react-utilities'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { useId } from '@fluentui/react-utilities'; +import { useImageBase_unstable as useImage } from '@fluentui/react-image'; +import { useInputBase_unstable as useInput } from '@fluentui/react-input'; +import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; +import { useIsSSR } from '@fluentui/react-utilities'; +import { useLinkBase_unstable as useLink } from '@fluentui/react-link'; +import { useMergedRefs } from '@fluentui/react-utilities'; +import { usePersonaBase_unstable as usePersona } from '@fluentui/react-persona'; +import { usePortalMountNode } from '@fluentui/react-shared-contexts'; +import { usePresenceBadgeBase_unstable as usePresenceBadge } from '@fluentui/react-badge'; +import { useRadioBase_unstable as useRadio } from '@fluentui/react-radio'; +import { useRadioGroupBase_unstable as useRadioGroup } from '@fluentui/react-radio'; +import { useRatingBase_unstable as useRating } from '@fluentui/react-rating'; +import { useRatingDisplayBase_unstable as useRatingDisplay } from '@fluentui/react-rating'; +import { useScrollbarWidth } from '@fluentui/react-utilities'; +import { useSearchBoxBase_unstable as useSearchBox } from '@fluentui/react-search'; +import { useSelection } from '@fluentui/react-utilities'; +import { useSkeletonBase_unstable as useSkeleton } from '@fluentui/react-skeleton'; +import { useSkeletonItemBase_unstable as useSkeletonItem } from '@fluentui/react-skeleton'; +import { useSliderBase_unstable as useSlider } from '@fluentui/react-slider'; +import { useSpinButtonBase_unstable as useSpinButton } from '@fluentui/react-spinbutton'; +import { useSpinnerBase_unstable as useSpinner } from '@fluentui/react-spinner'; +import { useSwitchBase_unstable as useSwitch } from '@fluentui/react-switch'; +import { useTextareaBase_unstable as useTextarea } from '@fluentui/react-textarea'; +import { useThemeClassName_unstable as useThemeClassName } from '@fluentui/react-shared-contexts'; +import { useTimeout } from '@fluentui/react-utilities'; +import { useToggleButtonBase_unstable as useToggleButton } from '@fluentui/react-button'; +import { useTooltipVisibility_unstable as useTooltipVisibility } from '@fluentui/react-shared-contexts'; + +export { AccordionContextValue } + +export { AccordionContextValues } + +export { AccordionHeaderContextValues } + +export { AccordionHeaderProps } + +export { AccordionHeaderSlots } + +export { AccordionHeaderState } + +export { AccordionItemContextValue } + +export { AccordionItemContextValues } + +export { AccordionItemProps } + +export { AccordionItemSlots } + +export { AccordionItemState } + +export { AccordionPanelProps } + +export { AccordionPanelSlots } + +export { AccordionPanelState } + +export { AccordionProps } + +export { AccordionSlots } + +export { AccordionState } + +export { AnnounceContextValue } + +export { AnnounceProvider } + +export { assertSlots } + +export { BadgeProps } + +export { BadgeSlots } + +export { BadgeState } + +export { BreadcrumbButtonProps } + +export { BreadcrumbButtonSlots } + +export { BreadcrumbButtonState } + +export { BreadcrumbContextValues } + +export { BreadcrumbDividerProps } + +export { BreadcrumbDividerSlots } + +export { BreadcrumbDividerState } + +export { BreadcrumbItemProps } + +export { BreadcrumbItemSlots } + +export { BreadcrumbItemState } + +export { BreadcrumbProps } + +export { BreadcrumbSlots } + +export { BreadcrumbState } export { ButtonProps } @@ -18,17 +312,415 @@ export { ButtonSlots } export { ButtonState } +export { CardFooterProps } + +export { CardFooterSlots } + +export { CardFooterState } + +export { CardHeaderProps } + +export { CardHeaderSlots } + +export { CardHeaderState } + +export { CardPreviewProps } + +export { CardPreviewSlots } + +export { CardPreviewState } + +export { CardProps } + +export { CardSlots } + +export { CardState } + +export { ComponentProps } + +export { ComponentState } + // @public export function composeComponent(options: ComposeComponentOptions): ForwardRefComponent; // @public export type ComposeComponentOptions = [ContextValues] extends [never] ? ComposeComponentOptionsWithoutContext : ComposeComponentOptionsWithContext; +export { CounterBadgeProps } + +export { CounterBadgeState } + +export { DividerProps } + +export { DividerSlots } + +export { DividerState } + +export { ForwardRefComponent } + +export { getIntrinsicElementProps } + +export { getNativeElementProps } + +export { getPartitionedNativeProps } + +export { getSlotClassNameProp_unstable } + +export { getSlots } + +export { IdPrefixProvider } + +export { ImageProps } + +export { ImageSlots } + +export { ImageState } + +export { InputProps } + +export { InputSlots } + +export { InputState } + +export { isHTMLElement } + +export { JSXElement } + +export { JSXIntrinsicElement } + +export { JSXIntrinsicElementKeys } + +export { LinkProps } + +export { LinkSlots } + +export { LinkState } + +export { mergeCallbacks } + +export { OnSelectionChangeCallback } + +export { OnSelectionChangeData } + +export { PersonaProps } + +export { PersonaSlots } + +export { PersonaState } + +export { PortalMountNodeProvider } + +export { PresenceBadgeProps } + +export { PresenceBadgeState } + +export { RadioGroupContextValue } + +export { RadioGroupContextValues } + +export { RadioGroupProps } + +export { RadioGroupSlots } + +export { RadioGroupState } + +export { RadioProps } + +export { RadioSlots } + +export { RadioState } + +export { RatingContextValues } + +export { RatingDisplayContextValues } + +export { RatingDisplayProps } + +export { RatingDisplayState } + +export { RatingItemProps } + +export { RatingItemSlots } + +export { RatingItemState } + +export { RatingProps } + +export { RatingState } + +export { renderAccordion } + +export { renderAccordionHeader } + +export { renderAccordionItem } + +// @public (undocumented) +export const renderAccordionPanel: (state: AccordionPanelState) => JSXElement; + +export { renderBadge } + +export { renderBreadcrumb } + +export { renderBreadcrumbButton } + +export { renderBreadcrumbDivider } + +export { renderBreadcrumbItem } + +export { renderButton } + +export { renderCard } + +export { renderCardFooter } + +export { renderCardHeader } + +export { renderCardPreview } + +export { renderDivider } + +export { renderImage } + +export { renderInput } + +export { renderLink } + // @public (undocumented) -export const renderButton: (state: ButtonState) => JSXElement; +export const renderPersona: (state: PersonaState) => JSXElement; + +export { renderRadio } + +export { renderRadioGroup } + +export { renderRating } + +export { renderRatingDisplay } + +export { renderSearchBox } + +export { renderSkeleton } + +export { renderSkeletonItem } + +export { renderSlider } + +export { renderSpinButton } + +export { renderSpinner } + +export { renderSwitch } + +export { renderTextarea } + +export { renderToggleButton } + +export { resetIdsForTests } + +export { resolveShorthand } + +export { ResolveShorthandFunction } + +export { ResolveShorthandOptions } + +export { SearchBoxProps } + +export { SearchBoxSlots } + +export { SearchBoxState } + +export { SelectionHookParams } + +export { SelectionItemId } + +export { SelectionMethods } + +export { SelectionMode_2 as SelectionMode } + +export { SkeletonContextValues } + +export { SkeletonItemProps } + +export { SkeletonItemSlots } + +export { SkeletonItemState } + +export { SkeletonProps } + +export { SkeletonState } + +export { SliderProps } + +export { SliderSlots } + +export { SliderState } + +export { Slot } + +export { slot } + +export { SlotClassNames } + +export { SlotComponentType } + +export { SlotOptions } + +export { SlotPropsRecord } + +export { SlotRenderFunction } + +export { SpinButtonProps } + +export { SpinButtonSlots } + +export { SpinButtonState } + +export { SpinnerProps } + +export { SpinnerSlots } + +export { SpinnerState } + +export { SSRProvider } + +export { SwitchProps } + +export { SwitchSlots } + +export { SwitchState } + +export { TextareaProps } + +export { TextareaSlots } + +export { TextareaState } + +export { ToggleButtonProps } + +export { ToggleButtonState } + +export { useAccordion } + +// @public (undocumented) +export const useAccordionContextValues: (state: AccordionState) => AccordionContextValues; + +export { useAccordionHeader } + +// @public (undocumented) +export const useAccordionHeaderContextValues: (state: AccordionHeaderState) => AccordionHeaderContextValues; + +export { useAccordionItem } + +export { useAccordionItemContext } + +export { useAccordionItemContextValues } + +export { useAccordionPanel } + +export { useAnimationFrame } + +export { useAnnounce } + +export { useApplyScrollbarWidth } + +export { useBadge } + +export { useBreadcrumb } + +export { useBreadcrumbButton } + +export { useBreadcrumbDivider } + +export { useBreadcrumbItem } export { useButton } +export { useCard } + +// @public (undocumented) +export const useCardContextValues: (state: CardState) => CardContextValue; + +export { useCardFooter } + +export { useCardHeader } + +export { useCardPreview } + +export { useCounterBadge } + +export { useDivider } + +export { useEventCallback } + +export { useFluent } + +export { useId } + +export { useImage } + +export { useInput } + +export { useIsomorphicLayoutEffect } + +export { useIsSSR } + +export { useLink } + +export { useMergedRefs } + +export { usePersona } + +export { usePortalMountNode } + +export { usePresenceBadge } + +export { useRadio } + +export { useRadioGroup } + +// @public (undocumented) +export const useRadioGroupContextValues: (state: RadioGroupState) => RadioGroupContextValues; + +export { useRating } + +// @public (undocumented) +export const useRatingContextValues: (state: RatingState) => RatingContextValues; + +export { useRatingDisplay } + +// @public (undocumented) +export const useRatingDisplayContextValues: (state: RatingDisplayState) => RatingDisplayContextValues; + +export { useScrollbarWidth } + +export { useSearchBox } + +export { useSelection } + +export { useSkeleton } + +// @public (undocumented) +export const useSkeletonContextValues: (state: SkeletonState) => SkeletonContextValues; + +export { useSkeletonItem } + +export { useSlider } + +export { useSpinButton } + +export { useSpinner } + +export { useSwitch } + +export { useTextarea } + +export { useThemeClassName } + +export { useTimeout } + +export { useToggleButton } + +export { useTooltipVisibility } + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-base-components/library/package.json b/packages/react-components/react-base-components/library/package.json index 7bcc759345a87..8bf9996e8f36b 100644 --- a/packages/react-components/react-base-components/library/package.json +++ b/packages/react-components/react-base-components/library/package.json @@ -64,7 +64,6 @@ "@fluentui/react-utilities": "^9.26.2", "@fluentui/react-text": "^9.6.15", "@fluentui/react-tree": "^9.15.15", - "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { diff --git a/packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx b/packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx deleted file mode 100644 index 49b10e492654e..0000000000000 --- a/packages/react-components/react-base-components/library/src/components/Button/Button.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { useButton, renderButton } from './Button'; -import { composeComponent } from '../../utils/composeComponent/composeComponent'; - -const Button = composeComponent({ - displayName: 'Button', - useState: useButton, - useStyles(state) { - state.root.className = [ - 'btn', - state.disabled && `btn--disabled`, - state.iconOnly && 'btn--icon', - state.disabledFocusable && `btn--disabledFocusable`, - ] - .filter(Boolean) - .join(' '); - - if (state.icon) { - state.icon.className = 'btn__icon'; - } - }, - render: renderButton, -}); - -describe('Button', () => { - it('renders a button element by default', () => { - const { getByRole } = render( - , - ); - - const button = getByRole('button'); - expect(button).toMatchInlineSnapshot(` - - `); - }); - - it('renders an anchor element when href is provided', () => { - const { getByRole } = render( - , - ); - - const link = getByRole('link'); - expect(link).toMatchInlineSnapshot(` - - Visit example.com - - `); - }); -}); diff --git a/packages/react-components/react-base-components/library/src/components/Button/Button.ts b/packages/react-components/react-base-components/library/src/components/Button/Button.ts deleted file mode 100644 index 6b8c38529eb72..0000000000000 --- a/packages/react-components/react-base-components/library/src/components/Button/Button.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type ButtonBaseState, renderButton_unstable } from '@fluentui/react-button'; -import type { JSXElement } from '@fluentui/react-utilities'; - -export type { - ButtonSlots, - ButtonBaseState as ButtonState, - ButtonBaseProps as ButtonProps, -} from '@fluentui/react-button'; - -export { useButtonBase_unstable as useButton } from '@fluentui/react-button'; - -// TODO: Remove the cast once the renderButton_unstable is updated to use the ButtonBaseState instead of ButtonState -export const renderButton = renderButton_unstable as (state: ButtonBaseState) => JSXElement; diff --git a/packages/react-components/react-base-components/library/src/index.ts b/packages/react-components/react-base-components/library/src/index.ts index 1a65f67dc8818..97088668507c1 100644 --- a/packages/react-components/react-base-components/library/src/index.ts +++ b/packages/react-components/react-base-components/library/src/index.ts @@ -1,5 +1,441 @@ -export type { ButtonProps, ButtonState, ButtonSlots } from './components/Button/Button'; -export { useButton, renderButton } from './components/Button/Button'; +import type { JSXElement } from '@fluentui/react-utilities'; + +export { + AnnounceProvider, + PortalMountNodeProvider, + useAnnounce, + useFluent_unstable as useFluent, + usePortalMountNode, + useTooltipVisibility_unstable as useTooltipVisibility, + useThemeClassName_unstable as useThemeClassName, +} from '@fluentui/react-shared-contexts'; +export type { AnnounceContextValue } from '@fluentui/react-shared-contexts'; +export { + // getNativeElementProps is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + getNativeElementProps, + getIntrinsicElementProps, + getPartitionedNativeProps, + getSlotClassNameProp_unstable, + // getSlots is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + getSlots, + slot, + assertSlots, + IdPrefixProvider, + resetIdsForTests, + // resolveShorthand is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + resolveShorthand, + SSRProvider, + useAnimationFrame, + useId, + useIsomorphicLayoutEffect, + useEventCallback, + mergeCallbacks, + useIsSSR, + useMergedRefs, + useApplyScrollbarWidth, + useScrollbarWidth, + useSelection, + useTimeout, + isHTMLElement, +} from '@fluentui/react-utilities'; +export type { + ComponentProps, + ComponentState, + ForwardRefComponent, + JSXElement, + JSXIntrinsicElement, + JSXIntrinsicElementKeys, + // ResolveShorthandFunction is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + ResolveShorthandFunction, + // ResolveShorthandOptions is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + ResolveShorthandOptions, + Slot, + SlotOptions, + SlotComponentType, + SlotClassNames, + SlotPropsRecord, + SlotRenderFunction, + OnSelectionChangeCallback, + OnSelectionChangeData, + SelectionHookParams, + SelectionItemId, + SelectionMethods, + SelectionMode, +} from '@fluentui/react-utilities'; + +// ─── @fluentui/react-accordion ─────────────────────────────────────────────── + +import type { + AccordionBaseState, + AccordionHeaderBaseState, + AccordionPanelBaseState, + AccordionContextValues, + AccordionHeaderContextValues, +} from '@fluentui/react-accordion'; +import { + useAccordionContextValues_unstable, + useAccordionHeaderContextValues_unstable, + renderAccordionPanel_unstable, +} from '@fluentui/react-accordion'; + +export type { + AccordionSlots, + AccordionBaseState as AccordionState, + AccordionBaseProps as AccordionProps, + AccordionContextValue, + AccordionContextValues, + AccordionItemSlots, + AccordionItemProps, + AccordionItemState, + AccordionItemContextValues, + AccordionItemContextValue, + AccordionHeaderSlots, + AccordionHeaderBaseState as AccordionHeaderState, + AccordionHeaderBaseProps as AccordionHeaderProps, + AccordionHeaderContextValues, + AccordionPanelSlots, + AccordionPanelBaseState as AccordionPanelState, + AccordionPanelBaseProps as AccordionPanelProps, +} from '@fluentui/react-accordion'; + +export { + useAccordionBase_unstable as useAccordion, + renderAccordion_unstable as renderAccordion, + useAccordionItemContextValues_unstable as useAccordionItemContextValues, + useAccordionItemContext_unstable as useAccordionItemContext, + useAccordionItem_unstable as useAccordionItem, + renderAccordionItem_unstable as renderAccordionItem, + useAccordionHeaderBase_unstable as useAccordionHeader, + renderAccordionHeader_unstable as renderAccordionHeader, + useAccordionPanelBase_unstable as useAccordionPanel, +} from '@fluentui/react-accordion'; + +export const useAccordionContextValues = useAccordionContextValues_unstable as ( + state: AccordionBaseState, +) => AccordionContextValues; + +export const useAccordionHeaderContextValues = useAccordionHeaderContextValues_unstable as ( + state: AccordionHeaderBaseState, +) => AccordionHeaderContextValues; + +export const renderAccordionPanel = renderAccordionPanel_unstable as (state: AccordionPanelBaseState) => JSXElement; + +// ─── @fluentui/react-badge ─────────────────────────────────────────────────── + +export type { + BadgeSlots, + BadgeBaseState as BadgeState, + BadgeBaseProps as BadgeProps, + PresenceBadgeBaseState as PresenceBadgeState, + PresenceBadgeBaseProps as PresenceBadgeProps, + CounterBadgeBaseState as CounterBadgeState, + CounterBadgeBaseProps as CounterBadgeProps, +} from '@fluentui/react-badge'; + +export { + useBadgeBase_unstable as useBadge, + renderBadge_unstable as renderBadge, + usePresenceBadgeBase_unstable as usePresenceBadge, + useCounterBadgeBase_unstable as useCounterBadge, +} from '@fluentui/react-badge'; + +// ─── @fluentui/react-breadcrumb ────────────────────────────────────────────── + +export type { + BreadcrumbSlots, + BreadcrumbBaseState as BreadcrumbState, + BreadcrumbBaseProps as BreadcrumbProps, + BreadcrumbDividerSlots, + BreadcrumbDividerBaseState as BreadcrumbDividerState, + BreadcrumbDividerBaseProps as BreadcrumbDividerProps, + BreadcrumbItemSlots, + BreadcrumbItemBaseState as BreadcrumbItemState, + BreadcrumbItemBaseProps as BreadcrumbItemProps, + BreadcrumbButtonSlots, + BreadcrumbButtonBaseState as BreadcrumbButtonState, + BreadcrumbButtonBaseProps as BreadcrumbButtonProps, + BreadcrumbContextValues, +} from '@fluentui/react-breadcrumb'; + +export { + useBreadcrumbBase_unstable as useBreadcrumb, + useBreadcrumbDividerBase_unstable as useBreadcrumbDivider, + useBreadcrumbItemBase_unstable as useBreadcrumbItem, + useBreadcrumbButtonBase_unstable as useBreadcrumbButton, + renderBreadcrumb_unstable as renderBreadcrumb, + renderBreadcrumbDivider_unstable as renderBreadcrumbDivider, + renderBreadcrumbItem_unstable as renderBreadcrumbItem, + renderBreadcrumbButton_unstable as renderBreadcrumbButton, +} from '@fluentui/react-breadcrumb'; + +// ─── @fluentui/react-button ────────────────────────────────────────────────── + +export type { + ButtonSlots, + ButtonBaseState as ButtonState, + ButtonBaseProps as ButtonProps, + ToggleButtonBaseState as ToggleButtonState, + ToggleButtonBaseProps as ToggleButtonProps, +} from '@fluentui/react-button'; + +export { + useButtonBase_unstable as useButton, + renderButton_unstable as renderButton, + useToggleButtonBase_unstable as useToggleButton, + renderToggleButton_unstable as renderToggleButton, +} from '@fluentui/react-button'; + +// ─── @fluentui/react-card ──────────────────────────────────────────────────── +import { type CardBaseState, type CardContextValue, useCardContext_unstable } from '@fluentui/react-card'; + +export type { + CardSlots, + CardBaseState as CardState, + CardBaseProps as CardProps, + CardFooterSlots, + CardFooterBaseState as CardFooterState, + CardFooterBaseProps as CardFooterProps, + CardHeaderSlots, + CardHeaderBaseState as CardHeaderState, + CardHeaderBaseProps as CardHeaderProps, + CardPreviewSlots, + CardPreviewBaseState as CardPreviewState, + CardPreviewBaseProps as CardPreviewProps, +} from '@fluentui/react-card'; + +export { + useCardBase_unstable as useCard, + renderCard_unstable as renderCard, + useCardFooterBase_unstable as useCardFooter, + renderCardFooter_unstable as renderCardFooter, + useCardHeaderBase_unstable as useCardHeader, + renderCardHeader_unstable as renderCardHeader, + useCardPreviewBase_unstable as useCardPreview, + renderCardPreview_unstable as renderCardPreview, +} from '@fluentui/react-card'; + +export const useCardContextValues = useCardContext_unstable as (state: CardBaseState) => CardContextValue; + +// ─── @fluentui/react-divider ───────────────────────────────────────────────── + +export type { + DividerSlots, + DividerBaseState as DividerState, + DividerBaseProps as DividerProps, +} from '@fluentui/react-divider'; + +export { + useDividerBase_unstable as useDivider, + renderDivider_unstable as renderDivider, +} from '@fluentui/react-divider'; + +// ─── @fluentui/react-image ─────────────────────────────────────────────────── + +export type { ImageSlots, ImageBaseState as ImageState, ImageBaseProps as ImageProps } from '@fluentui/react-image'; + +export { useImageBase_unstable as useImage, renderImage_unstable as renderImage } from '@fluentui/react-image'; + +// ─── @fluentui/react-input ─────────────────────────────────────────────────── + +export type { InputSlots, InputBaseState as InputState, InputBaseProps as InputProps } from '@fluentui/react-input'; + +export { useInputBase_unstable as useInput, renderInput_unstable as renderInput } from '@fluentui/react-input'; + +// ─── @fluentui/react-link ──────────────────────────────────────────────────── + +export type { LinkSlots, LinkBaseState as LinkState, LinkBaseProps as LinkProps } from '@fluentui/react-link'; + +export { useLinkBase_unstable as useLink, renderLink_unstable as renderLink } from '@fluentui/react-link'; + +// ─── @fluentui/react-persona ───────────────────────────────────────────────── + +import type { PersonaBaseState } from '@fluentui/react-persona'; +import { renderPersona_unstable } from '@fluentui/react-persona'; + +export type { + PersonaSlots, + PersonaBaseState as PersonaState, + PersonaBaseProps as PersonaProps, +} from '@fluentui/react-persona'; + +export { usePersonaBase_unstable as usePersona } from '@fluentui/react-persona'; + +export const renderPersona = renderPersona_unstable as (state: PersonaBaseState) => JSXElement; + +// ─── @fluentui/react-radio ─────────────────────────────────────────────────── + +import { + type RadioGroupBaseState, + type RadioGroupContextValues, + useRadioGroupContextValues as useRadioGroupContextValues_unstable, +} from '@fluentui/react-radio'; + +export type { + RadioGroupSlots, + RadioGroupBaseState as RadioGroupState, + RadioGroupBaseProps as RadioGroupProps, + RadioSlots, + RadioBaseState as RadioState, + RadioBaseProps as RadioProps, + RadioGroupContextValue, + RadioGroupContextValues, +} from '@fluentui/react-radio'; + +export { + useRadioGroupBase_unstable as useRadioGroup, + renderRadioGroup_unstable as renderRadioGroup, + useRadioBase_unstable as useRadio, + renderRadio_unstable as renderRadio, +} from '@fluentui/react-radio'; + +export const useRadioGroupContextValues = useRadioGroupContextValues_unstable as ( + state: RadioGroupBaseState, +) => RadioGroupContextValues; + +// ─── @fluentui/react-rating ────────────────────────────────────────────────── + +import type { + RatingBaseState, + RatingContextValues, + RatingDisplayContextValues, + RatingDisplayBaseState, +} from '@fluentui/react-rating'; +import { + useRatingContextValues as useRatingContextValues_unstable, + useRatingDisplayContextValues as useRatingDisplayContextValues_unstable, +} from '@fluentui/react-rating'; + +export type { + RatingBaseState as RatingState, + RatingBaseProps as RatingProps, + RatingContextValues, + RatingDisplayBaseState as RatingDisplayState, + RatingDisplayBaseProps as RatingDisplayProps, + RatingDisplayContextValues, + RatingItemSlots, + RatingItemBaseState as RatingItemState, + RatingItemBaseProps as RatingItemProps, +} from '@fluentui/react-rating'; + +export { + useRatingBase_unstable as useRating, + useRatingDisplayBase_unstable as useRatingDisplay, + renderRating_unstable as renderRating, + renderRatingDisplay_unstable as renderRatingDisplay, + // useRatingItemBase_unstable as useRatingItem, +} from '@fluentui/react-rating'; + +export const useRatingContextValues = useRatingContextValues_unstable as ( + state: RatingBaseState, +) => RatingContextValues; + +export const useRatingDisplayContextValues = useRatingDisplayContextValues_unstable as ( + state: RatingDisplayBaseState, +) => RatingDisplayContextValues; + +// ─── @fluentui/react-search ────────────────────────────────────────────────── + +export type { + SearchBoxSlots, + SearchBoxBaseState as SearchBoxState, + SearchBoxBaseProps as SearchBoxProps, +} from '@fluentui/react-search'; + +export { + useSearchBoxBase_unstable as useSearchBox, + renderSearchBox_unstable as renderSearchBox, +} from '@fluentui/react-search'; + +// ─── @fluentui/react-skeleton ──────────────────────────────────────────────── + +import type { SkeletonBaseState, SkeletonContextValues } from '@fluentui/react-skeleton'; +import { useSkeletonContextValues as useSkeletonContextValues_unstable } from '@fluentui/react-skeleton'; + +export type { + SkeletonBaseState as SkeletonState, + SkeletonBaseProps as SkeletonProps, + SkeletonItemSlots, + SkeletonItemBaseState as SkeletonItemState, + SkeletonItemBaseProps as SkeletonItemProps, + SkeletonContextValues, +} from '@fluentui/react-skeleton'; + +export { + useSkeletonBase_unstable as useSkeleton, + useSkeletonItemBase_unstable as useSkeletonItem, + renderSkeletonItem_unstable as renderSkeletonItem, + renderSkeleton_unstable as renderSkeleton, +} from '@fluentui/react-skeleton'; + +export const useSkeletonContextValues = useSkeletonContextValues_unstable as ( + state: SkeletonBaseState, +) => SkeletonContextValues; + +// ─── @fluentui/react-slider ────────────────────────────────────────────────── + +export type { + SliderSlots, + SliderBaseState as SliderState, + SliderBaseProps as SliderProps, +} from '@fluentui/react-slider'; + +export { useSliderBase_unstable as useSlider, renderSlider_unstable as renderSlider } from '@fluentui/react-slider'; + +// ─── @fluentui/react-spinbutton ────────────────────────────────────────────── + +export type { + SpinButtonSlots, + SpinButtonBaseState as SpinButtonState, + SpinButtonBaseProps as SpinButtonProps, +} from '@fluentui/react-spinbutton'; + +export { + useSpinButtonBase_unstable as useSpinButton, + renderSpinButton_unstable as renderSpinButton, +} from '@fluentui/react-spinbutton'; + +// ─── @fluentui/react-spinner ───────────────────────────────────────────────── + +export type { + SpinnerSlots, + SpinnerBaseState as SpinnerState, + SpinnerBaseProps as SpinnerProps, +} from '@fluentui/react-spinner'; + +export { + useSpinnerBase_unstable as useSpinner, + renderSpinner_unstable as renderSpinner, +} from '@fluentui/react-spinner'; + +// ─── @fluentui/react-switch ────────────────────────────────────────────────── + +export type { + SwitchSlots, + SwitchBaseState as SwitchState, + SwitchBaseProps as SwitchProps, +} from '@fluentui/react-switch'; + +export { useSwitchBase_unstable as useSwitch, renderSwitch_unstable as renderSwitch } from '@fluentui/react-switch'; + +// ─── @fluentui/react-textarea ──────────────────────────────────────────────── + +export type { + TextareaSlots, + TextareaBaseState as TextareaState, + TextareaBaseProps as TextareaProps, +} from '@fluentui/react-textarea'; + +export { + useTextareaBase_unstable as useTextarea, + renderTextarea_unstable as renderTextarea, +} from '@fluentui/react-textarea'; + +// ─── Utilities ─────────────────────────────────────────────────────────────── export { composeComponent } from './utils/composeComponent'; export type { ComposeComponentOptions } from './utils/composeComponent'; diff --git a/packages/react-components/react-base-components/library/src/testing/isConformant.ts b/packages/react-components/react-base-components/library/src/testing/isConformant.ts deleted file mode 100644 index 8ed2da0f92513..0000000000000 --- a/packages/react-components/react-base-components/library/src/testing/isConformant.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { isConformant as baseIsConformant } from '@fluentui/react-conformance'; -import type { IsConformantOptions, TestObject } from '@fluentui/react-conformance'; -import griffelTests from '@fluentui/react-conformance-griffel'; - -export function isConformant( - testInfo: Omit, 'componentPath'> & { componentPath?: string }, -): void { - const defaultOptions: Partial> = { - tsConfig: { configName: 'tsconfig.spec.json' }, - componentPath: require.main?.filename.replace('.test', ''), - extraTests: griffelTests as TestObject, - }; - - baseIsConformant(defaultOptions, testInfo); -} diff --git a/packages/react-components/react-base-components/stories/src/.gitkeep b/packages/react-components/react-base-components/stories/src/.gitkeep deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/packages/react-components/react-base-components/stories/src/Accordion/ExampleAccordion.stories.tsx b/packages/react-components/react-base-components/stories/src/Accordion/ExampleAccordion.stories.tsx new file mode 100644 index 0000000000000..e975e77daad06 --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Accordion/ExampleAccordion.stories.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; + +import { + composeComponent, + useAccordionHeader, + renderAccordionHeader, + useAccordionPanel, + renderAccordionPanel, + useAccordion, + useAccordionContextValues, + renderAccordion, + type AccordionProps, + type AccordionState, + type AccordionContextValues, + useAccordionItem, + renderAccordionItem, + useAccordionItemContextValues, + useAccordionHeaderContextValues, + AccordionHeaderProps, +} from '@fluentui/react-base-components'; + +const styles = { + root: { + width: '300px', + }, + item: { + marginBottom: '8px', + }, + header: { + display: 'flex', + alignItems: 'center', + width: '100%', + padding: '12px 16px', + backgroundColor: '#f5f5f5', + border: '1px solid #e5e5e5', + borderRadius: '6px 6px 0 0', + cursor: 'pointer', + fontSize: '14px', + fontWeight: 600, + textAlign: 'left' as const, + }, + headerButton: { + all: 'unset' as const, + display: 'flex', + alignItems: 'center', + gap: '8px', + width: '100%', + }, + panel: { + width: '100%', + padding: '12px 16px', + backgroundColor: '#ffffff', + border: '1px solid #e5e5e5', + borderTopWidth: 0, + borderRadius: '0 0 6px 6px', + fontSize: '14px', + lineHeight: 1.5, + }, +} as const; + +const Accordion = composeComponent({ + displayName: 'Accordion', + useState: useAccordion, + useContextValues: useAccordionContextValues, + useStyles: (state: AccordionState) => { + state.root.style = styles.root; + }, + render: renderAccordion, +}); + +const AccordionItem = composeComponent({ + displayName: 'AccordionItem', + useState: useAccordionItem, + useContextValues: useAccordionItemContextValues, + useStyles(state) { + state.root.style = styles.item; + }, + render: renderAccordionItem, +}); + +const AccordionHeader = composeComponent({ + displayName: 'AccordionHeader', + useState(props: AccordionHeaderProps, ref: React.Ref) { + const state = useAccordionHeader(props, ref); + + if (state.expandIcon) { + state.expandIcon.children = state.open ? '-' : '+'; + } + + return state; + }, + useContextValues: useAccordionHeaderContextValues, + useStyles: state => { + state.root.style = styles.header; + state.button.style = styles.headerButton; + }, + render: renderAccordionHeader, +}); + +const AccordionPanel = composeComponent({ + displayName: 'AccordionPanel', + useState: useAccordionPanel, + useStyles: state => { + state.root.style = { ...styles.panel, display: state.open ? 'block' : 'none' }; + }, + render: renderAccordionPanel, +}); + +export const Example = (): React.ReactNode => ( + console.log('Toggled item:', data.value)}> + + Section #1 + Section #1 content. + + + Section #2 + Section #2 content. + + + Section #3 + Section #3 content. + + +); + +Example.parameters = { + docs: { + description: { + story: + 'Apply custom styles to `AccordionHeader` and `AccordionPanel` using the `useStyles` hook inside `composeComponent`. Set styles on the `state.root` and `state.button` slot objects. This pattern works with any styling approach — inline styles, CSS modules, or CSS-in-JS — while retaining all accessibility and keyboard interaction from the base hooks.', + }, + }, +}; diff --git a/packages/react-components/react-base-components/stories/src/Accordion/index.stories.tsx b/packages/react-components/react-base-components/stories/src/Accordion/index.stories.tsx new file mode 100644 index 0000000000000..d462620c9afba --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Accordion/index.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { AccordionProps } from '@fluentui/react-base-components'; + +export { Example } from './ExampleAccordion.stories'; + +const meta: Meta = { + title: 'Base Components/Accordion', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Accordion components for building custom expandable section implementations. Compose `useAccordion`, `useAccordionHeader`, and `useAccordionPanel` with their corresponding render functions via `composeComponent`. The base hooks provide keyboard navigation, ARIA attributes, and expand/collapse state management while leaving all visual styling to you.', + }, + }, + }, +}; + +export default meta; diff --git a/packages/react-components/react-base-components/stories/src/Badge/ExampleBadge.stories.tsx b/packages/react-components/react-base-components/stories/src/Badge/ExampleBadge.stories.tsx new file mode 100644 index 0000000000000..5ee1b14805f68 --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Badge/ExampleBadge.stories.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; + +import { composeComponent, useBadge, renderBadge, useCounterBadge } from '@fluentui/react-base-components'; +import type { BadgeProps, BadgeState, CounterBadgeState } from '@fluentui/react-base-components'; + +type BadgeColor = 'success' | 'warning' | 'error' | 'info'; + +type CustomBadgeProps = BadgeProps & { color?: BadgeColor }; +type CustomBadgeState = BadgeState & { color: BadgeColor }; + +const colorStyles: Record = { + success: { backgroundColor: '#107c10', color: '#ffffff' }, + warning: { backgroundColor: '#797673', color: '#ffffff' }, + error: { backgroundColor: '#a4262c', color: '#ffffff' }, + info: { backgroundColor: '#0078d4', color: '#ffffff' }, +}; + +const baseStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '20px', + height: '20px', + padding: '0 6px', + borderRadius: '10px', + fontSize: '12px', + fontWeight: 600, +}; + +const Badge = composeComponent({ + displayName: 'Badge', + useState(props, ref) { + const { color = 'info', ...badgeProps } = props; + const state = useBadge(badgeProps, ref); + return { ...state, color }; + }, + useStyles(state: CustomBadgeState) { + state.root.style = { ...baseStyle, ...colorStyles[state.color] }; + }, + render: renderBadge, +}); + +const CounterBadge = composeComponent({ + displayName: 'CounterBadge', + useState: useCounterBadge, + useStyles(state: CounterBadgeState) { + state.root.style = { ...baseStyle, backgroundColor: '#a4262c', color: '#ffffff' }; + }, + render: renderBadge, +}); + +export const Example = (): React.ReactNode => ( +
+ Done + Review + Blocked + 12 + +
+); + +Example.parameters = { + docs: { + description: { + story: + 'Extend the base badge with a custom `color` prop and apply color-mapped styles in `useStyles`. This same pattern applies to `PresenceBadge` and `CounterBadge` — all three share the `renderBadge` render function and accept custom styling through `useStyles`. Mix and match the three variants in your design system to handle notification counts, status indicators, and presence badges.', + }, + }, +}; diff --git a/packages/react-components/react-base-components/stories/src/Badge/ExamplePresenceBadge.stories.tsx b/packages/react-components/react-base-components/stories/src/Badge/ExamplePresenceBadge.stories.tsx new file mode 100644 index 0000000000000..6e36448de8189 --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Badge/ExamplePresenceBadge.stories.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; + +import { composeComponent, usePresenceBadge, renderBadge } from '@fluentui/react-base-components'; +import type { PresenceBadgeState } from '@fluentui/react-base-components'; + +const baseStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '20px', + height: '20px', + padding: '0 6px', + borderRadius: '10px', + fontSize: '12px', + fontWeight: 600, +}; + +const PresenceBadge = composeComponent({ + displayName: 'PresenceBadge', + useState: usePresenceBadge, + useStyles(state: PresenceBadgeState) { + state.root.style = { ...baseStyle, backgroundColor: '#107c10', color: '#ffffff' }; + }, + render: renderBadge, +}); + +export const ExamplePresenceBadge = (): React.ReactNode => ( +
+ + + + +
+); + +ExamplePresenceBadge.parameters = { + docs: { + description: { + story: + 'Compose `usePresenceBadge` with `renderBadge` via `composeComponent` to build a presence indicator badge. The hook computes the correct ARIA role and status props — apply styles in `useStyles`.', + }, + }, +}; diff --git a/packages/react-components/react-base-components/stories/src/Badge/index.stories.tsx b/packages/react-components/react-base-components/stories/src/Badge/index.stories.tsx new file mode 100644 index 0000000000000..dd6a14e067b55 --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Badge/index.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { BadgeProps } from '@fluentui/react-base-components'; + +export { Example } from './ExampleBadge.stories'; +export { ExamplePresenceBadge } from './ExamplePresenceBadge.stories'; + +const meta: Meta = { + title: 'Base Components/Badge', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Badge components (`Badge`, `PresenceBadge`, `CounterBadge`) for building custom badge implementations. Compose `useBadge`, `usePresenceBadge`, or `useCounterBadge` with `renderBadge` via `composeComponent`. The base hooks compute the correct ARIA roles, variant props, and slot structure so you can focus purely on styling.', + }, + }, + }, +}; + +export default meta; diff --git a/packages/react-components/react-base-components/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx b/packages/react-components/react-base-components/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx new file mode 100644 index 0000000000000..eb7e5ef3adcaa --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; + +import { + composeComponent, + useBreadcrumb, + renderBreadcrumb, + useBreadcrumbItem, + renderBreadcrumbItem, + useBreadcrumbDivider, + renderBreadcrumbDivider, + useBreadcrumbButton, + renderBreadcrumbButton, +} from '@fluentui/react-base-components'; +import type { + BreadcrumbState, + BreadcrumbItemState, + BreadcrumbDividerState, + BreadcrumbButtonState, +} from '@fluentui/react-base-components'; + +const Breadcrumb = composeComponent({ + displayName: 'Breadcrumb', + useState: useBreadcrumb, + useStyles(state: BreadcrumbState) { + state.root.style = { display: 'flex', alignItems: 'center', listStyle: 'none', padding: 0, margin: 0 }; + if (state.list) { + state.list.style = { + display: 'flex', + alignItems: 'center', + gap: '4px', + listStyle: 'none', + padding: 0, + margin: 0, + }; + } + }, + render: renderBreadcrumb, +}); + +const BreadcrumbItem = composeComponent({ + displayName: 'BreadcrumbItem', + useState: useBreadcrumbItem, + useStyles(state: BreadcrumbItemState) { + state.root.style = { display: 'flex', alignItems: 'center' }; + }, + render: renderBreadcrumbItem, +}); + +const BreadcrumbDivider = composeComponent({ + displayName: 'BreadcrumbDivider', + useState: useBreadcrumbDivider, + useStyles(state: BreadcrumbDividerState) { + state.root.style = { color: '#8a8886', fontSize: '12px', userSelect: 'none' }; + }, + render: renderBreadcrumbDivider, +}); + +const BreadcrumbButton = composeComponent({ + displayName: 'BreadcrumbButton', + useState: useBreadcrumbButton, + useStyles(state: BreadcrumbButtonState) { + const isCurrent = state.current; + state.root.style = { + padding: '2px 4px', + borderRadius: '4px', + fontSize: '13px', + fontWeight: isCurrent ? 600 : 400, + color: isCurrent ? '#323130' : '#0078d4', + textDecoration: 'none', + background: 'none', + border: 'none', + cursor: isCurrent ? 'default' : 'pointer', + }; + }, + render: renderBreadcrumbButton, +}); + +export const Example = (): React.ReactNode => ( + + + Home + + / + + Documents + + / + + Report.pdf + + +); + +Example.parameters = { + docs: { + description: { + story: + 'Style each breadcrumb sub-component individually via `useStyles`, reading from state to apply conditional styles (e.g. bold + non-clickable for the current item). The `state.current` flag on `BreadcrumbButton` drives the visual distinction between navigable links and the active page. Use the same approach with CSS modules or a CSS-in-JS library by setting `state.root.className` instead of `state.root.style`.', + }, + }, +}; diff --git a/packages/react-components/react-base-components/stories/src/Breadcrumb/index.stories.tsx b/packages/react-components/react-base-components/stories/src/Breadcrumb/index.stories.tsx new file mode 100644 index 0000000000000..e8c0993f08ba6 --- /dev/null +++ b/packages/react-components/react-base-components/stories/src/Breadcrumb/index.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { BreadcrumbProps } from '@fluentui/react-base-components'; + +export { Example } from './ExampleBreadcrumb.stories'; + +const meta: Meta = { + title: 'Base Components/Breadcrumb', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Breadcrumb components (`Breadcrumb`, `BreadcrumbItem`, `BreadcrumbDivider`, `BreadcrumbButton`) for building custom navigation trails. Compose each sub-component individually using `useBreadcrumb*` hooks with their render functions. The base hooks produce a semantic `