From 24f85f25b3bf7c605c381a1f386c4f691e3dff95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 1 Sep 2023 10:59:59 +0200 Subject: [PATCH 001/136] React Components V9: List - initial commit --- .github/CODEOWNERS | 1 + .../react-list-preview/.babelrc.json | 4 ++ .../react-list-preview/.eslintrc.json | 4 ++ .../react-list-preview/.npmignore | 38 +++++++++++ .../react-list-preview/.storybook/main.js | 14 ++++ .../react-list-preview/.storybook/preview.js | 7 ++ .../.storybook/tsconfig.json | 10 +++ .../react-list-preview/.swcrc | 30 ++++++++ .../react-list-preview/LICENSE | 15 ++++ .../react-list-preview/README.md | 5 ++ .../config/api-extractor.json | 4 ++ .../react-list-preview/config/tests.js | 1 + .../react-list-preview/docs/Spec.md | 63 +++++++++++++++++ .../etc/react-list-preview.api.md | 68 +++++++++++++++++++ .../react-list-preview/jest.config.js | 21 ++++++ .../react-list-preview/just.config.ts | 5 ++ .../react-list-preview/package.json | 61 +++++++++++++++++ .../react-list-preview/project.json | 8 +++ .../react-list-preview/src/List.ts | 1 + .../react-list-preview/src/ListItem.ts | 1 + .../src/components/List/List.test.tsx | 18 +++++ .../src/components/List/List.tsx | 18 +++++ .../src/components/List/List.types.ts | 17 +++++ .../List/__snapshots__/List.test.tsx.snap | 11 +++ .../src/components/List/index.ts | 5 ++ .../src/components/List/renderList.tsx | 16 +++++ .../src/components/List/useList.ts | 31 +++++++++ .../components/List/useListStyles.styles.ts | 33 +++++++++ .../src/components/ListItem/ListItem.test.tsx | 18 +++++ .../src/components/ListItem/ListItem.tsx | 18 +++++ .../src/components/ListItem/ListItem.types.ts | 17 +++++ .../__snapshots__/ListItem.test.tsx.snap | 11 +++ .../src/components/ListItem/index.ts | 5 ++ .../components/ListItem/renderListItem.tsx | 16 +++++ .../src/components/ListItem/useListItem.ts | 31 +++++++++ .../ListItem/useListItemStyles.styles.ts | 33 +++++++++ .../react-list-preview/src/index.ts | 10 +++ .../src/testing/isConformant.ts | 15 ++++ .../react-list-preview/stories/.gitkeep | 0 .../stories/List/ListBestPractices.md | 5 ++ .../stories/List/ListDefault.stories.tsx | 4 ++ .../stories/List/ListDescription.md | 0 .../stories/List/index.stories.tsx | 18 +++++ .../stories/ListItem/ListItemBestPractices.md | 5 ++ .../ListItem/ListItemDefault.stories.tsx | 4 ++ .../stories/ListItem/ListItemDescription.md | 0 .../stories/ListItem/index.stories.tsx | 18 +++++ .../react-list-preview/tsconfig.json | 25 +++++++ .../react-list-preview/tsconfig.lib.json | 22 ++++++ .../react-list-preview/tsconfig.spec.json | 17 +++++ tsconfig.base.all.json | 3 +- tsconfig.base.json | 1 + 52 files changed, 805 insertions(+), 1 deletion(-) create mode 100644 packages/react-components/react-list-preview/.babelrc.json create mode 100644 packages/react-components/react-list-preview/.eslintrc.json create mode 100644 packages/react-components/react-list-preview/.npmignore create mode 100644 packages/react-components/react-list-preview/.storybook/main.js create mode 100644 packages/react-components/react-list-preview/.storybook/preview.js create mode 100644 packages/react-components/react-list-preview/.storybook/tsconfig.json create mode 100644 packages/react-components/react-list-preview/.swcrc create mode 100644 packages/react-components/react-list-preview/LICENSE create mode 100644 packages/react-components/react-list-preview/README.md create mode 100644 packages/react-components/react-list-preview/config/api-extractor.json create mode 100644 packages/react-components/react-list-preview/config/tests.js create mode 100644 packages/react-components/react-list-preview/docs/Spec.md create mode 100644 packages/react-components/react-list-preview/etc/react-list-preview.api.md create mode 100644 packages/react-components/react-list-preview/jest.config.js create mode 100644 packages/react-components/react-list-preview/just.config.ts create mode 100644 packages/react-components/react-list-preview/package.json create mode 100644 packages/react-components/react-list-preview/project.json create mode 100644 packages/react-components/react-list-preview/src/List.ts create mode 100644 packages/react-components/react-list-preview/src/ListItem.ts create mode 100644 packages/react-components/react-list-preview/src/components/List/List.test.tsx create mode 100644 packages/react-components/react-list-preview/src/components/List/List.tsx create mode 100644 packages/react-components/react-list-preview/src/components/List/List.types.ts create mode 100644 packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap create mode 100644 packages/react-components/react-list-preview/src/components/List/index.ts create mode 100644 packages/react-components/react-list-preview/src/components/List/renderList.tsx create mode 100644 packages/react-components/react-list-preview/src/components/List/useList.ts create mode 100644 packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/index.ts create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts create mode 100644 packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts create mode 100644 packages/react-components/react-list-preview/src/index.ts create mode 100644 packages/react-components/react-list-preview/src/testing/isConformant.ts create mode 100644 packages/react-components/react-list-preview/stories/.gitkeep create mode 100644 packages/react-components/react-list-preview/stories/List/ListBestPractices.md create mode 100644 packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListDescription.md create mode 100644 packages/react-components/react-list-preview/stories/List/index.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md create mode 100644 packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/ListItem/ListItemDescription.md create mode 100644 packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx create mode 100644 packages/react-components/react-list-preview/tsconfig.json create mode 100644 packages/react-components/react-list-preview/tsconfig.lib.json create mode 100644 packages/react-components/react-list-preview/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cb7af7f60a7e0b..e62e40894de759 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -241,6 +241,7 @@ packages/react-components/react-search-preview @microsoft/cxe-coastal packages/react-components/react-colorpicker-compat @microsoft/cxe-red @sopranopillow packages/react-components/react-nav-preview @microsoft/cxe-red @mltejera packages/react-components/react-motion-preview @microsoft/cxe-prg @marcosmoura +packages/react-components/react-list-preview @microsoft/teams-prg # <%= NX-CODEOWNER-PLACEHOLDER %> ## Components diff --git a/packages/react-components/react-list-preview/.babelrc.json b/packages/react-components/react-list-preview/.babelrc.json new file mode 100644 index 00000000000000..45fb71ca16d2c3 --- /dev/null +++ b/packages/react-components/react-list-preview/.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-list-preview/.eslintrc.json b/packages/react-components/react-list-preview/.eslintrc.json new file mode 100644 index 00000000000000..ceea884c70dccc --- /dev/null +++ b/packages/react-components/react-list-preview/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["plugin:@fluentui/eslint-plugin/react"], + "root": true +} diff --git a/packages/react-components/react-list-preview/.npmignore b/packages/react-components/react-list-preview/.npmignore new file mode 100644 index 00000000000000..a5817be2414dec --- /dev/null +++ b/packages/react-components/react-list-preview/.npmignore @@ -0,0 +1,38 @@ +.storybook/ +.vscode/ +bundle-size/ +config/ +coverage/ +docs/ +etc/ +node_modules/ +src/ +stories/ +dist/types/ +temp/ +__fixtures__ +__mocks__ +__tests__ + +*.api.json +*.log +*.spec.* +*.cy.* +*.test.* +*.yml + +# config files +*config.* +*rc.* +.editorconfig +.eslint* +.git* +.prettierignore +.swcrc +project.json + +# exclude gitignore patterns explicitly +!lib +!lib-commonjs +!lib-amd +!dist/*.d.ts diff --git a/packages/react-components/react-list-preview/.storybook/main.js b/packages/react-components/react-list-preview/.storybook/main.js new file mode 100644 index 00000000000000..26536b61b387f6 --- /dev/null +++ b/packages/react-components/react-list-preview/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../stories/**/*.stories.mdx', '../stories/**/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-list-preview/.storybook/preview.js b/packages/react-components/react-list-preview/.storybook/preview.js new file mode 100644 index 00000000000000..1939500a3d18c7 --- /dev/null +++ b/packages/react-components/react-list-preview/.storybook/preview.js @@ -0,0 +1,7 @@ +import * as rootPreview from '../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; diff --git a/packages/react-components/react-list-preview/.storybook/tsconfig.json b/packages/react-components/react-list-preview/.storybook/tsconfig.json new file mode 100644 index 00000000000000..ea89218a3d916f --- /dev/null +++ b/packages/react-components/react-list-preview/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment", "storybook__addons"] + }, + "include": ["../stories/**/*.stories.ts", "../stories/**/*.stories.tsx", "*.js"] +} diff --git a/packages/react-components/react-list-preview/.swcrc b/packages/react-components/react-list-preview/.swcrc new file mode 100644 index 00000000000000..b4ffa86dee3067 --- /dev/null +++ b/packages/react-components/react-list-preview/.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-list-preview/LICENSE b/packages/react-components/react-list-preview/LICENSE new file mode 100644 index 00000000000000..1a85f39e0650b8 --- /dev/null +++ b/packages/react-components/react-list-preview/LICENSE @@ -0,0 +1,15 @@ +@fluentui/react-list-preview + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Note: Usage of the fonts and icons referenced in Fluent UI React is subject to the terms listed at https://aka.ms/fluentui-assets-license diff --git a/packages/react-components/react-list-preview/README.md b/packages/react-components/react-list-preview/README.md new file mode 100644 index 00000000000000..839279aca3d3b6 --- /dev/null +++ b/packages/react-components/react-list-preview/README.md @@ -0,0 +1,5 @@ +# @fluentui/react-list-preview + +**React List 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-list-preview/config/api-extractor.json b/packages/react-components/react-list-preview/config/api-extractor.json new file mode 100644 index 00000000000000..e533bf30b48a2b --- /dev/null +++ b/packages/react-components/react-list-preview/config/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$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" +} diff --git a/packages/react-components/react-list-preview/config/tests.js b/packages/react-components/react-list-preview/config/tests.js new file mode 100644 index 00000000000000..2e211ae9e21420 --- /dev/null +++ b/packages/react-components/react-list-preview/config/tests.js @@ -0,0 +1 @@ +/** Jest test setup file. */ diff --git a/packages/react-components/react-list-preview/docs/Spec.md b/packages/react-components/react-list-preview/docs/Spec.md new file mode 100644 index 00000000000000..a3a755a6aa0de6 --- /dev/null +++ b/packages/react-components/react-list-preview/docs/Spec.md @@ -0,0 +1,63 @@ +# @fluentui/react-list-preview 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-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md new file mode 100644 index 00000000000000..234a14766e474a --- /dev/null +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -0,0 +1,68 @@ +## API Report File for "@fluentui/react-list-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import * as React_2 from 'react'; +import type { Slot } from '@fluentui/react-utilities'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +// @public +export const List: ForwardRefComponent; + +// @public (undocumented) +export const listClassNames: SlotClassNames; + +// @public +export const ListItem: ForwardRefComponent; + +// @public (undocumented) +export const listItemClassNames: SlotClassNames; + +// @public +export type ListItemProps = ComponentProps & {}; + +// @public (undocumented) +export type ListItemSlots = { + root: Slot<'div'>; +}; + +// @public +export type ListItemState = ComponentState; + +// @public +export type ListProps = ComponentProps & {}; + +// @public (undocumented) +export type ListSlots = { + root: Slot<'div'>; +}; + +// @public +export type ListState = ComponentState; + +// @public +export const renderList_unstable: (state: ListState) => JSX.Element; + +// @public +export const renderListItem_unstable: (state: ListItemState) => JSX.Element; + +// @public +export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; + +// @public +export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref) => ListItemState; + +// @public +export const useListItemStyles_unstable: (state: ListItemState) => ListItemState; + +// @public +export const useListStyles_unstable: (state: ListState) => ListState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-list-preview/jest.config.js b/packages/react-components/react-list-preview/jest.config.js new file mode 100644 index 00000000000000..49a746ffcd03ee --- /dev/null +++ b/packages/react-components/react-list-preview/jest.config.js @@ -0,0 +1,21 @@ +// @ts-check + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-list-preview', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + isolatedModules: true, + }, + ], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-list-preview/just.config.ts b/packages/react-components/react-list-preview/just.config.ts new file mode 100644 index 00000000000000..b7b2c9a33bf435 --- /dev/null +++ b/packages/react-components/react-list-preview/just.config.ts @@ -0,0 +1,5 @@ +import { preset, task } from '@fluentui/scripts-tasks'; + +preset(); + +task('build', 'build:react-components').cached?.(); diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json new file mode 100644 index 00000000000000..c499751eada481 --- /dev/null +++ b/packages/react-components/react-list-preview/package.json @@ -0,0 +1,61 @@ +{ + "name": "@fluentui/react-list-preview", + "version": "0.0.0", + "private": true, + "description": "New fluentui react package", + "main": "lib-commonjs/index.js", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "scripts": { + "build": "just-scripts build", + "clean": "just-scripts clean", + "generate-api": "just-scripts generate-api", + "lint": "just-scripts lint", + "start": "yarn storybook", + "storybook": "start-storybook", + "test": "jest --passWithNoTests", + "test-ssr": "test-ssr \"./stories/**/*.stories.tsx\"", + "type-check": "tsc -b tsconfig.json" + }, + "devDependencies": { + "@fluentui/eslint-plugin": "*", + "@fluentui/react-conformance": "*", + "@fluentui/react-conformance-griffel": "*", + "@fluentui/scripts-api-extractor": "*", + "@fluentui/scripts-tasks": "*" + }, + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.3", + "@fluentui/react-theme": "^9.1.11", + "@fluentui/react-utilities": "^9.13.0", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.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-list-preview/project.json b/packages/react-components/react-list-preview/project.json new file mode 100644 index 00000000000000..47497ab9de6df0 --- /dev/null +++ b/packages/react-components/react-list-preview/project.json @@ -0,0 +1,8 @@ +{ + "name": "@fluentui/react-list-preview", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-list-preview/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-list-preview/src/List.ts b/packages/react-components/react-list-preview/src/List.ts new file mode 100644 index 00000000000000..038abd3e740ea4 --- /dev/null +++ b/packages/react-components/react-list-preview/src/List.ts @@ -0,0 +1 @@ +export * from './components/List/index'; diff --git a/packages/react-components/react-list-preview/src/ListItem.ts b/packages/react-components/react-list-preview/src/ListItem.ts new file mode 100644 index 00000000000000..d846474af071ad --- /dev/null +++ b/packages/react-components/react-list-preview/src/ListItem.ts @@ -0,0 +1 @@ +export * from './components/ListItem/index'; diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx new file mode 100644 index 00000000000000..91866c402c08d3 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { List } from './List'; + +describe('List', () => { + isConformant({ + Component: List, + displayName: 'List', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default List); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-list-preview/src/components/List/List.tsx b/packages/react-components/react-list-preview/src/components/List/List.tsx new file mode 100644 index 00000000000000..02858c786657e2 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/List.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useList_unstable } from './useList'; +import { renderList_unstable } from './renderList'; +import { useListStyles_unstable } from './useListStyles.styles'; +import type { ListProps } from './List.types'; + +/** + * List component - TODO: add more docs + */ +export const List: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useList_unstable(props, ref); + + useListStyles_unstable(state); + return renderList_unstable(state); +}); + +List.displayName = 'List'; diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts new file mode 100644 index 00000000000000..2bd3e77ab99a3b --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type ListSlots = { + root: Slot<'div'>; +}; + +/** + * List Props + */ +export type ListProps = ComponentProps & {}; + +/** + * State used in rendering List + */ +export type ListState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ListProps. +// & Required> diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap new file mode 100644 index 00000000000000..c7e02b09599b43 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`List renders a default state 1`] = ` +
+
+ Default List +
+
+`; diff --git a/packages/react-components/react-list-preview/src/components/List/index.ts b/packages/react-components/react-list-preview/src/components/List/index.ts new file mode 100644 index 00000000000000..edd6a0bf1be045 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/index.ts @@ -0,0 +1,5 @@ +export * from './List'; +export * from './List.types'; +export * from './renderList'; +export * from './useList'; +export * from './useListStyles.styles'; diff --git a/packages/react-components/react-list-preview/src/components/List/renderList.tsx b/packages/react-components/react-list-preview/src/components/List/renderList.tsx new file mode 100644 index 00000000000000..543dd7aed9b4b1 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/renderList.tsx @@ -0,0 +1,16 @@ +/** @jsxRuntime classic */ +/** @jsx createElement */ + +import { createElement } from '@fluentui/react-jsx-runtime'; +import { assertSlots } from '@fluentui/react-utilities'; +import type { ListState, ListSlots } from './List.types'; + +/** + * Render the final JSX of List + */ +export const renderList_unstable = (state: ListState) => { + assertSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts new file mode 100644 index 00000000000000..6fbef958213605 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import type { ListProps, ListState } from './List.types'; + +/** + * Create the state required to render List. + * + * The returned state can be modified with hooks such as useListStyles_unstable, + * before being passed to renderList_unstable. + * + * @param props - props from this instance of List + * @param ref - reference to root HTMLElement of List + */ +export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: slot.always( + getNativeElementProps('div', { + ref, + ...props, + }), + { elementType: 'div' }, + ), + }; +}; diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts new file mode 100644 index 00000000000000..19986555d581bc --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -0,0 +1,33 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { ListSlots, ListState } from './List.types'; + +export const listClassNames: SlotClassNames = { + root: 'fui-List', + // TODO: add class names for all slots on ListSlots. + // Should be of the form `: 'fui-List__` +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots +}); + +/** + * Apply styling to the List slots based on the state + */ +export const useListStyles_unstable = (state: ListState): ListState => { + const styles = useStyles(); + state.root.className = mergeClasses(listClassNames.root, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx new file mode 100644 index 00000000000000..0b23f1dc97ad24 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { ListItem } from './ListItem'; + +describe('ListItem', () => { + isConformant({ + Component: ListItem, + displayName: 'ListItem', + }); + + // TODO add more tests here, and create visual regression tests in /apps/vr-tests + + it('renders a default state', () => { + const result = render(Default ListItem); + expect(result.container).toMatchSnapshot(); + }); +}); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx new file mode 100644 index 00000000000000..0fa07df09361b9 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useListItem_unstable } from './useListItem'; +import { renderListItem_unstable } from './renderListItem'; +import { useListItemStyles_unstable } from './useListItemStyles.styles'; +import type { ListItemProps } from './ListItem.types'; + +/** + * ListItem component - TODO: add more docs + */ +export const ListItem: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useListItem_unstable(props, ref); + + useListItemStyles_unstable(state); + return renderListItem_unstable(state); +}); + +ListItem.displayName = 'ListItem'; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts new file mode 100644 index 00000000000000..aa6539c260648a --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -0,0 +1,17 @@ +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +export type ListItemSlots = { + root: Slot<'div'>; +}; + +/** + * ListItem Props + */ +export type ListItemProps = ComponentProps & {}; + +/** + * State used in rendering ListItem + */ +export type ListItemState = ComponentState; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ListItemProps. +// & Required> diff --git a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap new file mode 100644 index 00000000000000..21a24d5f673ba6 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ListItem renders a default state 1`] = ` +
+
+ Default ListItem +
+
+`; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/index.ts b/packages/react-components/react-list-preview/src/components/ListItem/index.ts new file mode 100644 index 00000000000000..08dc684b19814a --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/index.ts @@ -0,0 +1,5 @@ +export * from './ListItem'; +export * from './ListItem.types'; +export * from './renderListItem'; +export * from './useListItem'; +export * from './useListItemStyles.styles'; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx new file mode 100644 index 00000000000000..af252ab5f79f5b --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx @@ -0,0 +1,16 @@ +/** @jsxRuntime classic */ +/** @jsx createElement */ + +import { createElement } from '@fluentui/react-jsx-runtime'; +import { assertSlots } from '@fluentui/react-utilities'; +import type { ListItemState, ListItemSlots } from './ListItem.types'; + +/** + * Render the final JSX of ListItem + */ +export const renderListItem_unstable = (state: ListItemState) => { + assertSlots(state); + + // TODO Add additional slots in the appropriate place + return ; +}; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts new file mode 100644 index 00000000000000..3d3d8aaa870efb --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import type { ListItemProps, ListItemState } from './ListItem.types'; + +/** + * Create the state required to render ListItem. + * + * The returned state can be modified with hooks such as useListItemStyles_unstable, + * before being passed to renderListItem_unstable. + * + * @param props - props from this instance of ListItem + * @param ref - reference to root HTMLElement of ListItem + */ +export const useListItem_unstable = (props: ListItemProps, ref: React.Ref): ListItemState => { + return { + // TODO add appropriate props/defaults + components: { + // TODO add each slot's element type or component + root: 'div', + }, + // TODO add appropriate slots, for example: + // mySlot: resolveShorthand(props.mySlot), + root: slot.always( + getNativeElementProps('div', { + ref, + ...props, + }), + { elementType: 'div' }, + ), + }; +}; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts new file mode 100644 index 00000000000000..302e8cfffeae16 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -0,0 +1,33 @@ +import { makeStyles, mergeClasses } from '@griffel/react'; +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { ListItemSlots, ListItemState } from './ListItem.types'; + +export const listItemClassNames: SlotClassNames = { + root: 'fui-ListItem', + // TODO: add class names for all slots on ListItemSlots. + // Should be of the form `: 'fui-ListItem__` +}; + +/** + * Styles for the root slot + */ +const useStyles = makeStyles({ + root: { + // TODO Add default styles for the root element + }, + + // TODO add additional classes for different states and/or slots +}); + +/** + * Apply styling to the ListItem slots based on the state + */ +export const useListItemStyles_unstable = (state: ListItemState): ListItemState => { + const styles = useStyles(); + state.root.className = mergeClasses(listItemClassNames.root, styles.root, state.root.className); + + // TODO Add class names to slots, for example: + // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + + return state; +}; diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts new file mode 100644 index 00000000000000..a938d39dcb44a8 --- /dev/null +++ b/packages/react-components/react-list-preview/src/index.ts @@ -0,0 +1,10 @@ +export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List'; +export type { ListProps, ListSlots, ListState } from './List'; +export { + ListItem, + listItemClassNames, + renderListItem_unstable, + useListItemStyles_unstable, + useListItem_unstable, +} from './ListItem'; +export type { ListItemProps, ListItemSlots, ListItemState } from './ListItem'; diff --git a/packages/react-components/react-list-preview/src/testing/isConformant.ts b/packages/react-components/react-list-preview/src/testing/isConformant.ts new file mode 100644 index 00000000000000..a3d988f29a1728 --- /dev/null +++ b/packages/react-components/react-list-preview/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 }, +) { + 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-list-preview/stories/.gitkeep b/packages/react-components/react-list-preview/stories/.gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md new file mode 100644 index 00000000000000..08ff8ddeeb5f86 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md @@ -0,0 +1,5 @@ +## Best practices + +### Do + +### Don't diff --git a/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx new file mode 100644 index 00000000000000..0169f28c0a3808 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx @@ -0,0 +1,4 @@ +import * as React from 'react'; +import { List, ListProps } from '@fluentui/react-list-preview'; + +export const Default = (props: Partial) => ; diff --git a/packages/react-components/react-list-preview/stories/List/ListDescription.md b/packages/react-components/react-list-preview/stories/List/ListDescription.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx new file mode 100644 index 00000000000000..bda361cec97ed7 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -0,0 +1,18 @@ +import { List } from '@fluentui/react-list-preview'; + +import descriptionMd from './ListDescription.md'; +import bestPracticesMd from './ListBestPractices.md'; + +export { Default } from './ListDefault.stories'; + +export default { + title: 'Preview Components/List', + component: List, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\n'), + }, + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md b/packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md new file mode 100644 index 00000000000000..08ff8ddeeb5f86 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md @@ -0,0 +1,5 @@ +## Best practices + +### Do + +### Don't diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx b/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx new file mode 100644 index 00000000000000..e38a36d8cbcfcb --- /dev/null +++ b/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx @@ -0,0 +1,4 @@ +import * as React from 'react'; +import { ListItem, ListItemProps } from '@fluentui/react-list-preview'; + +export const Default = (props: Partial) => ; diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemDescription.md b/packages/react-components/react-list-preview/stories/ListItem/ListItemDescription.md new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx b/packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx new file mode 100644 index 00000000000000..fdd4db71031fff --- /dev/null +++ b/packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx @@ -0,0 +1,18 @@ +import { ListItem } from '@fluentui/react-list-preview'; + +import descriptionMd from './ListItemDescription.md'; +import bestPracticesMd from './ListItemBestPractices.md'; + +export { Default } from './ListItemDefault.stories'; + +export default { + title: 'Preview Components/ListItem', + component: ListItem, + parameters: { + docs: { + description: { + component: [descriptionMd, bestPracticesMd].join('\n'), + }, + }, + }, +}; diff --git a/packages/react-components/react-list-preview/tsconfig.json b/packages/react-components/react-list-preview/tsconfig.json new file mode 100644 index 00000000000000..1941a041d46c19 --- /dev/null +++ b/packages/react-components/react-list-preview/tsconfig.json @@ -0,0 +1,25 @@ +{ + "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" + }, + { + "path": "./.storybook/tsconfig.json" + } + ] +} diff --git a/packages/react-components/react-list-preview/tsconfig.lib.json b/packages/react-components/react-list-preview/tsconfig.lib.json new file mode 100644 index 00000000000000..6f90cf95c005bd --- /dev/null +++ b/packages/react-components/react-list-preview/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-list-preview/tsconfig.spec.json b/packages/react-components/react-list-preview/tsconfig.spec.json new file mode 100644 index 00000000000000..911456fe4b4d91 --- /dev/null +++ b/packages/react-components/react-list-preview/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index 257989ebd92fd0..0e057a5c54ac79 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -161,7 +161,8 @@ "@fluentui/react-virtualizer": ["packages/react-components/react-virtualizer/src/index.ts"], "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], "@fluentui/tokens": ["packages/tokens/src/index.ts"], - "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] + "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"], + "@fluentui/react-list-preview": ["packages/react-components/react-list-preview/src/index.ts"] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index a651e6c671d559..902989029496ac 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -53,6 +53,7 @@ "@fluentui/react-jsx-runtime/jsx-runtime": ["packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], "@fluentui/react-label": ["packages/react-components/react-label/src/index.ts"], "@fluentui/react-link": ["packages/react-components/react-link/src/index.ts"], + "@fluentui/react-list-preview": ["packages/react-components/react-list-preview/src/index.ts"], "@fluentui/react-menu": ["packages/react-components/react-menu/src/index.ts"], "@fluentui/react-migration-v0-v9": ["packages/react-components/react-migration-v0-v9/src/index.ts"], "@fluentui/react-migration-v8-v9": ["packages/react-components/react-migration-v8-v9/src/index.ts"], From 33ba3aa87625d0e2ccc8a550e9cf402dd2e5142e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 19 Sep 2023 10:35:29 +0200 Subject: [PATCH 002/136] list wip, selection works --- .../react-list-preview/package.json | 2 + .../src/components/List/List.tsx | 4 +- .../src/components/List/List.types.ts | 28 ++- .../src/components/List/listContext.ts | 11 + .../src/components/List/renderList.tsx | 12 +- .../src/components/List/useList.ts | 26 ++- .../components/List/useListContextValues.ts | 17 ++ .../components/List/useListStyles.styles.ts | 24 ++- .../src/components/ListItem/ListItem.types.ts | 4 +- .../components/ListItem/renderListItem.tsx | 4 +- .../src/components/ListItem/useListItem.ts | 45 ++-- .../ListItem/useListItemStyles.styles.ts | 28 ++- .../react-list-preview/src/hooks/index.ts | 2 + .../react-list-preview/src/hooks/types.ts | 21 ++ .../src/hooks/useListFeatures.ts | 23 ++ .../src/hooks/useListSelection.tsx | 56 +++++ .../react-list-preview/src/index.ts | 12 +- .../List/ListArrowNavigation.stories.tsx | 9 + .../stories/List/ListBestPractices.md | 7 + .../stories/List/ListDefault.stories.tsx | 14 +- .../stories/List/ListDescription.md | 1 + .../stories/List/ListGrid.stories.tsx | 24 +++ .../stories/List/ListHorizontal.stories.tsx | 24 +++ .../stories/List/ListOverflowing.stories.tsx | 84 ++++++++ .../stories/List/ListSelection.stories.tsx | 62 ++++++ .../List/ListWithMultipleActions.stories.tsx | 48 +++++ .../stories/List/VirtualizedList.stories.tsx | 30 +++ .../stories/List/countries.js | 198 ++++++++++++++++++ .../stories/List/index.stories.tsx | 7 + .../react-list-preview/stories/List/names.js | 54 +++++ 30 files changed, 839 insertions(+), 42 deletions(-) create mode 100644 packages/react-components/react-list-preview/src/components/List/listContext.ts create mode 100644 packages/react-components/react-list-preview/src/components/List/useListContextValues.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/index.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/types.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/useListFeatures.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/useListSelection.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListArrowNavigation.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/countries.js create mode 100644 packages/react-components/react-list-preview/stories/List/names.js diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index c499751eada481..5d6dd748e2553f 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -33,7 +33,9 @@ "dependencies": { "@fluentui/react-jsx-runtime": "^9.0.3", "@fluentui/react-theme": "^9.1.11", + "@fluentui/react-tabster": "^9.12.5", "@fluentui/react-utilities": "^9.13.0", + "@fluentui/react-button": "^9.3.34", "@griffel/react": "^1.5.14", "@swc/helpers": "^0.5.1" }, diff --git a/packages/react-components/react-list-preview/src/components/List/List.tsx b/packages/react-components/react-list-preview/src/components/List/List.tsx index 02858c786657e2..f90b13838d1d3c 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.tsx @@ -4,15 +4,17 @@ import { useList_unstable } from './useList'; import { renderList_unstable } from './renderList'; import { useListStyles_unstable } from './useListStyles.styles'; import type { ListProps } from './List.types'; +import { useListContextValues_unstable } from './useListContextValues'; /** * List component - TODO: add more docs */ export const List: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useList_unstable(props, ref); + const listContext = useListContextValues_unstable(state); useListStyles_unstable(state); - return renderList_unstable(state); + return renderList_unstable(state, listContext); }); List.displayName = 'List'; diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 2bd3e77ab99a3b..c330bbb95f6396 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,17 +1,35 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; + +export enum ListLayout { + Horizontal = 'horizontal', + Vertical = 'vertical', + Grid = 'grid', +} export type ListSlots = { - root: Slot<'div'>; + root: NonNullable>; }; /** * List Props */ -export type ListProps = ComponentProps & {}; +export type ListProps = ComponentProps & { + layout?: ListLayout; + customArrowNavigationOptions?: Partial; + focusable?: boolean; + focusableItems?: boolean; +}; + +export type ListContextValue = { + focusableItems: boolean; +}; + +export type ListContextValues = { + listContext: ListContextValue; +}; /** * State used in rendering List */ -export type ListState = ComponentState; -// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ListProps. -// & Required> +export type ListState = ComponentState & Required> & ListContextValue; diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts new file mode 100644 index 00000000000000..9d5ee08edbe528 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { ListContextValue } from './List.types'; + +const listContext = React.createContext(undefined); + +export const listContextDefaultValue: ListContextValue = { + focusableItems: false, +}; + +export const ListContextProvider = listContext.Provider; +export const useListContext = () => React.useContext(listContext) ?? listContextDefaultValue; diff --git a/packages/react-components/react-list-preview/src/components/List/renderList.tsx b/packages/react-components/react-list-preview/src/components/List/renderList.tsx index 543dd7aed9b4b1..a3e00050b29c5e 100644 --- a/packages/react-components/react-list-preview/src/components/List/renderList.tsx +++ b/packages/react-components/react-list-preview/src/components/List/renderList.tsx @@ -3,14 +3,18 @@ import { createElement } from '@fluentui/react-jsx-runtime'; import { assertSlots } from '@fluentui/react-utilities'; -import type { ListState, ListSlots } from './List.types'; +import type { ListState, ListSlots, ListContextValues } from './List.types'; +import { ListContextProvider } from './listContext'; /** * Render the final JSX of List */ -export const renderList_unstable = (state: ListState) => { +export const renderList_unstable = (state: ListState, contextValues: ListContextValues) => { assertSlots(state); - // TODO Add additional slots in the appropriate place - return ; + return ( + + + + ); }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 6fbef958213605..d17046ea25ab03 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { getNativeElementProps, slot } from '@fluentui/react-utilities'; -import type { ListProps, ListState } from './List.types'; +import { useArrowNavigationGroup } from '@fluentui/react-tabster'; +import { ListLayout, ListProps, ListState } from './List.types'; /** * Create the state required to render List. @@ -12,20 +13,35 @@ import type { ListProps, ListState } from './List.types'; * @param ref - reference to root HTMLElement of List */ export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { + const { + layout = ListLayout.Vertical, + focusableItems = false, + focusable = false, + customArrowNavigationOptions, + } = props; + + const arrowNavigationAttributes = useArrowNavigationGroup({ + axis: layout === ListLayout.Grid ? 'grid-linear' : 'both', + memorizeCurrent: true, + ...(customArrowNavigationOptions || {}), + }); + return { - // TODO add appropriate props/defaults components: { - // TODO add each slot's element type or component root: 'div', }, - // TODO add appropriate slots, for example: - // mySlot: resolveShorthand(props.mySlot), root: slot.always( getNativeElementProps('div', { ref, + role: 'list', + tabIndex: focusable ? 0 : undefined, + ...arrowNavigationAttributes, ...props, }), { elementType: 'div' }, ), + layout, + // context: + focusableItems, }; }; diff --git a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts new file mode 100644 index 00000000000000..e9f1341fba7592 --- /dev/null +++ b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { ListContextValues, ListState } from './List.types'; + +export function useListContextValues_unstable(state: ListState): ListContextValues { + const { focusableItems } = state; + + const listContext = React.useMemo( + () => ({ + focusableItems, + }), + [focusableItems], + ); + + return { + listContext, + }; +} diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts index 19986555d581bc..299daf12d1da3c 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -1,6 +1,6 @@ import { makeStyles, mergeClasses } from '@griffel/react'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import type { ListSlots, ListState } from './List.types'; +import { ListLayout, ListOrientation, ListSlots, ListState } from './List.types'; export const listClassNames: SlotClassNames = { root: 'fui-List', @@ -16,6 +16,14 @@ const useStyles = makeStyles({ // TODO Add default styles for the root element }, + rootHorizontal: { + display: 'flex', + }, + + rootGrid: { + display: 'grid', + }, + // TODO add additional classes for different states and/or slots }); @@ -24,7 +32,19 @@ const useStyles = makeStyles({ */ export const useListStyles_unstable = (state: ListState): ListState => { const styles = useStyles(); - state.root.className = mergeClasses(listClassNames.root, styles.root, state.root.className); + + const layoutToStyles = { + [ListLayout.Vertical]: styles.root, + [ListLayout.Horizontal]: styles.rootHorizontal, + [ListLayout.Grid]: styles.rootGrid, + }; + + state.root.className = mergeClasses( + listClassNames.root, + styles.root, + layoutToStyles[state.layout], + state.root.className, + ); // TODO Add class names to slots, for example: // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index aa6539c260648a..ea72cf1e354bcf 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -1,7 +1,9 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { Button } from '@fluentui/react-button'; export type ListItemSlots = { - root: Slot<'div'>; + root: NonNullable>; + button?: Slot; }; /** diff --git a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx index af252ab5f79f5b..3d6d965bbc34b0 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx @@ -12,5 +12,7 @@ export const renderListItem_unstable = (state: ListItemState) => { assertSlots(state); // TODO Add additional slots in the appropriate place - return ; + return ( + {state.button ? {state.root.children} : state.root.children} + ); }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts index 3d3d8aaa870efb..1d1df04f69dff7 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -1,6 +1,9 @@ import * as React from 'react'; +import { useFocusableGroup } from '@fluentui/react-tabster'; import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import { Button } from '@fluentui/react-button'; import type { ListItemProps, ListItemState } from './ListItem.types'; +import { useListContext } from '../List/listContext'; /** * Create the state required to render ListItem. @@ -12,20 +15,38 @@ import type { ListItemProps, ListItemState } from './ListItem.types'; * @param ref - reference to root HTMLElement of ListItem */ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref): ListItemState => { - return { - // TODO add appropriate props/defaults + const { focusableItems } = useListContext(); + const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); + + const root = slot.always( + getNativeElementProps('div', { + ref, + role: 'listitem', + tabIndex: focusableItems ? 0 : undefined, + ...focusableGroupAttrs, + ...props, + }), + { elementType: 'div' }, + ); + + const button = slot.optional(props.button, { elementType: Button, defaultProps: { appearance: 'transparent' } }); + + const state: ListItemState = { components: { - // TODO add each slot's element type or component root: 'div', + button: Button, }, - // TODO add appropriate slots, for example: - // mySlot: resolveShorthand(props.mySlot), - root: slot.always( - getNativeElementProps('div', { - ref, - ...props, - }), - { elementType: 'div' }, - ), + root, + button, }; + + // move the aria labels from root to button, because we don't want the voice over to focus on the list item itself + if (button && !button['aria-label'] && !button['aria-labelledby']) { + button['aria-label'] = root['aria-label']; + button['aria-labelledby'] = root['aria-labelledby']; + root['aria-label'] = undefined; + root['aria-labelledby'] = undefined; + } + + return state; }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 302e8cfffeae16..2b3384bef8815c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -1,11 +1,12 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; import type { ListItemSlots, ListItemState } from './ListItem.types'; +import { tokens } from '@fluentui/react-theme'; export const listItemClassNames: SlotClassNames = { root: 'fui-ListItem', - // TODO: add class names for all slots on ListItemSlots. - // Should be of the form `: 'fui-ListItem__` + button: 'fui-ListItem__button', }; /** @@ -13,10 +14,20 @@ export const listItemClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - // TODO Add default styles for the root element + ...createCustomFocusIndicatorStyle( + { + ...shorthands.outline('2px', 'solid', tokens.colorStrokeFocus2), + ...shorthands.borderRadius(tokens.borderRadiusMedium), + }, + { selector: 'focus' }, + ), + }, + button: { + display: 'block', + width: '100%', + textAlign: 'left', + ...shorthands.padding(0), }, - - // TODO add additional classes for different states and/or slots }); /** @@ -26,8 +37,9 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState const styles = useStyles(); state.root.className = mergeClasses(listItemClassNames.root, styles.root, state.root.className); - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); + if (state.button) { + state.button.className = mergeClasses(listItemClassNames.button, styles.button, state.button?.className); + } return state; }; diff --git a/packages/react-components/react-list-preview/src/hooks/index.ts b/packages/react-components/react-list-preview/src/hooks/index.ts new file mode 100644 index 00000000000000..9940887a557c9f --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useListFeatures'; +export * from './useListSelection'; diff --git a/packages/react-components/react-list-preview/src/hooks/types.ts b/packages/react-components/react-list-preview/src/hooks/types.ts new file mode 100644 index 00000000000000..fefa1405521ca0 --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/types.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; + +export type UseListFeaturesOptions = { + items: TItem[]; +}; + +export type ListSelectionState = { + isSelected: (item: string | number) => boolean; + toggleItem: (e: React.SyntheticEvent, id: string | number) => void; + deselectItem: (e: React.SyntheticEvent, id: string | number) => void; + selectItem: (e: React.SyntheticEvent, id: string | number) => void; + clearSelection: (e: React.SyntheticEvent) => void; +}; + +export type UseListSelectionOptions = {}; // multiselect etc + +export interface ListFeaturesState extends Pick, 'items'> { + selection: ListSelectionState; +} + +export type ListFeaturePlugin = (listState: ListFeaturesState) => ListFeaturesState; diff --git a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts new file mode 100644 index 00000000000000..0e59c10947682e --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts @@ -0,0 +1,23 @@ +import { ListFeaturePlugin, UseListFeaturesOptions } from './types'; +import { defaultListSelectionState } from './useListSelection'; + +export function useListFeatures(options: UseListFeaturesOptions, plugins: ListFeaturePlugin[] = []) { + const { items } = options; + // const getRows = (rowEnhancer = defaultRowEnhancer) => + // items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); + + const initialState = { + // getRowId, + items, + selection: defaultListSelectionState, + // columns, + // getRows, + // selection: defaultTableSelectionState, + // sort: defaultTableSortState, + // eslint-disable-next-line @typescript-eslint/naming-convention + // columnSizing_unstable: defaultColumnSizingState, + // tableRef: React.createRef(), + }; + + return plugins.reduce((state, plugin) => plugin(state), initialState); +} diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx new file mode 100644 index 00000000000000..478abbb25bfec5 --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -0,0 +1,56 @@ +import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; +import * as React from 'react'; +import { ListFeaturesState, ListSelectionState, UseListSelectionOptions } from './types'; + +export const defaultListSelectionState: ListSelectionState = { + isSelected: () => false, + toggleItem: () => undefined, + selectItem: () => undefined, + deselectItem: () => undefined, + clearSelection: () => undefined, +}; + +export function useListSelection(options: SelectionHookParams = { selectionMode: 'multiselect' }) { + // False positive, these plugin hooks are intended to be run on every render + // eslint-disable-next-line react-hooks/rules-of-hooks + return (listState: ListFeaturesState) => useListSelectionState(listState, options); +} + +export function useListSelectionState(listState: ListFeaturesState, options: SelectionHookParams) { + const { selectionMode, defaultSelectedItems } = options; + const [selectedItems, setSelectedItems] = React.useState(() => new Set(defaultSelectedItems || [])); + + const [selected, selectionMethods] = useSelection({ + selectionMode, + defaultSelectedItems, + selectedItems, + onSelectionChange: (e, data) => setSelectedItems(data.selectedItems), + }); + + const toggleItem: ListSelectionState['toggleItem'] = useEventCallback((e, itemId) => + selectionMethods.toggleItem(e, itemId), + ); + + const deselectItem: ListSelectionState['deselectItem'] = useEventCallback((e, itemId: string | number) => + selectionMethods.deselectItem(e, itemId), + ); + + const selectItem: ListSelectionState['selectItem'] = useEventCallback((e, itemId: string | number) => + selectionMethods.selectItem(e, itemId), + ); + + const clearSelection: ListSelectionState['clearSelection'] = useEventCallback(e => selectionMethods.clearItems(e)); + + return { + ...listState, + selection: { + selectedItems: selected, + toggleItem, + deselectItem, + selectItem, + setSelectedItems, + isSelected: (id: string | number) => selectionMethods.isSelected(id), + clearSelection, + }, + }; +} diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index a938d39dcb44a8..154639f65a069b 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -1,4 +1,12 @@ -export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List'; +export { + List, + listClassNames, + renderList_unstable, + useListStyles_unstable, + useList_unstable, + ListLayout, +} from './List'; + export type { ListProps, ListSlots, ListState } from './List'; export { ListItem, @@ -8,3 +16,5 @@ export { useListItem_unstable, } from './ListItem'; export type { ListItemProps, ListItemSlots, ListItemState } from './ListItem'; + +export { useListFeatures, useListSelection } from './hooks'; diff --git a/packages/react-components/react-list-preview/stories/List/ListArrowNavigation.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListArrowNavigation.stories.tsx new file mode 100644 index 00000000000000..ba0bfd24724796 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListArrowNavigation.stories.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import { List, ListItem } from '@fluentui/react-list-preview'; + +export const ListArrowNavigation = () => ( + + hello + How Are you + +); diff --git a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md index 08ff8ddeeb5f86..ec958919633840 100644 --- a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md +++ b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md @@ -2,4 +2,11 @@ ### Do +- Use `focusable` property on the `List` where the items have no actionable elements inside. +- Use `focusableItems` property when the list items have more than one actionable element. + ### Don't + +- Don't use `focusableItems` prop if the list items have zero actionable elements, use `focusable` instead. + This way the list itself is focusable and users can scroll by using up and down arrows after focusing it. +- Don't use `focusableItems` prop if the list items have 1 actionable element, make that element focusable instead. diff --git a/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx index 0169f28c0a3808..4ff1cb2fdf38f3 100644 --- a/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx @@ -1,4 +1,14 @@ import * as React from 'react'; -import { List, ListProps } from '@fluentui/react-list-preview'; +import { List, ListItem } from '@fluentui/react-list-preview'; -export const Default = (props: Partial) => ; +export const Default = () => ( + + Asia + Africa + Europe + North America + South America + Australia/Oceania + Antarctica + +); diff --git a/packages/react-components/react-list-preview/stories/List/ListDescription.md b/packages/react-components/react-list-preview/stories/List/ListDescription.md index e69de29bb2d1d6..05dff727ef9fae 100644 --- a/packages/react-components/react-list-preview/stories/List/ListDescription.md +++ b/packages/react-components/react-list-preview/stories/List/ListDescription.md @@ -0,0 +1 @@ +A list provides a component for rendering sets of items of various sizes. It’s agnostic of layout, the tile component used, and selection management. diff --git a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx new file mode 100644 index 00000000000000..a64d8f99164cb7 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { List, ListProps, ListItem, ListLayout } from '@fluentui/react-list-preview'; +import { makeStyles } from '@fluentui/react-components'; + +const useStyles = makeStyles({ + root: { + 'grid-template-columns': '1fr 1fr 1fr', + }, +}); + +export const ListGrid = (props: Partial) => { + const classes = useStyles(); + return ( + + Asia + Africa + Europe + North America + South America + Australia/Oceania + Antarctica + + ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx new file mode 100644 index 00000000000000..199662b513b348 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { List, ListItem, ListLayout } from '@fluentui/react-list-preview'; +import { makeStyles, shorthands } from '@griffel/react'; + +const useStyles = makeStyles({ + listItem: { + ...shorthands.padding('4px'), + }, +}); + +export const ListHorizontal = () => { + const classes = useStyles(); + return ( + + Asia + Africa + Europe + North America + South America + Australia/Oceania + Antarctica + + ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx new file mode 100644 index 00000000000000..bb593657945272 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx @@ -0,0 +1,84 @@ +import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; +import * as React from 'react'; +import names from './names'; + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + button: { + display: 'block', + width: '100%', + textAlign: 'left', + }, +}); + +export const ListOverflowing = () => { + const classes = useStyles(); + return ( + <> +

List with focusable buttons

+
+ + {names.map(name => ( + + + + ))} + +
+

List with focusable listitem

+
+ + {names.map(name => ( + alert(name)}> + + + ))} + +
+

List with built in button slot

+
+ + {names.map(name => ( + alert(name) }}> + + + ))} + +
+ + ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx new file mode 100644 index 00000000000000..360bb651421508 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx @@ -0,0 +1,62 @@ +import { Button, Checkbox, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; +import * as React from 'react'; +import names from './names'; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const origItems: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + button: { + ...shorthands.padding(0), + }, +}); + +export const ListSelection = () => { + const classes = useStyles(); + + const { items, selection } = useListFeatures({ items: origItems }, [useListSelection()]); + const { isSelected, toggleItem, selectItem, deselectItem, clearSelection } = selection; + console.log('___', selection); + + return ( +
+ + + + + + {items.map(({ name, avatar, id }) => ( + toggleItem(e, id) }}> + + + + ))} + +
+ ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx new file mode 100644 index 00000000000000..9441f66ff6defb --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx @@ -0,0 +1,48 @@ +import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; +import * as React from 'react'; +import names from './names'; + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + listItem: { + display: 'flex', + alignItems: 'center', + ...shorthands.gap('4px'), + }, + primaryButton: { + marginRight: 'auto', + }, +}); + +export const ListWithMultipleActions = () => { + const classes = useStyles(); + return ( +
+ + {names.map(name => ( + + + + + + ))} + +
+ ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx new file mode 100644 index 00000000000000..519989b113a3ac --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { FixedSizeList } from 'react-window'; +import { List, ListItem } from '@fluentui/react-list-preview'; + +import countries from './countries'; + +export const VirtualizedList = () => { + return ( + + + {({ index, style, data }) => ( + + {data[index]} + + )} + + + ); +}; + +VirtualizedList.parameters = { + docs: { + description: { + story: [ + 'When creating a list of large size, one way of making sure you are getting the best performance', + 'is to use virtualization. In this example we are leveraging the `react-window` package.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/countries.js b/packages/react-components/react-list-preview/stories/List/countries.js new file mode 100644 index 00000000000000..e4719d4e30335a --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/countries.js @@ -0,0 +1,198 @@ +export default [ + ' Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Antigua & Deps', + 'Argentina', + 'Armenia', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bhutan', + 'Bolivia', + 'Bosnia Herzegovina', + 'Botswana', + 'Brazil', + 'Brunei', + 'Bulgaria', + 'Burkina', + 'Burundi', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Cape Verde', + 'Central African Rep', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Comoros', + 'Congo', + 'Congo {Democratic Rep}', + 'Costa Rica', + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'East Timor', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Ethiopia', + 'Fiji', + 'Finland', + 'France', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Greece', + 'Grenada', + 'Guatemala', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Honduras', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland {Republic}', + 'Israel', + 'Italy', + 'Ivory Coast', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Korea North', + 'Korea South', + 'Kosovo', + 'Kuwait', + 'Kyrgyzstan', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Macedonia', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Morocco', + 'Mozambique', + 'Myanmar, {Burma}', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Qatar', + 'Romania', + 'Russian Federation', + 'Rwanda', + 'St Kitts & Nevis', + 'St Lucia', + 'Saint Vincent & the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome & Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Swaziland', + 'Sweden', + 'Switzerland', + 'Syria', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Togo', + 'Tonga', + 'Trinidad & Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Vatican City', + 'Venezuela', + 'Vietnam', + 'Yemen', + 'Zambia', + 'Zimbabwe', +]; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index bda361cec97ed7..622d16cab837ff 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -4,6 +4,13 @@ import descriptionMd from './ListDescription.md'; import bestPracticesMd from './ListBestPractices.md'; export { Default } from './ListDefault.stories'; +export { ListHorizontal } from './ListHorizontal.stories'; +export { ListGrid } from './ListGrid.stories'; +export { ListArrowNavigation } from './ListArrowNavigation.stories'; +export { ListOverflowing } from './ListOverflowing.stories'; +export { ListSelection } from './ListSelection.stories'; +export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; +export { VirtualizedList } from './VirtualizedList.stories'; export default { title: 'Preview Components/List', diff --git a/packages/react-components/react-list-preview/stories/List/names.js b/packages/react-components/react-list-preview/stories/List/names.js new file mode 100644 index 00000000000000..bebc9bc7aacec6 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/names.js @@ -0,0 +1,54 @@ +//source: https://gist.githubusercontent.com/daramasala/37fe19924b1e0bd8979b6becc4eb0c1b/raw/120afae8d52c88d6b48c32fa5dfbb2a37c919216/names.txt + +export default [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', + 'Leanna Vadnais', + 'Latasha Tang', + 'Liliana Newlon', + 'Dorthy Leon', + 'Gigi Sin', + 'Jeneva Gratz', + 'Benedict Ramer', + 'Dee Rahn', + 'Bridget Cavallaro', + 'Dominga Follette', + 'Robyn Bernat', + 'Freeda Borg', + 'Margarett Rocamora', + 'Mazie Joly', + 'Matha Vore', + 'Yoko Brobst', + 'Ernesto Mccullum', + 'Maire Capel', + 'Ashely Housand', + 'Eleonor Sesco', + 'Gerardo Biggers', + 'Shanon Rosati', + 'Genevive Oldham', + 'Morgan Bevacqua', + 'Steve Howle', + 'Jenny Brenner', + 'Meagan Ponder', + 'Georgeann Vachon', + 'Arica Oloughlin', + 'Lekisha Chilcote', + 'Wilfred Demaris', + 'Jarrett Silas', + 'Keiko Varner', + 'Brittni Schwebach', + 'Rafael Patten', + 'Carson Cottone', + 'Onie Koga', +]; From 9652c2b0ef3c974d07e7764f1e1a92e8016c1d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 13 Oct 2023 13:19:29 +0200 Subject: [PATCH 003/136] remove selection logic, update overflowin example --- .../src/components/ListItem/useListItem.ts | 11 ++- .../react-list-preview/src/hooks/index.ts | 2 - .../react-list-preview/src/hooks/types.ts | 21 ----- .../src/hooks/useListFeatures.ts | 23 ------ .../src/hooks/useListSelection.tsx | 56 ------------- .../react-list-preview/src/index.ts | 2 - .../stories/List/ListBestPractices.md | 1 + .../stories/List/ListDescription.md | 30 ++++++- .../stories/List/ListGrid.stories.tsx | 13 ++- .../stories/List/ListOverflowing.stories.tsx | 79 +++++-------------- .../stories/List/ListSelection.stories.tsx | 62 --------------- .../List/ListWithMultipleActions.stories.tsx | 7 +- .../stories/List/index.stories.tsx | 1 - 13 files changed, 71 insertions(+), 237 deletions(-) delete mode 100644 packages/react-components/react-list-preview/src/hooks/index.ts delete mode 100644 packages/react-components/react-list-preview/src/hooks/types.ts delete mode 100644 packages/react-components/react-list-preview/src/hooks/useListFeatures.ts delete mode 100644 packages/react-components/react-list-preview/src/hooks/useListSelection.tsx delete mode 100644 packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts index 1d1df04f69dff7..225551391de74c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -40,12 +40,11 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref = { - items: TItem[]; -}; - -export type ListSelectionState = { - isSelected: (item: string | number) => boolean; - toggleItem: (e: React.SyntheticEvent, id: string | number) => void; - deselectItem: (e: React.SyntheticEvent, id: string | number) => void; - selectItem: (e: React.SyntheticEvent, id: string | number) => void; - clearSelection: (e: React.SyntheticEvent) => void; -}; - -export type UseListSelectionOptions = {}; // multiselect etc - -export interface ListFeaturesState extends Pick, 'items'> { - selection: ListSelectionState; -} - -export type ListFeaturePlugin = (listState: ListFeaturesState) => ListFeaturesState; diff --git a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts deleted file mode 100644 index 0e59c10947682e..00000000000000 --- a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ListFeaturePlugin, UseListFeaturesOptions } from './types'; -import { defaultListSelectionState } from './useListSelection'; - -export function useListFeatures(options: UseListFeaturesOptions, plugins: ListFeaturePlugin[] = []) { - const { items } = options; - // const getRows = (rowEnhancer = defaultRowEnhancer) => - // items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); - - const initialState = { - // getRowId, - items, - selection: defaultListSelectionState, - // columns, - // getRows, - // selection: defaultTableSelectionState, - // sort: defaultTableSortState, - // eslint-disable-next-line @typescript-eslint/naming-convention - // columnSizing_unstable: defaultColumnSizingState, - // tableRef: React.createRef(), - }; - - return plugins.reduce((state, plugin) => plugin(state), initialState); -} diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx deleted file mode 100644 index 478abbb25bfec5..00000000000000 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; -import * as React from 'react'; -import { ListFeaturesState, ListSelectionState, UseListSelectionOptions } from './types'; - -export const defaultListSelectionState: ListSelectionState = { - isSelected: () => false, - toggleItem: () => undefined, - selectItem: () => undefined, - deselectItem: () => undefined, - clearSelection: () => undefined, -}; - -export function useListSelection(options: SelectionHookParams = { selectionMode: 'multiselect' }) { - // False positive, these plugin hooks are intended to be run on every render - // eslint-disable-next-line react-hooks/rules-of-hooks - return (listState: ListFeaturesState) => useListSelectionState(listState, options); -} - -export function useListSelectionState(listState: ListFeaturesState, options: SelectionHookParams) { - const { selectionMode, defaultSelectedItems } = options; - const [selectedItems, setSelectedItems] = React.useState(() => new Set(defaultSelectedItems || [])); - - const [selected, selectionMethods] = useSelection({ - selectionMode, - defaultSelectedItems, - selectedItems, - onSelectionChange: (e, data) => setSelectedItems(data.selectedItems), - }); - - const toggleItem: ListSelectionState['toggleItem'] = useEventCallback((e, itemId) => - selectionMethods.toggleItem(e, itemId), - ); - - const deselectItem: ListSelectionState['deselectItem'] = useEventCallback((e, itemId: string | number) => - selectionMethods.deselectItem(e, itemId), - ); - - const selectItem: ListSelectionState['selectItem'] = useEventCallback((e, itemId: string | number) => - selectionMethods.selectItem(e, itemId), - ); - - const clearSelection: ListSelectionState['clearSelection'] = useEventCallback(e => selectionMethods.clearItems(e)); - - return { - ...listState, - selection: { - selectedItems: selected, - toggleItem, - deselectItem, - selectItem, - setSelectedItems, - isSelected: (id: string | number) => selectionMethods.isSelected(id), - clearSelection, - }, - }; -} diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 154639f65a069b..fb5ad05280b201 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -16,5 +16,3 @@ export { useListItem_unstable, } from './ListItem'; export type { ListItemProps, ListItemSlots, ListItemState } from './ListItem'; - -export { useListFeatures, useListSelection } from './hooks'; diff --git a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md index ec958919633840..0a769dd95b6a8e 100644 --- a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md +++ b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md @@ -4,6 +4,7 @@ - Use `focusable` property on the `List` where the items have no actionable elements inside. - Use `focusableItems` property when the list items have more than one actionable element. +- If the ListItem has one action, use the `button` slot with `onClick` handler. Also use this slot to add a11y attributes like `aria-label`. ### Don't diff --git a/packages/react-components/react-list-preview/stories/List/ListDescription.md b/packages/react-components/react-list-preview/stories/List/ListDescription.md index 05dff727ef9fae..c6a4f9ca65ca55 100644 --- a/packages/react-components/react-list-preview/stories/List/ListDescription.md +++ b/packages/react-components/react-list-preview/stories/List/ListDescription.md @@ -1 +1,29 @@ -A list provides a component for rendering sets of items of various sizes. It’s agnostic of layout, the tile component used, and selection management. +The List is a component for rendering sets of items. It supports multiple layouts, namely `vertical` (default), `horizontal` and `grid`. + +There are 2 basic use cases for List, based on the elements it contains: + +### Non-interactive list + +A simple list with non-interactive elements inside of it. Imagine a list of ingredients for a dish, a list of requirements for a project or list of your favorite animals. + +### Interactive lists + +An interactive list is a List where each of its items has at least one actionable element. Imagine a list of emails (clicking will open in), a list of contacts(clicking will open a conversation) or a list of installed apps (clicking will open it's details.) + +The interactive lists can then be further divided into 2 different categories: + +#### List items with a single action + +If the list item supports a single action, the preferred way of handling this is attaching the handler as `onClick` property on the `button` slot of the `ListItem`. + +In this case you don't want to make the actual list items focusable and you don't provide any accessibility attributes to it. The `button` slot is what you should be targeting. + +Making the List Item focusable or accessible by screen readers adds one unnecessary focusable element in the hierarchy that doesn't do anything. + +#### List items with multiple actions + +If the list needs to support more than single click action, your best bet is to define the buttons inside of the ListItem manually. + +You also want to make the list item focusable by adding `focusableItems` property on the List itself. + +This makes sure the list is navigable with up and down arrows and user can enter the group (ListItem) to select the action they want to take with the list. diff --git a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx index a64d8f99164cb7..854a160c8cf262 100644 --- a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx @@ -4,7 +4,7 @@ import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ root: { - 'grid-template-columns': '1fr 1fr 1fr', + 'grid-template-columns': 'repeat(3, 1fr)', }, }); @@ -22,3 +22,14 @@ export const ListGrid = (props: Partial) => {
); }; + +ListGrid.parameters = { + docs: { + description: { + story: [ + 'List can have a grid layout. What this means is that the wrapper will have `display: grid` applied to it. Also the arrow navigation will work horizontaly and vertically.', + "\nBy default, the grid doesn't have any columns defined. It is up to the user to define the columns using CSS. Please refer to the example below.", + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx index bb593657945272..8a5467104d7700 100644 --- a/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx @@ -19,66 +19,23 @@ const useStyles = makeStyles({ export const ListOverflowing = () => { const classes = useStyles(); return ( - <> -

List with focusable buttons

-
- - {names.map(name => ( - - - - ))} - -
-

List with focusable listitem

-
- - {names.map(name => ( - alert(name)}> - - - ))} - -
-

List with built in button slot

-
- - {names.map(name => ( - alert(name) }}> - - - ))} - -
- +
+ + {names.map(name => ( + alert(name), 'aria-label': name }}> + + + ))} + +
); }; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx deleted file mode 100644 index 360bb651421508..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Button, Checkbox, makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; -import * as React from 'react'; -import names from './names'; - -type Item = { - name: string; - id: string; - avatar: string; -}; - -const origItems: Item[] = names.map(name => ({ - name, - id: name, - avatar: - 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', -})); - -const useStyles = makeStyles({ - wrapper: { - maxHeight: '300px', - overflowY: 'auto', - ...shorthands.padding('2px'), - }, - button: { - ...shorthands.padding(0), - }, -}); - -export const ListSelection = () => { - const classes = useStyles(); - - const { items, selection } = useListFeatures({ items: origItems }, [useListSelection()]); - const { isSelected, toggleItem, selectItem, deselectItem, clearSelection } = selection; - console.log('___', selection); - - return ( -
- - - - - - {items.map(({ name, avatar, id }) => ( - toggleItem(e, id) }}> - - - - ))} - -
- ); -}; diff --git a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx index 9441f66ff6defb..b5997c4ec5d33f 100644 --- a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx @@ -26,7 +26,12 @@ export const ListWithMultipleActions = () => { {names.map(name => ( - + + + + + } + /> + + ); +}; + +export const ListMultipleActions = (props: Partial) => { + const classes = useStyles(); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +ListMultipleActions.parameters = { + docs: { + description: { + story: [ + 'List can have a grid layout. What this means is that the wrapper will have `display: grid` applied to it. Also the arrow navigation will work horizontaly and vertically.', + "\nBy default, the grid doesn't have any columns defined. It is up to the user to define the columns using CSS. Please refer to the example below.", + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 4d23b6b34a3711..7fac0d986bfa87 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -10,6 +10,7 @@ export { ListWithAction } from './ListWithAction.stories'; // export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; +export { ListMultipleActions } from './ListMultipleActions.stories'; export default { title: 'Preview Components/List', From 980fba1ad618145a5c7d5d44db348c9e41d244b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 1 Nov 2023 10:33:58 +0100 Subject: [PATCH 011/136] Revert "remove selection logic, update overflowin example" This reverts commit 9652c2b0ef3c974d07e7764f1e1a92e8016c1d12. --- .../react-list-preview/src/hooks/index.ts | 2 + .../react-list-preview/src/hooks/types.ts | 21 +++++ .../src/hooks/useListFeatures.ts | 23 +++++ .../src/hooks/useListSelection.tsx | 56 +++++++++++++ .../react-list-preview/src/index.ts | 10 ++- .../stories/List/ListOverflowing.stories.tsx | 84 +++++++++++++++++++ .../stories/List/ListSelection.stories.tsx | 62 ++++++++++++++ .../stories/List/index.stories.tsx | 4 +- 8 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 packages/react-components/react-list-preview/src/hooks/index.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/types.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/useListFeatures.ts create mode 100644 packages/react-components/react-list-preview/src/hooks/useListSelection.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx diff --git a/packages/react-components/react-list-preview/src/hooks/index.ts b/packages/react-components/react-list-preview/src/hooks/index.ts new file mode 100644 index 00000000000000..9940887a557c9f --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useListFeatures'; +export * from './useListSelection'; diff --git a/packages/react-components/react-list-preview/src/hooks/types.ts b/packages/react-components/react-list-preview/src/hooks/types.ts new file mode 100644 index 00000000000000..fefa1405521ca0 --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/types.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; + +export type UseListFeaturesOptions = { + items: TItem[]; +}; + +export type ListSelectionState = { + isSelected: (item: string | number) => boolean; + toggleItem: (e: React.SyntheticEvent, id: string | number) => void; + deselectItem: (e: React.SyntheticEvent, id: string | number) => void; + selectItem: (e: React.SyntheticEvent, id: string | number) => void; + clearSelection: (e: React.SyntheticEvent) => void; +}; + +export type UseListSelectionOptions = {}; // multiselect etc + +export interface ListFeaturesState extends Pick, 'items'> { + selection: ListSelectionState; +} + +export type ListFeaturePlugin = (listState: ListFeaturesState) => ListFeaturesState; diff --git a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts new file mode 100644 index 00000000000000..0e59c10947682e --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts @@ -0,0 +1,23 @@ +import { ListFeaturePlugin, UseListFeaturesOptions } from './types'; +import { defaultListSelectionState } from './useListSelection'; + +export function useListFeatures(options: UseListFeaturesOptions, plugins: ListFeaturePlugin[] = []) { + const { items } = options; + // const getRows = (rowEnhancer = defaultRowEnhancer) => + // items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); + + const initialState = { + // getRowId, + items, + selection: defaultListSelectionState, + // columns, + // getRows, + // selection: defaultTableSelectionState, + // sort: defaultTableSortState, + // eslint-disable-next-line @typescript-eslint/naming-convention + // columnSizing_unstable: defaultColumnSizingState, + // tableRef: React.createRef(), + }; + + return plugins.reduce((state, plugin) => plugin(state), initialState); +} diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx new file mode 100644 index 00000000000000..478abbb25bfec5 --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -0,0 +1,56 @@ +import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; +import * as React from 'react'; +import { ListFeaturesState, ListSelectionState, UseListSelectionOptions } from './types'; + +export const defaultListSelectionState: ListSelectionState = { + isSelected: () => false, + toggleItem: () => undefined, + selectItem: () => undefined, + deselectItem: () => undefined, + clearSelection: () => undefined, +}; + +export function useListSelection(options: SelectionHookParams = { selectionMode: 'multiselect' }) { + // False positive, these plugin hooks are intended to be run on every render + // eslint-disable-next-line react-hooks/rules-of-hooks + return (listState: ListFeaturesState) => useListSelectionState(listState, options); +} + +export function useListSelectionState(listState: ListFeaturesState, options: SelectionHookParams) { + const { selectionMode, defaultSelectedItems } = options; + const [selectedItems, setSelectedItems] = React.useState(() => new Set(defaultSelectedItems || [])); + + const [selected, selectionMethods] = useSelection({ + selectionMode, + defaultSelectedItems, + selectedItems, + onSelectionChange: (e, data) => setSelectedItems(data.selectedItems), + }); + + const toggleItem: ListSelectionState['toggleItem'] = useEventCallback((e, itemId) => + selectionMethods.toggleItem(e, itemId), + ); + + const deselectItem: ListSelectionState['deselectItem'] = useEventCallback((e, itemId: string | number) => + selectionMethods.deselectItem(e, itemId), + ); + + const selectItem: ListSelectionState['selectItem'] = useEventCallback((e, itemId: string | number) => + selectionMethods.selectItem(e, itemId), + ); + + const clearSelection: ListSelectionState['clearSelection'] = useEventCallback(e => selectionMethods.clearItems(e)); + + return { + ...listState, + selection: { + selectedItems: selected, + toggleItem, + deselectItem, + selectItem, + setSelectedItems, + isSelected: (id: string | number) => selectionMethods.isSelected(id), + clearSelection, + }, + }; +} diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 11e04a3f949c8a..3945e1d7519679 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -16,5 +16,13 @@ export { useListItem_unstable, } from './ListItem'; export type { ListItemProps, ListItemSlots, ListItemState } from './ListItem'; -export { ListItemButton, listItemButtonClassNames, renderListItemButton_unstable, useListItemButtonStyles_unstable, useListItemButton_unstable } from './ListItemButton'; +export { + ListItemButton, + listItemButtonClassNames, + renderListItemButton_unstable, + useListItemButtonStyles_unstable, + useListItemButton_unstable, +} from './ListItemButton'; export type { ListItemButtonProps, ListItemButtonSlots, ListItemButtonState } from './ListItemButton'; + +export { useListFeatures, useListSelection } from './hooks'; diff --git a/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx new file mode 100644 index 00000000000000..bb593657945272 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx @@ -0,0 +1,84 @@ +import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; +import * as React from 'react'; +import names from './names'; + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + button: { + display: 'block', + width: '100%', + textAlign: 'left', + }, +}); + +export const ListOverflowing = () => { + const classes = useStyles(); + return ( + <> +

List with focusable buttons

+
+ + {names.map(name => ( + + + + ))} + +
+

List with focusable listitem

+
+ + {names.map(name => ( + alert(name)}> + + + ))} + +
+

List with built in button slot

+
+ + {names.map(name => ( + alert(name) }}> + + + ))} + +
+ + ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx new file mode 100644 index 00000000000000..360bb651421508 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx @@ -0,0 +1,62 @@ +import { Button, Checkbox, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; +import * as React from 'react'; +import names from './names'; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const origItems: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + button: { + ...shorthands.padding(0), + }, +}); + +export const ListSelection = () => { + const classes = useStyles(); + + const { items, selection } = useListFeatures({ items: origItems }, [useListSelection()]); + const { isSelected, toggleItem, selectItem, deselectItem, clearSelection } = selection; + console.log('___', selection); + + return ( +
+ + + + + + {items.map(({ name, avatar, id }) => ( + toggleItem(e, id) }}> + + + + ))} + +
+ ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 7fac0d986bfa87..be956dfa8ec4ff 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -8,9 +8,11 @@ export { ListHorizontal } from './ListHorizontal.stories'; export { ListGrid } from './ListGrid.stories'; export { ListWithAction } from './ListWithAction.stories'; // export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; +export { ListOverflowing } from './ListOverflowing.stories'; +export { ListSelection } from './ListSelection.stories'; +export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; -export { ListMultipleActions } from './ListMultipleActions.stories'; export default { title: 'Preview Components/List', From 942b2c86a39257c732d7f810ebc8452885f9edf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 1 Nov 2023 10:38:46 +0100 Subject: [PATCH 012/136] internal selection state handling --- .../react-list-preview/package.json | 6 +- .../src/components/List/List.types.ts | 17 ++++- .../src/components/List/listContext.ts | 19 +++-- .../src/components/List/useList.ts | 54 +++++++++++++- .../components/List/useListContextValues.ts | 9 ++- .../src/components/ListItem/ListItem.types.ts | 10 ++- .../components/ListItem/renderListItem.tsx | 8 ++- .../src/components/ListItem/useListItem.ts | 42 +++++++++-- .../ListItem/useListItemStyles.styles.ts | 16 ++++- .../react-list-preview/src/hooks/types.ts | 11 ++- .../src/hooks/useListFeatures.ts | 15 ++-- .../src/hooks/useListSelection.tsx | 26 +++++-- .../react-list-preview/src/index.ts | 2 +- .../List/ListSelectionInternal.stories.tsx | 72 +++++++++++++++++++ .../stories/List/index.stories.tsx | 1 + 15 files changed, 269 insertions(+), 39 deletions(-) create mode 100644 packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 5d6dd748e2553f..2945c9740ad6d0 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -31,11 +31,13 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { + "@fluentui/react-button": "^9.3.34", + "@fluentui/react-checkbox": "^9.1.35", + "@fluentui/react-context-selector": "^9.1.31", "@fluentui/react-jsx-runtime": "^9.0.3", - "@fluentui/react-theme": "^9.1.11", "@fluentui/react-tabster": "^9.12.5", + "@fluentui/react-theme": "^9.1.11", "@fluentui/react-utilities": "^9.13.0", - "@fluentui/react-button": "^9.3.34", "@griffel/react": "^1.5.14", "@swc/helpers": "^0.5.1" }, diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 318a9f86c87410..48738314e2913f 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,5 +1,7 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import * as React from 'react'; +import type { ComponentProps, ComponentState, OnSelectionChangeData, Slot } from '@fluentui/react-utilities'; import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; +import { ListSelectionState } from '../../hooks/types'; export enum ListLayout { Horizontal = 'horizontal', @@ -7,6 +9,11 @@ export enum ListLayout { Grid = 'grid', } +// Component ref interface +export type IList = { + selection: ListState['selection']; +}; + export type ListSlots = { root: NonNullable>; }; @@ -18,10 +25,18 @@ export type ListProps = ComponentProps & { layout?: ListLayout; customArrowNavigationOptions?: Partial; focusableItems?: boolean; + selectable?: boolean; + onSelectionChange?: (event: React.SyntheticEvent, data: OnSelectionChangeData) => void; + componentRef: React.Ref; }; export type ListContextValue = { focusableItems: boolean; + items: Array<{ id: string | number }>; + registerItem: (id: string | number, ref: React.RefObject) => void; + deregisterItem: (id: string | number, ref: React.RefObject) => void; + selection: ListSelectionState; + selectable: boolean; }; export type ListContextValues = { diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index 9d5ee08edbe528..ef645e8d7e4230 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -1,11 +1,22 @@ -import * as React from 'react'; +import { createContext, useContextSelector } from '@fluentui/react-context-selector'; +import type { ContextSelector } from '@fluentui/react-context-selector'; import { ListContextValue } from './List.types'; -const listContext = React.createContext(undefined); - export const listContextDefaultValue: ListContextValue = { focusableItems: false, + items: [], + registerItem: () => { + /* noop */ + }, + deregisterItem: () => { + /* noop */ + }, + selection: undefined, }; +const listContext = createContext(undefined); + export const ListContextProvider = listContext.Provider; -export const useListContext = () => React.useContext(listContext) ?? listContextDefaultValue; + +export const useListContext_unstable = (selector: ContextSelector): T => + useContextSelector(listContext, (ctx = listContextDefaultValue) => selector(ctx)); diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index fa87a5eb3e1aa7..e68513787c176c 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -1,7 +1,19 @@ import * as React from 'react'; -import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useEventCallback } from '@fluentui/react-utilities'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; -import { ListLayout, ListProps, ListState } from './List.types'; +import { ListLayout, ListProps, ListState, IList } from './List.types'; +import { useListFeatures } from '../../hooks/useListFeatures'; +import { useListSelection } from '../../hooks/useListSelection'; + +const useComponentRef = (componentRef: React.Ref | undefined, selection: ListState['selection']) => { + React.useImperativeHandle( + componentRef, + () => ({ + selection, + }), + [selection], + ); +}; /** * Create the state required to render List. @@ -13,7 +25,14 @@ import { ListLayout, ListProps, ListState } from './List.types'; * @param ref - reference to root HTMLElement of List */ export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { - const { layout = ListLayout.Vertical, focusableItems = false, customArrowNavigationOptions } = props; + const { + layout = ListLayout.Vertical, + focusableItems = false, + customArrowNavigationOptions, + selectable = false, + componentRef, + onSelectionChange, + } = props; const arrowNavigationAttributes = useArrowNavigationGroup({ axis: layout === ListLayout.Grid ? 'grid-linear' : 'both', @@ -21,6 +40,30 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): ...(customArrowNavigationOptions || {}), }); + // const items = React.useRef>([]); + const [items, setItems] = React.useState>([]); + + const { selection } = useListFeatures({ items }, [ + useListSelection({ onSelectionChange, selectionMode: 'multiselect' }), + ]); + + // const rootRef = React.useRef(null); + // const mergedRootRefs: React.Ref = useMergedRefs(rootRef, forwardedRef); + useComponentRef(componentRef, selection); + + const registerItem = useEventCallback((id: string | number) => { + if (!items.find(item => item.id === id)) { + setItems(current => [...current, { id }]); + } + }); + + const deregisterItem = useEventCallback((id: string | number) => { + // items.current = items.current.filter(item => item.id !== id); + if (items.find(k => k.id === id)) { + setItems(current => current.filter(item => item.id !== id)); + } + }); + return { components: { root: 'div', @@ -37,5 +80,10 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): layout, // context: focusableItems, + items, + registerItem, + deregisterItem, + selectable, + selection, }; }; diff --git a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts index e9f1341fba7592..71ab5a7758681c 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts @@ -2,13 +2,18 @@ import * as React from 'react'; import { ListContextValues, ListState } from './List.types'; export function useListContextValues_unstable(state: ListState): ListContextValues { - const { focusableItems } = state; + const { focusableItems, items, registerItem, deregisterItem, selectable, selection } = state; const listContext = React.useMemo( () => ({ focusableItems, + items, + registerItem, + deregisterItem, + selectable, + selection, }), - [focusableItems], + [focusableItems, items, registerItem, deregisterItem, selection, selectable], ); return { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 2848792d21a0bf..54925e66587e60 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -1,15 +1,21 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import { Checkbox } from '@fluentui/react-checkbox'; export type ListItemSlots = { root: NonNullable>; + checkbox?: Slot; }; /** * ListItem Props */ -export type ListItemProps = ComponentProps & {}; +export type ListItemProps = ComponentProps & { + value: string | number; +}; /** * State used in rendering ListItem */ -export type ListItemState = ComponentState; +export type ListItemState = ComponentState & { selectable: boolean }; +// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ListItemProps. +// & Required> diff --git a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx index 14f1e8f0ee0694..29a59094109e80 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx @@ -11,5 +11,11 @@ import type { ListItemState, ListItemSlots } from './ListItem.types'; export const renderListItem_unstable = (state: ListItemState) => { assertSlots(state); - return ; + // TODO Add additional slots in the appropriate place + return ( + + {state.checkbox ? : null} + {state.root.children} + + ); }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts index ba0aa818b82908..a109d7c1f0408a 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -1,8 +1,9 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; -import { getNativeElementProps, slot } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { Checkbox } from '@fluentui/react-checkbox'; import type { ListItemProps, ListItemState } from './ListItem.types'; -import { useListContext } from '../List/listContext'; +import { useListContext_unstable } from '../List/listContext'; /** * Create the state required to render ListItem. @@ -14,12 +15,32 @@ import { useListContext } from '../List/listContext'; * @param ref - reference to root HTMLElement of ListItem */ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref): ListItemState => { - const { focusableItems } = useListContext(); + const id = useId('listItem'); + const { value = id } = props; + + const focusableItems = useListContext_unstable(ctx => ctx.focusableItems); + const registerItem = useListContext_unstable(ctx => ctx.registerItem); + const deregisterItem = useListContext_unstable(ctx => ctx.deregisterItem); + const selectable = useListContext_unstable(ctx => ctx.selectable); + const selection = useListContext_unstable(ctx => ctx.selection); + const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); + const innerRef = React.useRef(null); + + React.useEffect(() => { + registerItem(value, innerRef); + + return () => { + deregisterItem(value, innerRef); + }; + // Always make sure the dependencies are stable across rerenders, otherwise we go + // in a loop of registering and deregistering. + }, [innerRef, value, registerItem, deregisterItem]); + const root = slot.always( getNativeElementProps('div', { - ref, + ref: useMergedRefs(ref, innerRef), role: 'listitem', tabIndex: focusableItems ? 0 : undefined, ...focusableGroupAttrs, @@ -28,11 +49,24 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref selection.toggleItem(e, value), + }, + }); + const state: ListItemState = { components: { root: 'div', + checkbox: Checkbox, }, root, + checkbox, + selectable, }; return state; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index a2960f3e6637a8..745c3e95cd1e29 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -6,6 +6,7 @@ import { tokens } from '@fluentui/react-theme'; export const listItemClassNames: SlotClassNames = { root: 'fui-ListItem', + checkbox: 'fui-ListItem__checkbox', }; /** @@ -21,6 +22,10 @@ const useStyles = makeStyles({ { selector: 'focus' }, ), }, + + rootSelectable: { + display: 'flex', + }, }); /** @@ -28,7 +33,16 @@ const useStyles = makeStyles({ */ export const useListItemStyles_unstable = (state: ListItemState): ListItemState => { const styles = useStyles(); - state.root.className = mergeClasses(listItemClassNames.root, styles.root, state.root.className); + state.root.className = mergeClasses( + listItemClassNames.root, + styles.root, + state.selectable && styles.rootSelectable, + state.root.className, + ); + + if (state.checkbox) { + state.checkbox.className = mergeClasses(listItemClassNames.checkbox, state.checkbox?.className); + } return state; }; diff --git a/packages/react-components/react-list-preview/src/hooks/types.ts b/packages/react-components/react-list-preview/src/hooks/types.ts index fefa1405521ca0..ab102ac02c425f 100644 --- a/packages/react-components/react-list-preview/src/hooks/types.ts +++ b/packages/react-components/react-list-preview/src/hooks/types.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -export type UseListFeaturesOptions = { +export type UseListFeaturesOptions = { items: TItem[]; }; @@ -10,12 +10,17 @@ export type ListSelectionState = { deselectItem: (e: React.SyntheticEvent, id: string | number) => void; selectItem: (e: React.SyntheticEvent, id: string | number) => void; clearSelection: (e: React.SyntheticEvent) => void; + toggleAllItems: (e: React.SyntheticEvent) => void; + selectedItems: Set; }; export type UseListSelectionOptions = {}; // multiselect etc -export interface ListFeaturesState extends Pick, 'items'> { +export interface ListFeaturesState + extends Pick, 'items'> { selection: ListSelectionState; } -export type ListFeaturePlugin = (listState: ListFeaturesState) => ListFeaturesState; +export type ListFeaturePlugin = ( + listState: ListFeaturesState, +) => ListFeaturesState; diff --git a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts index 0e59c10947682e..a02696ea324d95 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts +++ b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts @@ -1,22 +1,15 @@ import { ListFeaturePlugin, UseListFeaturesOptions } from './types'; import { defaultListSelectionState } from './useListSelection'; -export function useListFeatures(options: UseListFeaturesOptions, plugins: ListFeaturePlugin[] = []) { +export function useListFeatures( + options: UseListFeaturesOptions, + plugins: ListFeaturePlugin[] = [], +) { const { items } = options; - // const getRows = (rowEnhancer = defaultRowEnhancer) => - // items.map((item, i) => rowEnhancer({ item, rowId: getRowId?.(item) ?? i })); const initialState = { - // getRowId, items, selection: defaultListSelectionState, - // columns, - // getRows, - // selection: defaultTableSelectionState, - // sort: defaultTableSortState, - // eslint-disable-next-line @typescript-eslint/naming-convention - // columnSizing_unstable: defaultColumnSizingState, - // tableRef: React.createRef(), }; return plugins.reduce((state, plugin) => plugin(state), initialState); diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 478abbb25bfec5..a8bee8be87f722 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -8,29 +8,46 @@ export const defaultListSelectionState: ListSelectionState = { selectItem: () => undefined, deselectItem: () => undefined, clearSelection: () => undefined, + toggleAllItems: () => undefined, + selectedItems: new Set(), }; -export function useListSelection(options: SelectionHookParams = { selectionMode: 'multiselect' }) { +export function useListSelection( + options: SelectionHookParams = { selectionMode: 'multiselect' }, +) { // False positive, these plugin hooks are intended to be run on every render // eslint-disable-next-line react-hooks/rules-of-hooks return (listState: ListFeaturesState) => useListSelectionState(listState, options); } -export function useListSelectionState(listState: ListFeaturesState, options: SelectionHookParams) { - const { selectionMode, defaultSelectedItems } = options; +export function useListSelectionState( + listState: ListFeaturesState, + options: SelectionHookParams, +) { + const { selectionMode, defaultSelectedItems, onSelectionChange } = options; const [selectedItems, setSelectedItems] = React.useState(() => new Set(defaultSelectedItems || [])); const [selected, selectionMethods] = useSelection({ selectionMode, defaultSelectedItems, selectedItems, - onSelectionChange: (e, data) => setSelectedItems(data.selectedItems), + onSelectionChange: (e, data) => { + setSelectedItems(data.selectedItems); + onSelectionChange?.(e, data); + }, }); const toggleItem: ListSelectionState['toggleItem'] = useEventCallback((e, itemId) => selectionMethods.toggleItem(e, itemId), ); + const toggleAllItems: ListSelectionState['toggleAllItems'] = useEventCallback(e => { + selectionMethods.toggleAllItems( + e, + listState.items.map(item => item.id), + ); + }); + const deselectItem: ListSelectionState['deselectItem'] = useEventCallback((e, itemId: string | number) => selectionMethods.deselectItem(e, itemId), ); @@ -46,6 +63,7 @@ export function useListSelectionState(listState: ListFeaturesState selection: { selectedItems: selected, toggleItem, + toggleAllItems, deselectItem, selectItem, setSelectedItems, diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 3945e1d7519679..13210067963e56 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -7,7 +7,7 @@ export { ListLayout, } from './List'; -export type { ListProps, ListSlots, ListState } from './List'; +export type { ListProps, ListSlots, ListState, IList } from './List'; export { ListItem, listItemClassNames, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx new file mode 100644 index 00000000000000..19f05989cb3a3e --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx @@ -0,0 +1,72 @@ +import { Button, Checkbox, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem, useListFeatures, useListSelection, IList } from '@fluentui/react-list-preview'; +import * as React from 'react'; +import names from './names'; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const origItems: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + button: { + ...shorthands.padding(0), + }, +}); + +export const ListSelectionInternal = () => { + const classes = useStyles(); + const [currentIndex, setCurrentIndex] = React.useState(4); + + const items = React.useMemo(() => { + return origItems.slice(0, currentIndex); + }, [currentIndex]); + + const ref = React.useRef(null); + const [selectedItems, setSelectedItems] = React.useState>([]); + + return ( +
+ + +
Selected: {selectedItems.join(', ')}
+ + setSelectedItems(Array.from(data.selectedItems))} + componentRef={ref} + > + {items.map(({ name, avatar }) => { + return ( + + + + ); + })} + +
+ ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index be956dfa8ec4ff..20c3efe91da5ae 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -10,6 +10,7 @@ export { ListWithAction } from './ListWithAction.stories'; // export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; export { ListOverflowing } from './ListOverflowing.stories'; export { ListSelection } from './ListSelection.stories'; +export { ListSelectionInternal } from './ListSelectionInternal.stories'; export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; From 6deefb5c7145d325bfae60d8283c6132dee2c5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 1 Nov 2023 16:09:38 +0100 Subject: [PATCH 013/136] continue work on selection --- .../src/components/List/List.types.ts | 11 +- .../src/components/List/useList.ts | 20 +-- .../components/List/useListStyles.styles.ts | 5 +- .../src/components/ListItem/ListItem.types.ts | 4 +- .../src/components/ListItem/useListItem.ts | 54 +++++- .../ListItem/useListItemStyles.styles.ts | 2 + .../src/hooks/useListSelection.tsx | 2 +- .../List/ListMultipleActions.stories.tsx | 155 +++++++++++------- .../stories/List/ListOverflowing.stories.tsx | 84 ---------- .../List/ListSelectionInternal.stories.tsx | 10 +- .../stories/List/index.stories.tsx | 4 +- 11 files changed, 177 insertions(+), 174 deletions(-) delete mode 100644 packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 48738314e2913f..5c7e5c1c759eaa 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,5 +1,11 @@ import * as React from 'react'; -import type { ComponentProps, ComponentState, OnSelectionChangeData, Slot } from '@fluentui/react-utilities'; +import type { + ComponentProps, + ComponentState, + OnSelectionChangeData, + Slot, + SelectionMode, +} from '@fluentui/react-utilities'; import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; import { ListSelectionState } from '../../hooks/types'; @@ -15,7 +21,7 @@ export type IList = { }; export type ListSlots = { - root: NonNullable>; + root: NonNullable>; }; /** @@ -26,6 +32,7 @@ export type ListProps = ComponentProps & { customArrowNavigationOptions?: Partial; focusableItems?: boolean; selectable?: boolean; + selectionMode?: SelectionMode; onSelectionChange?: (event: React.SyntheticEvent, data: OnSelectionChangeData) => void; componentRef: React.Ref; }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index e68513787c176c..661ba8a8a4ea2f 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -30,6 +30,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): focusableItems = false, customArrowNavigationOptions, selectable = false, + selectionMode = 'multiselect', componentRef, onSelectionChange, } = props; @@ -40,15 +41,9 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): ...(customArrowNavigationOptions || {}), }); - // const items = React.useRef>([]); const [items, setItems] = React.useState>([]); - const { selection } = useListFeatures({ items }, [ - useListSelection({ onSelectionChange, selectionMode: 'multiselect' }), - ]); - - // const rootRef = React.useRef(null); - // const mergedRootRefs: React.Ref = useMergedRefs(rootRef, forwardedRef); + const { selection } = useListFeatures({ items }, [useListSelection({ onSelectionChange, selectionMode })]); useComponentRef(componentRef, selection); const registerItem = useEventCallback((id: string | number) => { @@ -58,7 +53,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): }); const deregisterItem = useEventCallback((id: string | number) => { - // items.current = items.current.filter(item => item.id !== id); if (items.find(k => k.id === id)) { setItems(current => current.filter(item => item.id !== id)); } @@ -66,16 +60,18 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): return { components: { - root: 'div', + root: 'ul', }, root: slot.always( - getNativeElementProps('div', { + getNativeElementProps('ul', { ref, - role: 'list', + role: selectable ? 'listbox' : 'list', + tabIndex: -1, + 'aria-multiselectable': selectable ? true : undefined, // TODO not true for single select ...arrowNavigationAttributes, ...props, }), - { elementType: 'div' }, + { elementType: 'ul' }, ), layout, // context: diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts index 55ee283e0f8ff3..bf2e9ba241ec7d 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -1,4 +1,4 @@ -import { makeStyles, mergeClasses } from '@griffel/react'; +import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { ListLayout, ListSlots, ListState } from './List.types'; @@ -13,7 +13,8 @@ export const listClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - // TODO Add default styles for the root element + ...shorthands.padding(0), + ...shorthands.margin(0), }, rootHorizontal: { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 54925e66587e60..0934cace697fd3 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -2,7 +2,7 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utili import { Checkbox } from '@fluentui/react-checkbox'; export type ListItemSlots = { - root: NonNullable>; + root: NonNullable>; checkbox?: Slot; }; @@ -10,7 +10,7 @@ export type ListItemSlots = { * ListItem Props */ export type ListItemProps = ComponentProps & { - value: string | number; + value?: string | number; }; /** diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts index a109d7c1f0408a..e2def0be55de90 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; -import { getNativeElementProps, slot, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; import { Checkbox } from '@fluentui/react-checkbox'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; @@ -16,7 +16,7 @@ import { useListContext_unstable } from '../List/listContext'; */ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref): ListItemState => { const id = useId('listItem'); - const { value = id } = props; + const { value = id, onKeyDown, onClick } = props; const focusableItems = useListContext_unstable(ctx => ctx.focusableItems); const registerItem = useListContext_unstable(ctx => ctx.registerItem); @@ -38,22 +38,62 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { + onKeyDown?.(e); + + // Compare targets to make sure this only triggers when the event is fired on the list item + // and not on a button inside + if (e.defaultPrevented || e.target !== e.currentTarget) { + return; + } + + if (e.key === ' ') { + e.preventDefault(); + selection.toggleItem(e, value); + } + }); + + const handleClick: typeof onClick = useEventCallback(e => { + onClick?.(e); + + if (e.defaultPrevented) { + return; + } + + if (selectable) { + selection.toggleItem(e, value); + } + }); + + const ariaLabel = selectable + ? selection.isSelected(value) + ? `${props['aria-label']}, selected` + : `${props['aria-label']}, not selected` + : props['aria-label']; + const root = slot.always( - getNativeElementProps('div', { + getNativeElementProps('li', { ref: useMergedRefs(ref, innerRef), - role: 'listitem', - tabIndex: focusableItems ? 0 : undefined, + // role: selectable ? 'option' : 'listitem', + role: 'option', + tabIndex: focusableItems || selectable ? 0 : undefined, + 'aria-selected': selectable ? selection.isSelected(value) : undefined, + id: value, ...focusableGroupAttrs, ...props, + onKeyDown: handleKeyDown, + onClick: handleClick, + // 'aria-live': 'polite', + 'aria-label': ariaLabel, }), - { elementType: 'div' }, + { elementType: 'li' }, ); const checkbox = slot.optional(props.checkbox, { renderByDefault: selectable, elementType: Checkbox, defaultProps: { - tabIndex: 0, + tabIndex: -1, checked: selection.isSelected(value), onChange: e => selection.toggleItem(e, value), }, diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 745c3e95cd1e29..b3d39a9f3d1298 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -14,6 +14,7 @@ export const listItemClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { + position: 'relative', ...createCustomFocusIndicatorStyle( { ...shorthands.outline('2px', 'solid', tokens.colorStrokeFocus2), @@ -25,6 +26,7 @@ const useStyles = makeStyles({ rootSelectable: { display: 'flex', + cursor: 'pointer', }, }); diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index a8bee8be87f722..9276a9a5d67e9a 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -1,6 +1,6 @@ import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; import * as React from 'react'; -import { ListFeaturesState, ListSelectionState, UseListSelectionOptions } from './types'; +import { ListFeaturesState, ListSelectionState } from './types'; export const defaultListSelectionState: ListSelectionState = { isSelected: () => false, diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index bccd5d26b7ed48..8ac473d8d64db4 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -55,79 +55,114 @@ const useStyles = makeStyles({ display: 'flex', ...shorthands.gap('8px'), }, + selectedMark: { + position: 'absolute', + left: '20px', + top: '20px', + zIndex: 1, + fontSize: '24px', + }, }); -const CardExample = (props: CardProps) => { +const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { const styles = useStyles(); return ( - - } - > - Presentation Preview - + + {props.selected &&
} + + } + > + Presentation Preview + - iOS App Prototype} - description={You created 53m ago} - action={ -
- - - - + + + -
- } - /> -
+ + + { + e.preventDefault(); + alert('Clicked menu item'); + }} + > + About + + { + e.preventDefault(); + alert('Clicked menu item'); + }} + > + Uninstall + + { + e.preventDefault(); + alert('Clicked menu item'); + }} + > + Block + + + + + + } + /> +
+
); }; export const ListMultipleActions = (props: Partial) => { const classes = useStyles(); + + const [selectedItems, setSelectedItems] = React.useState>([]); + return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - + setSelectedItems(Array.from(data.selectedItems))} + selectionMode="multiselect" + > + + + + + + + + + ); }; diff --git a/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx deleted file mode 100644 index bb593657945272..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/ListOverflowing.stories.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { List, ListItem } from '@fluentui/react-list-preview'; -import * as React from 'react'; -import names from './names'; - -const useStyles = makeStyles({ - wrapper: { - maxHeight: '300px', - overflowY: 'auto', - ...shorthands.padding('2px'), - }, - button: { - display: 'block', - width: '100%', - textAlign: 'left', - }, -}); - -export const ListOverflowing = () => { - const classes = useStyles(); - return ( - <> -

List with focusable buttons

-
- - {names.map(name => ( - - - - ))} - -
-

List with focusable listitem

-
- - {names.map(name => ( - alert(name)}> - - - ))} - -
-

List with built in button slot

-
- - {names.map(name => ( - alert(name) }}> - - - ))} - -
- - ); -}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx index 19f05989cb3a3e..e7ea857deb98a6 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionInternal.stories.tsx @@ -52,7 +52,7 @@ export const ListSelectionInternal = () => { > {items.map(({ name, avatar }) => { return ( - + { }, }} /> + ); })} diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 20c3efe91da5ae..905f0671943cf1 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -7,11 +7,9 @@ export { Default } from './ListDefault.stories'; export { ListHorizontal } from './ListHorizontal.stories'; export { ListGrid } from './ListGrid.stories'; export { ListWithAction } from './ListWithAction.stories'; -// export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; -export { ListOverflowing } from './ListOverflowing.stories'; +export { ListMultipleActions } from './ListMultipleActions.stories'; export { ListSelection } from './ListSelection.stories'; export { ListSelectionInternal } from './ListSelectionInternal.stories'; -export { ListWithMultipleActions } from './ListWithMultipleActions.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; From 6635c23d9005ad4de232d5cacad5c25c59f67448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 2 Nov 2023 10:07:46 +0100 Subject: [PATCH 014/136] actually render li, rename --- .../react-list-preview/src/components/List/List.types.ts | 2 +- .../src/components/ListItem/useListItem.ts | 2 +- ...al.stories.tsx => ListSelectionUncontrolled.stories.tsx} | 6 +++--- .../react-list-preview/stories/List/index.stories.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename packages/react-components/react-list-preview/stories/List/{ListSelectionInternal.stories.tsx => ListSelectionUncontrolled.stories.tsx} (89%) diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 5c7e5c1c759eaa..32f2719d206bc5 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -34,7 +34,7 @@ export type ListProps = ComponentProps & { selectable?: boolean; selectionMode?: SelectionMode; onSelectionChange?: (event: React.SyntheticEvent, data: OnSelectionChangeData) => void; - componentRef: React.Ref; + componentRef?: React.Ref; }; export type ListContextValue = { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts index e2def0be55de90..44590f7f82d709 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -101,7 +101,7 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { +export const ListSelectionUncontrolled = () => { const classes = useStyles(); const [currentIndex, setCurrentIndex] = React.useState(4); diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 905f0671943cf1..9563c4657e140b 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -9,7 +9,7 @@ export { ListGrid } from './ListGrid.stories'; export { ListWithAction } from './ListWithAction.stories'; export { ListMultipleActions } from './ListMultipleActions.stories'; export { ListSelection } from './ListSelection.stories'; -export { ListSelectionInternal } from './ListSelectionInternal.stories'; +export { ListSelectionUncontrolled as ListSelectionInternal } from './ListSelectionUncontrolled.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; From 9ff61faa3b88b107c816930d8099e428456ff263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 2 Nov 2023 11:51:11 +0100 Subject: [PATCH 015/136] controlled wip --- .../react-components/src/index.ts | 7 +++ .../src/components/List/List.types.ts | 8 ++-- .../src/components/List/useList.ts | 11 ++--- .../src/components/ListItem/ListItem.types.ts | 2 +- .../src/components/ListItem/useListItem.ts | 47 +++++++++---------- .../ListItem/useListItemStyles.styles.ts | 6 +-- .../react-list-preview/src/hooks/types.ts | 6 +++ .../src/hooks/useListSelection.tsx | 18 +++++++ .../List/ListMultipleActions.stories.tsx | 1 - .../stories/List/index.stories.tsx | 3 +- .../react-utilities/src/index.ts | 6 +-- 11 files changed, 71 insertions(+), 44 deletions(-) diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index f8e9e60a5e5d2a..de798a0c3cfdbb 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -111,6 +111,7 @@ export { useIsSSR, useMergedRefs, useScrollbarWidth, + useSelection, } from '@fluentui/react-utilities'; export type { ComponentProps, @@ -124,6 +125,12 @@ export type { SlotClassNames, SlotPropsRecord, SlotRenderFunction, + OnSelectionChangeCallback, + OnSelectionChangeData, + SelectionHookParams, + SelectionItemId, + SelectionMethods, + SelectionMode, } from '@fluentui/react-utilities'; // Components diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 32f2719d206bc5..33d9bb69bf31b4 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -5,6 +5,7 @@ import type { OnSelectionChangeData, Slot, SelectionMode, + SelectionItemId, } from '@fluentui/react-utilities'; import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; import { ListSelectionState } from '../../hooks/types'; @@ -40,10 +41,9 @@ export type ListProps = ComponentProps & { export type ListContextValue = { focusableItems: boolean; items: Array<{ id: string | number }>; - registerItem: (id: string | number, ref: React.RefObject) => void; - deregisterItem: (id: string | number, ref: React.RefObject) => void; - selection: ListSelectionState; - selectable: boolean; + selection?: ListSelectionState; + registerItem?: (id: string | number, ref: React.RefObject) => void; + deregisterItem?: (id: string | number, ref: React.RefObject) => void; }; export type ListContextValues = { diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 661ba8a8a4ea2f..64841e8b9becfa 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -65,9 +65,8 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): root: slot.always( getNativeElementProps('ul', { ref, - role: selectable ? 'listbox' : 'list', tabIndex: -1, - 'aria-multiselectable': selectable ? true : undefined, // TODO not true for single select + ...selection.getListProps(), ...arrowNavigationAttributes, ...props, }), @@ -77,9 +76,9 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): // context: focusableItems, items, - registerItem, - deregisterItem, - selectable, - selection, + registerItem: selectable ? registerItem : undefined, + deregisterItem: selectable ? deregisterItem : undefined, + // only pass down selection state if its handled internally, otherwise just report the events + selection: selectable ? selection : undefined, }; }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 0934cace697fd3..4eab09a49a1c52 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -16,6 +16,6 @@ export type ListItemProps = ComponentProps & { /** * State used in rendering ListItem */ -export type ListItemState = ComponentState & { selectable: boolean }; +export type ListItemState = ComponentState & {}; // TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ListItemProps. // & Required> diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts index 44590f7f82d709..90eeb3198c9fd5 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts @@ -19,20 +19,19 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref ctx.focusableItems); + const selection = useListContext_unstable(ctx => ctx.selection); const registerItem = useListContext_unstable(ctx => ctx.registerItem); const deregisterItem = useListContext_unstable(ctx => ctx.deregisterItem); - const selectable = useListContext_unstable(ctx => ctx.selectable); - const selection = useListContext_unstable(ctx => ctx.selection); const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); const innerRef = React.useRef(null); React.useEffect(() => { - registerItem(value, innerRef); + registerItem?.(value, innerRef); return () => { - deregisterItem(value, innerRef); + deregisterItem?.(value, innerRef); }; // Always make sure the dependencies are stable across rerenders, otherwise we go // in a loop of registering and deregistering. @@ -41,6 +40,11 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { onKeyDown?.(e); + // Return early if selection state is not provided = not selectable or controlled + if (!selection) { + return; + } + // Compare targets to make sure this only triggers when the event is fired on the list item // and not on a button inside if (e.defaultPrevented || e.target !== e.currentTarget) { @@ -56,46 +60,40 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { onClick?.(e); - if (e.defaultPrevented) { + // Return early if selection state is not provided = or controlled + if (!selection) { return; } - if (selectable) { - selection.toggleItem(e, value); + if (e.defaultPrevented) { + return; } - }); - const ariaLabel = selectable - ? selection.isSelected(value) - ? `${props['aria-label']}, selected` - : `${props['aria-label']}, not selected` - : props['aria-label']; + selection.toggleItem(e, value); + }); const root = slot.always( getNativeElementProps('li', { ref: useMergedRefs(ref, innerRef), - // role: selectable ? 'option' : 'listitem', - role: 'option', - tabIndex: focusableItems || selectable ? 0 : undefined, - 'aria-selected': selectable ? selection.isSelected(value) : undefined, + tabIndex: focusableItems ? 0 : undefined, + role: 'listitem', id: value, + ...(selection ? selection.getListItemProps(value) : {}), ...focusableGroupAttrs, ...props, - onKeyDown: handleKeyDown, - onClick: handleClick, - // 'aria-live': 'polite', - 'aria-label': ariaLabel, + onKeyDown: selection ? handleKeyDown : onKeyDown, + onClick: selection ? handleClick : onClick, }), { elementType: 'li' }, ); const checkbox = slot.optional(props.checkbox, { - renderByDefault: selectable, + renderByDefault: !!selection, elementType: Checkbox, defaultProps: { tabIndex: -1, - checked: selection.isSelected(value), - onChange: e => selection.toggleItem(e, value), + checked: selection?.isSelected(value), + onChange: e => selection?.toggleItem(e, value), }, }); @@ -106,7 +104,6 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref = { items: TItem[]; @@ -11,6 +13,10 @@ export type ListSelectionState = { selectItem: (e: React.SyntheticEvent, id: string | number) => void; clearSelection: (e: React.SyntheticEvent) => void; toggleAllItems: (e: React.SyntheticEvent) => void; + getListProps: () => Pick, 'role' | 'aria-multiselectable'>; + getListItemProps: ( + value: string | number, + ) => Pick, 'tabIndex' | 'role' | 'aria-selected'>; selectedItems: Set; }; diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 9276a9a5d67e9a..918d8c84952b5c 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -9,6 +9,8 @@ export const defaultListSelectionState: ListSelectionState = { deselectItem: () => undefined, clearSelection: () => undefined, toggleAllItems: () => undefined, + getListProps: () => ({}), + getListItemProps: () => ({}), selectedItems: new Set(), }; @@ -58,6 +60,20 @@ export function useListSelectionState( const clearSelection: ListSelectionState['clearSelection'] = useEventCallback(e => selectionMethods.clearItems(e)); + const getListProps: ListSelectionState['getListProps'] = () => { + return { + role: 'listbox', + 'aria-multiselectable': selectionMode === 'multiselect' ? true : undefined, + }; + }; + const getListItemProps: ListSelectionState['getListItemProps'] = value => { + return { + tabIndex: 0, + role: 'option', + 'aria-selected': selectionMethods.isSelected(value) || undefined, + }; + }; + return { ...listState, selection: { @@ -69,6 +85,8 @@ export function useListSelectionState( setSelectedItems, isSelected: (id: string | number) => selectionMethods.isSelected(id), clearSelection, + getListProps, + getListItemProps, }, }; } diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index 8ac473d8d64db4..401b97df234487 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -8,7 +8,6 @@ import { makeStyles, Menu, MenuItem, - MenuItemLink, MenuList, MenuPopover, MenuTrigger, diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 9563c4657e140b..e987c9f41d20f2 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -9,7 +9,8 @@ export { ListGrid } from './ListGrid.stories'; export { ListWithAction } from './ListWithAction.stories'; export { ListMultipleActions } from './ListMultipleActions.stories'; export { ListSelection } from './ListSelection.stories'; -export { ListSelectionUncontrolled as ListSelectionInternal } from './ListSelectionUncontrolled.stories'; +export { ListSelectionUncontrolled } from './ListSelectionUncontrolled.stories'; +export { ListSelectionControlled } from './ListSelectionControlled.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index 97fc981ebeb745..75c42027628e9a 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -72,12 +72,12 @@ export type { FluentTriggerComponent, TriggerProps } from './trigger/index'; export type { NativeTouchOrMouseEvent, ReactTouchOrMouseEvent, TouchOrMouseEvent } from './events/index'; export { isTouchEvent, isMouseEvent, getEventClientCoords } from './events/index'; +export { useSelection } from './selection/index'; export type { - SelectionMode, OnSelectionChangeCallback, OnSelectionChangeData, - SelectionItemId, SelectionHookParams, + SelectionItemId, SelectionMethods, + SelectionMode, } from './selection/index'; -export { useSelection } from './selection/index'; From 75b9f75cf4ee5920706a2e5cd2840a5e457e75e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 3 Nov 2023 11:07:39 +0100 Subject: [PATCH 016/136] checkmark, defaultSelected --- .../react-list-preview/package.json | 1 + .../src/components/List/List.types.ts | 1 + .../src/components/List/useList.ts | 5 +- .../components/List/useListStyles.styles.ts | 2 + .../src/components/ListItem/ListItem.types.ts | 3 +- .../components/ListItem/renderListItem.tsx | 3 +- .../{useListItem.ts => useListItem.tsx} | 16 ++-- .../ListItem/useListItemStyles.styles.ts | 23 ++++- .../src/hooks/useListSelection.tsx | 4 + .../List/ListMultipleActions.stories.tsx | 19 ++-- .../stories/List/ListSelection.stories.tsx | 62 ------------- .../List/ListSelectionControlled.stories.tsx | 90 +++++++++++++++++++ .../ListSelectionUncontrolled.stories.tsx | 29 +++--- .../stories/List/index.stories.tsx | 1 - 14 files changed, 156 insertions(+), 103 deletions(-) rename packages/react-components/react-list-preview/src/components/ListItem/{useListItem.ts => useListItem.tsx} (90%) delete mode 100644 packages/react-components/react-list-preview/stories/List/ListSelection.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 2945c9740ad6d0..1494e7a59afaf6 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -34,6 +34,7 @@ "@fluentui/react-button": "^9.3.34", "@fluentui/react-checkbox": "^9.1.35", "@fluentui/react-context-selector": "^9.1.31", + "@fluentui/react-icons": "^2.0.207", "@fluentui/react-jsx-runtime": "^9.0.3", "@fluentui/react-tabster": "^9.12.5", "@fluentui/react-theme": "^9.1.11", diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 33d9bb69bf31b4..188962a2fc528d 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -34,6 +34,7 @@ export type ListProps = ComponentProps & { focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode; + defaultSelectedItems?: SelectionItemId[]; onSelectionChange?: (event: React.SyntheticEvent, data: OnSelectionChangeData) => void; componentRef?: React.Ref; }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 64841e8b9becfa..c639ee5b7462c8 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -31,6 +31,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): customArrowNavigationOptions, selectable = false, selectionMode = 'multiselect', + defaultSelectedItems, componentRef, onSelectionChange, } = props; @@ -43,7 +44,9 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): const [items, setItems] = React.useState>([]); - const { selection } = useListFeatures({ items }, [useListSelection({ onSelectionChange, selectionMode })]); + const { selection } = useListFeatures({ items }, [ + useListSelection({ defaultSelectedItems, onSelectionChange, selectionMode }), + ]); useComponentRef(componentRef, selection); const registerItem = useEventCallback((id: string | number) => { diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts index bf2e9ba241ec7d..8b0ee5d0784f0b 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -15,6 +15,8 @@ const useStyles = makeStyles({ root: { ...shorthands.padding(0), ...shorthands.margin(0), + textIndent: 0, + listStyleType: 'none', }, rootHorizontal: { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 4eab09a49a1c52..705d9abc149486 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -1,9 +1,8 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { Checkbox } from '@fluentui/react-checkbox'; export type ListItemSlots = { root: NonNullable>; - checkbox?: Slot; + checkmark?: Slot<'div'>; }; /** diff --git a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx index 29a59094109e80..2fa02efc7d1cb8 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx @@ -11,10 +11,9 @@ import type { ListItemState, ListItemSlots } from './ListItem.types'; export const renderListItem_unstable = (state: ListItemState) => { assertSlots(state); - // TODO Add additional slots in the appropriate place return ( - {state.checkbox ? : null} + {state.checkmark ? : null} {state.root.children} ); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx similarity index 90% rename from packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts rename to packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 90eeb3198c9fd5..501c26a66bb329 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; import { getNativeElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; -import { Checkbox } from '@fluentui/react-checkbox'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; +import { Checkmark16Filled } from '@fluentui/react-icons'; /** * Create the state required to render ListItem. @@ -87,23 +87,19 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref : null }, renderByDefault: !!selection, - elementType: Checkbox, - defaultProps: { - tabIndex: -1, - checked: selection?.isSelected(value), - onChange: e => selection?.toggleItem(e, value), - }, + elementType: 'span', }); const state: ListItemState = { components: { root: 'li', - checkbox: Checkbox, + checkmark: 'span', }, root, - checkbox, + checkmark, }; return state; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 444b3477c12f73..dbbacf3f014cdd 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -6,7 +6,7 @@ import { tokens } from '@fluentui/react-theme'; export const listItemClassNames: SlotClassNames = { root: 'fui-ListItem', - checkbox: 'fui-ListItem__checkbox', + checkmark: 'fui-ListItem__checkmark', }; /** @@ -14,7 +14,10 @@ export const listItemClassNames: SlotClassNames = { */ const useStyles = makeStyles({ root: { - position: 'relative', + ...shorthands.padding(0), + ...shorthands.margin(0), + textIndent: 0, + listStyleType: 'none', ...createCustomFocusIndicatorStyle( { ...shorthands.outline('2px', 'solid', tokens.colorStrokeFocus2), @@ -28,6 +31,14 @@ const useStyles = makeStyles({ display: 'flex', //TODO needed? cursor: 'pointer', }, + + checkmark: { + display: 'flex', + alignSelf: 'center', + marginRight: '8px', + width: '16px', + height: '16px', + }, }); /** @@ -42,8 +53,12 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState state.root.className, ); - if (state.checkbox) { - state.checkbox.className = mergeClasses(listItemClassNames.checkbox, state.checkbox?.className); + if (state.checkmark) { + state.checkmark.className = mergeClasses( + listItemClassNames.checkmark, + styles.checkmark, + state.checkmark?.className, + ); } return state; diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 918d8c84952b5c..66921eb8824b36 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -1,3 +1,4 @@ +import { Checkmark16Filled } from '@fluentui/react-icons'; import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; import * as React from 'react'; import { ListFeaturesState, ListSelectionState } from './types'; @@ -71,6 +72,9 @@ export function useListSelectionState( tabIndex: 0, role: 'option', 'aria-selected': selectionMethods.isSelected(value) || undefined, + checkmark: { + children: selectionMethods.isSelected(value) ? : null, + }, }; }; diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index 401b97df234487..2cf62cf4200633 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -32,6 +32,10 @@ const useStyles = makeStyles({ ...shorthands.gap('16px'), }, + listItem: { + position: 'relative', + }, + caption: { color: tokens.colorNeutralForeground3, }, @@ -54,12 +58,11 @@ const useStyles = makeStyles({ display: 'flex', ...shorthands.gap('8px'), }, - selectedMark: { + checkmark: { position: 'absolute', - left: '20px', - top: '20px', + left: '10px', + top: '10px', zIndex: 1, - fontSize: '24px', }, }); @@ -67,8 +70,12 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = const styles = useStyles(); return ( - - {props.selected &&
} + ({ - name, - id: name, - avatar: - 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', -})); - -const useStyles = makeStyles({ - wrapper: { - maxHeight: '300px', - overflowY: 'auto', - ...shorthands.padding('2px'), - }, - button: { - ...shorthands.padding(0), - }, -}); - -export const ListSelection = () => { - const classes = useStyles(); - - const { items, selection } = useListFeatures({ items: origItems }, [useListSelection()]); - const { isSelected, toggleItem, selectItem, deselectItem, clearSelection } = selection; - console.log('___', selection); - - return ( -
- - - - - - {items.map(({ name, avatar, id }) => ( - toggleItem(e, id) }}> - - - - ))} - -
- ); -}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx new file mode 100644 index 00000000000000..cce2122707b74d --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -0,0 +1,90 @@ +import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { Checkmark16Filled } from '@fluentui/react-icons'; +import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; + +import * as React from 'react'; +import names from './names'; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const origItems: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + buttonControls: { + display: 'flex', + columnGap: '8px', + marginBottom: '16px', + }, + button: { + ...shorthands.padding(0), + }, +}); + +export const ListSelectionControlled = () => { + const classes = useStyles(); + const [currentIndex, setCurrentIndex] = React.useState(4); + + const items = React.useMemo(() => { + return origItems.slice(0, currentIndex); + }, [currentIndex]); + + const { selection } = useListFeatures({ items }, [ + useListSelection({ + selectionMode: 'multiselect', + onSelectionChange: (_, data) => console.log(data.selectedItems), + }), + ]); + + return ( +
+
+ + +
+ + + {items.map(({ name, avatar }) => { + return ( + selection.toggleItem(e, name)} + onKeyDown={e => { + if (e.key === ' ') { + e.preventDefault(); + selection.toggleItem(e, name); + } + }} + > + + + ); + })} + +
+ ); +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index 72f1264ca4c536..4736c2e5873be8 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -22,6 +22,11 @@ const useStyles = makeStyles({ overflowY: 'auto', ...shorthands.padding('2px'), }, + buttonControls: { + display: 'flex', + columnGap: '8px', + marginBottom: '16px', + }, button: { ...shorthands.padding(0), }, @@ -35,20 +40,22 @@ export const ListSelectionUncontrolled = () => { return origItems.slice(0, currentIndex); }, [currentIndex]); + const defaultSelectedItems = ['Demetra Manwaring', 'Sonya Farner']; + const ref = React.useRef(null); - const [selectedItems, setSelectedItems] = React.useState>([]); return (
- - -
Selected: {selectedItems.join(', ')}
- +
+ + +
setSelectedItems(Array.from(data.selectedItems))} componentRef={ref} + defaultSelectedItems={defaultSelectedItems} + // this is just a notification to the parent component, it doesn't control the state + onSelectionChange={(_, data) => console.log(data.selectedItems)} > {items.map(({ name, avatar }) => { return ( @@ -63,14 +70,6 @@ export const ListSelectionUncontrolled = () => { }, }} /> - ); })} diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index e987c9f41d20f2..10cb51b2dd69c2 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -8,7 +8,6 @@ export { ListHorizontal } from './ListHorizontal.stories'; export { ListGrid } from './ListGrid.stories'; export { ListWithAction } from './ListWithAction.stories'; export { ListMultipleActions } from './ListMultipleActions.stories'; -export { ListSelection } from './ListSelection.stories'; export { ListSelectionUncontrolled } from './ListSelectionUncontrolled.stories'; export { ListSelectionControlled } from './ListSelectionControlled.stories'; export { VirtualizedList } from './VirtualizedList.stories'; From 9d5478b1117e379f3e1f4e6a7b46dfade6ba2d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 3 Nov 2023 11:24:31 +0100 Subject: [PATCH 017/136] docs --- .../List/ListMultipleActions.stories.tsx | 2 ++ .../List/ListSelectionControlled.stories.tsx | 19 ++++++++++++++++++- .../ListSelectionUncontrolled.stories.tsx | 17 ++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index 2cf62cf4200633..074872611f0814 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -179,6 +179,8 @@ ListMultipleActions.parameters = { story: [ 'List can have a grid layout. What this means is that the wrapper will have `display: grid` applied to it. Also the arrow navigation will work horizontaly and vertically.', "\nBy default, the grid doesn't have any columns defined. It is up to the user to define the columns using CSS. Please refer to the example below.", + '', + 'You can also select the items with a click or using a Spacebar key.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index cce2122707b74d..f4153cb83a811d 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -1,5 +1,4 @@ import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { Checkmark16Filled } from '@fluentui/react-icons'; import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; import * as React from 'react'; @@ -88,3 +87,21 @@ export const ListSelectionControlled = () => {
); }; + +ListSelectionControlled.parameters = { + docs: { + description: { + story: [ + 'In the controlled approach you are in charge of the selection state. First, you create the state using `useListFeatures` in combination with `useListSelection` hooks.', + '', + 'This will return a List state object with `selection` property. You can then use the `selection` object to control the selection state.', + '', + 'In this case, you are in control of deciding what item should be selected and when, including listening on events and calling the `selection` methods.', + '', + 'The `selection` object also provides utility functions like `getListProps` and `getListItemProps`. These functions return props that should be applied to the List and ListItems respectively to ensure the right accessibility attributes are passed.', + '', + 'The `getListItemProps` also configures the `checkmark` hook to visualize the selection state of the item. Feel free to override this behavior by passing your own `checkmark` prop to the ListItem.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index 4736c2e5873be8..591c90f41e30cc 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -40,7 +40,7 @@ export const ListSelectionUncontrolled = () => { return origItems.slice(0, currentIndex); }, [currentIndex]); - const defaultSelectedItems = ['Demetra Manwaring', 'Sonya Farner']; + const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; const ref = React.useRef(null); @@ -77,3 +77,18 @@ export const ListSelectionUncontrolled = () => { ); }; + +ListSelectionUncontrolled.parameters = { + docs: { + description: { + story: [ + 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', + '', + 'You can pass `selectable` prop inside of the List component to get built-in selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state.', + '', + "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", + 'your list items and even store and retrieve them separately.', + ].join('\n'), + }, + }, +}; From 2eb24a8b880b085ee2fc2898c5f0114316cbcac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 3 Nov 2023 11:32:15 +0100 Subject: [PATCH 018/136] tokens --- .../src/components/List/useListStyles.styles.ts | 3 --- .../components/ListItem/useListItemStyles.styles.ts | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts index 8b0ee5d0784f0b..924f6d64a581f5 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -49,8 +49,5 @@ export const useListStyles_unstable = (state: ListState): ListState => { state.root.className, ); - // TODO Add class names to slots, for example: - // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className); - return state; }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index dbbacf3f014cdd..54f8d35142c522 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -20,7 +20,7 @@ const useStyles = makeStyles({ listStyleType: 'none', ...createCustomFocusIndicatorStyle( { - ...shorthands.outline('2px', 'solid', tokens.colorStrokeFocus2), + ...shorthands.outline(tokens.strokeWidthThick, 'solid', tokens.colorStrokeFocus2), ...shorthands.borderRadius(tokens.borderRadiusMedium), }, { selector: 'focus' }, @@ -28,16 +28,16 @@ const useStyles = makeStyles({ }, rootClickable: { - display: 'flex', //TODO needed? + display: 'flex', cursor: 'pointer', }, checkmark: { display: 'flex', alignSelf: 'center', - marginRight: '8px', - width: '16px', - height: '16px', + marginRight: tokens.spacingHorizontalS, + width: tokens.spacingHorizontalL, + height: tokens.spacingVerticalL, }, }); From eabf9ac471c10c15cce4c89c2e725f408e9cca50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 6 Nov 2023 15:18:05 +0100 Subject: [PATCH 019/136] A lot of optimizations --- .../src/components/List/listContext.ts | 12 +-- .../components/List/useListContextValues.ts | 5 +- .../src/components/ListItem/useListItem.tsx | 44 +++++----- .../ListItem/useListItemStyles.styles.ts | 1 - .../src/hooks/useListSelection.tsx | 25 +++++- .../List/ListSelectionControlled.stories.tsx | 81 ++++++++++++------- .../FluentProvider/useFluentProvider.ts | 5 +- 7 files changed, 109 insertions(+), 64 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index ef645e8d7e4230..9e626dda76408d 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -2,15 +2,15 @@ import { createContext, useContextSelector } from '@fluentui/react-context-selec import type { ContextSelector } from '@fluentui/react-context-selector'; import { ListContextValue } from './List.types'; +const noop = () => { + /*noop*/ +}; + export const listContextDefaultValue: ListContextValue = { focusableItems: false, items: [], - registerItem: () => { - /* noop */ - }, - deregisterItem: () => { - /* noop */ - }, + registerItem: noop, + deregisterItem: noop, selection: undefined, }; diff --git a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts index 71ab5a7758681c..01c8d65bf340ec 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { ListContextValues, ListState } from './List.types'; export function useListContextValues_unstable(state: ListState): ListContextValues { - const { focusableItems, items, registerItem, deregisterItem, selectable, selection } = state; + const { focusableItems, items, registerItem, deregisterItem, selection } = state; const listContext = React.useMemo( () => ({ @@ -10,10 +10,9 @@ export function useListContextValues_unstable(state: ListState): ListContextValu items, registerItem, deregisterItem, - selectable, selection, }), - [focusableItems, items, registerItem, deregisterItem, selection, selectable], + [focusableItems, items, registerItem, deregisterItem, selection], ); return { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 501c26a66bb329..5814f35aa6f630 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,10 +1,19 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; -import { getNativeElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { + getNativeElementProps, + slot, + useEventCallback, + useId, + useMergedRefs, + usePrevious, +} from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; import { Checkmark16Filled } from '@fluentui/react-icons'; +const EMPTY_OBJECT = {}; + /** * Create the state required to render ListItem. * @@ -19,9 +28,12 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref ctx.focusableItems); - const selection = useListContext_unstable(ctx => ctx.selection); const registerItem = useListContext_unstable(ctx => ctx.registerItem); const deregisterItem = useListContext_unstable(ctx => ctx.deregisterItem); + const toggleItem = useListContext_unstable(ctx => ctx.selection?.toggleItem); + const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); + const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); + const selectionProps = useListContext_unstable(ctx => ctx.selection?.getListItemProps(value) || EMPTY_OBJECT); const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); @@ -40,11 +52,6 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { onKeyDown?.(e); - // Return early if selection state is not provided = not selectable or controlled - if (!selection) { - return; - } - // Compare targets to make sure this only triggers when the event is fired on the list item // and not on a button inside if (e.defaultPrevented || e.target !== e.currentTarget) { @@ -53,23 +60,18 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { onClick?.(e); - // Return early if selection state is not provided = or controlled - if (!selection) { - return; - } - if (e.defaultPrevented) { return; } - selection.toggleItem(e, value); + toggleItem?.(e, value); }); const root = slot.always( @@ -78,25 +80,25 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref : null }, - renderByDefault: !!selection, - elementType: 'span', + defaultProps: { children: isSelected ? : null }, + renderByDefault: isSelectionEnabled, + elementType: 'div', }); const state: ListItemState = { components: { root: 'li', - checkmark: 'span', + checkmark: 'div', }, root, checkmark, diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 54f8d35142c522..c1e7ff8935b34c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -33,7 +33,6 @@ const useStyles = makeStyles({ }, checkmark: { - display: 'flex', alignSelf: 'center', marginRight: tokens.spacingHorizontalS, width: tokens.spacingHorizontalL, diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 66921eb8824b36..6f40a2abcdde69 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -67,16 +67,33 @@ export function useListSelectionState( 'aria-multiselectable': selectionMode === 'multiselect' ? true : undefined, }; }; - const getListItemProps: ListSelectionState['getListItemProps'] = value => { + + const listPropsForSelected = React.useMemo(() => { return { tabIndex: 0, role: 'option', - 'aria-selected': selectionMethods.isSelected(value) || undefined, + 'aria-selected': true, checkmark: { - children: selectionMethods.isSelected(value) ? : null, + children: , }, }; - }; + }, []); + + const listPropsForNotSelected = React.useMemo(() => { + return { + tabIndex: 0, + role: 'option', + 'aria-selected': false, + checkmark: { + children: null, + }, + }; + }, []); + + const getListItemProps: ListSelectionState['getListItemProps'] = React.useCallback( + value => (selectionMethods.isSelected(value) ? listPropsForSelected : listPropsForNotSelected), + [listPropsForNotSelected, listPropsForSelected, selectionMethods], + ); return { ...listState, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index f4153cb83a811d..669c286c3eac15 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -1,7 +1,15 @@ -import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { + Button, + makeStyles, + Persona, + PresenceBadgeProps, + shorthands, + useEventCallback, +} from '@fluentui/react-components'; import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; import * as React from 'react'; +import { ListSelectionState } from '../../src/hooks/types'; import names from './names'; type Item = { @@ -33,6 +41,41 @@ const useStyles = makeStyles({ }, }); +// This component is memoized, i.e. it will only re-render if the props change. +// This is important for performance, as we don't want to re-render the entire list +// when the selection state changes. +const MyListItem: React.FC<{ + name: string; + avatar: string; + toggleItem: (e: React.SyntheticEvent, id: string) => void; + selectionProps: ReturnType; +}> = React.memo(({ name, avatar, toggleItem, selectionProps }) => { + const onClick = useEventCallback((e: React.MouseEvent) => toggleItem(e, name)); + const onKeyDown = useEventCallback((e: React.KeyboardEvent) => { + if (e.key === ' ') { + e.preventDefault(); + toggleItem(e, name); + } + }); + + return ( + + + + ); +}); + export const ListSelectionControlled = () => { const classes = useStyles(); const [currentIndex, setCurrentIndex] = React.useState(4); @@ -56,33 +99,15 @@ export const ListSelectionControlled = () => { - {items.map(({ name, avatar }) => { - return ( - selection.toggleItem(e, name)} - onKeyDown={e => { - if (e.key === ' ') { - e.preventDefault(); - selection.toggleItem(e, name); - } - }} - > - - - ); - })} + {items.map(({ name, avatar }) => ( + + ))} ); diff --git a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts index d30535579bbcb5..f56ab888953392 100644 --- a/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts +++ b/packages/react-components/react-provider/src/components/FluentProvider/useFluentProvider.ts @@ -16,6 +16,8 @@ import * as React from 'react'; import { useFluentProviderThemeStyleTag } from './useFluentProviderThemeStyleTag'; import type { FluentProviderProps, FluentProviderState } from './FluentProvider.types'; +const EMPTY_OBJECT = {}; + /** * Create the state required to render FluentProvider. * @@ -32,7 +34,8 @@ export const useFluentProvider_unstable = ( const parentContext = useFluent(); const parentTheme = useTheme(); const parentOverrides = useOverrides(); - const parentCustomStyleHooks: CustomStyleHooksContextValue = React.useContext(CustomStyleHooksContext) || {}; + const parentCustomStyleHooks: CustomStyleHooksContextValue = + React.useContext(CustomStyleHooksContext) || EMPTY_OBJECT; /** * TODO: add merge functions to "dir" merge, From 829dd73a17f7b72708279e94582ac66ea8f6f59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 6 Nov 2023 15:32:36 +0100 Subject: [PATCH 020/136] changelog --- ...ct-components-7a159b4e-4c9c-449c-a0c3-64e9d1826642.json | 7 +++++++ ...act-utilities-18dc963b-51db-49d1-aed0-04054579847f.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@fluentui-react-components-7a159b4e-4c9c-449c-a0c3-64e9d1826642.json create mode 100644 change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json diff --git a/change/@fluentui-react-components-7a159b4e-4c9c-449c-a0c3-64e9d1826642.json b/change/@fluentui-react-components-7a159b4e-4c9c-449c-a0c3-64e9d1826642.json new file mode 100644 index 00000000000000..9c32c38ab2ff06 --- /dev/null +++ b/change/@fluentui-react-components-7a159b4e-4c9c-449c-a0c3-64e9d1826642.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Export selection utils", + "packageName": "@fluentui/react-components", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json b/change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json new file mode 100644 index 00000000000000..6efe5d856ffde3 --- /dev/null +++ b/change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: Export selection utils", + "packageName": "@fluentui/react-utilities", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +} From bb038468ae8768110f7e0c27448ef74dff68e944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 6 Nov 2023 16:12:51 +0100 Subject: [PATCH 021/136] yarnlock --- yarn.lock | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/yarn.lock b/yarn.lock index 5f161ac10a7189..e47f31b41def8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1896,6 +1896,14 @@ "@uifabric/set-version" "^7.0.23" tslib "^1.10.0" +"@fluentui/react-icons@^2.0.207": + version "2.0.222" + resolved "https://registry.yarnpkg.com/@fluentui/react-icons/-/react-icons-2.0.222.tgz#5d1cec5f7fe1fe759b83a4cd4c68bf227885f628" + integrity sha512-3Qy9GPww9rj51mJ6iEGCqSBEDZ8qBK+FK0BdtcVF4LFxpnPbB45hEf2dZ6LBQbfuKgH8NB3QHRSky75DjrjfdA== + dependencies: + "@griffel/react" "^1.0.0" + tslib "^2.1.0" + "@fluentui/react-icons@^2.0.217": version "2.0.217" resolved "https://registry.yarnpkg.com/@fluentui/react-icons/-/react-icons-2.0.217.tgz#a99540d6bb79630519f7e4fbb39ec66171fa8c4a" From aea3ccc0a03865ae857629c7967789fda767f0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 6 Nov 2023 16:23:01 +0100 Subject: [PATCH 022/136] bump outdated deps --- .../react-list-preview/package.json | 16 ++++++++-------- yarn.lock | 8 -------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 1494e7a59afaf6..085df9e0470a8b 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -31,14 +31,14 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-button": "^9.3.34", - "@fluentui/react-checkbox": "^9.1.35", - "@fluentui/react-context-selector": "^9.1.31", - "@fluentui/react-icons": "^2.0.207", - "@fluentui/react-jsx-runtime": "^9.0.3", - "@fluentui/react-tabster": "^9.12.5", - "@fluentui/react-theme": "^9.1.11", - "@fluentui/react-utilities": "^9.13.0", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", "@griffel/react": "^1.5.14", "@swc/helpers": "^0.5.1" }, diff --git a/yarn.lock b/yarn.lock index e47f31b41def8a..5f161ac10a7189 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1896,14 +1896,6 @@ "@uifabric/set-version" "^7.0.23" tslib "^1.10.0" -"@fluentui/react-icons@^2.0.207": - version "2.0.222" - resolved "https://registry.yarnpkg.com/@fluentui/react-icons/-/react-icons-2.0.222.tgz#5d1cec5f7fe1fe759b83a4cd4c68bf227885f628" - integrity sha512-3Qy9GPww9rj51mJ6iEGCqSBEDZ8qBK+FK0BdtcVF4LFxpnPbB45hEf2dZ6LBQbfuKgH8NB3QHRSky75DjrjfdA== - dependencies: - "@griffel/react" "^1.0.0" - tslib "^2.1.0" - "@fluentui/react-icons@^2.0.217": version "2.0.217" resolved "https://registry.yarnpkg.com/@fluentui/react-icons/-/react-icons-2.0.217.tgz#a99540d6bb79630519f7e4fbb39ec66171fa8c4a" From 78c75c51af79a8ef9c074bd481d41e4e04f8dbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 6 Nov 2023 21:50:46 +0100 Subject: [PATCH 023/136] api --- .../etc/react-list-preview.api.md | 102 ++++++++++++++++-- .../src/components/ListItem/useListItem.tsx | 11 +- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 234a14766e474a..e53511470c8a34 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -4,12 +4,25 @@ ```ts +/// + +import { Button } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { OnSelectionChangeData } from '@fluentui/react-utilities'; import * as React_2 from 'react'; +import { SelectionHookParams } from '@fluentui/react-utilities'; +import { SelectionItemId } from '@fluentui/react-utilities'; +import type { SelectionMode as SelectionMode_2 } from '@fluentui/react-utilities'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; + +// @public (undocumented) +export type IList = { + selection: ListState['selection']; +}; // @public export const List: ForwardRefComponent; @@ -20,46 +33,121 @@ export const listClassNames: SlotClassNames; // @public export const ListItem: ForwardRefComponent; +// @public +export const ListItemButton: ForwardRefComponent; + +// @public (undocumented) +export const listItemButtonClassNames: SlotClassNames; + +// @public +export type ListItemButtonProps = ComponentProps & {}; + +// @public (undocumented) +export type ListItemButtonSlots = { + root: Slot; +}; + +// @public +export type ListItemButtonState = ComponentState; + // @public (undocumented) export const listItemClassNames: SlotClassNames; // @public -export type ListItemProps = ComponentProps & {}; +export type ListItemProps = ComponentProps & { + value?: string | number; +}; // @public (undocumented) export type ListItemSlots = { - root: Slot<'div'>; + root: NonNullable>; + checkmark?: Slot<'div'>; }; // @public -export type ListItemState = ComponentState; +export type ListItemState = ComponentState & {}; + +// @public (undocumented) +export enum ListLayout { + // (undocumented) + Grid = "grid", + // (undocumented) + Horizontal = "horizontal", + // (undocumented) + Vertical = "vertical" +} // @public -export type ListProps = ComponentProps & {}; +export type ListProps = ComponentProps & { + layout?: ListLayout; + customArrowNavigationOptions?: Partial; + focusableItems?: boolean; + selectable?: boolean; + selectionMode?: SelectionMode_2; + defaultSelectedItems?: SelectionItemId[]; + onSelectionChange?: (event: React_2.SyntheticEvent, data: OnSelectionChangeData) => void; + componentRef?: React_2.Ref; +}; // @public (undocumented) export type ListSlots = { - root: Slot<'div'>; + root: NonNullable>; }; // @public -export type ListState = ComponentState; +export type ListState = ComponentState & Required> & ListContextValue; // @public -export const renderList_unstable: (state: ListState) => JSX.Element; +export const renderList_unstable: (state: ListState, contextValues: ListContextValues) => JSX.Element; // @public export const renderListItem_unstable: (state: ListItemState) => JSX.Element; +// @public +export const renderListItemButton_unstable: (state: ListItemButtonState) => JSX.Element; + // @public export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; +// @public (undocumented) +export function useListFeatures(options: UseListFeaturesOptions, plugins?: ListFeaturePlugin[]): { + items: TItem[]; + selection: ListSelectionState; +}; + // @public export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref) => ListItemState; +// @public +export const useListItemButton_unstable: (props: ListItemButtonProps, ref: React_2.Ref) => ListItemButtonState; + +// @public +export const useListItemButtonStyles_unstable: (state: ListItemButtonState) => ListItemButtonState; + // @public export const useListItemStyles_unstable: (state: ListItemState) => ListItemState; +// @public (undocumented) +export function useListSelection(options?: SelectionHookParams): (listState: ListFeaturesState) => { + selection: { + selectedItems: Set; + toggleItem: (e: React_2.SyntheticEvent, id: string | number) => void; + toggleAllItems: (e: React_2.SyntheticEvent) => void; + deselectItem: (e: React_2.SyntheticEvent, id: string | number) => void; + selectItem: (e: React_2.SyntheticEvent, id: string | number) => void; + setSelectedItems: React_2.Dispatch>>; + isSelected: (id: string | number) => boolean; + clearSelection: (e: React_2.SyntheticEvent) => void; + getListProps: () => Pick, "role" | "aria-multiselectable">; + getListItemProps: (value: string | number) => Pick, "tabIndex" | "role" | "aria-selected">; + }; + items: TItem[]; +}; + // @public export const useListStyles_unstable: (state: ListState) => ListState; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 5814f35aa6f630..ec9281d5dc78fb 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,13 +1,6 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; -import { - getNativeElementProps, - slot, - useEventCallback, - useId, - useMergedRefs, - usePrevious, -} from '@fluentui/react-utilities'; +import { getNativeElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; import { Checkmark16Filled } from '@fluentui/react-icons'; @@ -45,7 +38,7 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { deregisterItem?.(value, innerRef); }; - // Always make sure the dependencies are stable across rerenders, otherwise we go + // Always make sure the dependencies are stable across re-renders, otherwise we go // in a loop of registering and deregistering. }, [innerRef, value, registerItem, deregisterItem]); From 91d3fbf73c80029f87d82c55fecb0522ad5a927d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 7 Nov 2023 16:05:55 +0100 Subject: [PATCH 024/136] kick off spec, add MIGRATION.md --- .../react-list-preview/docs/MIGRATION.md | 207 ++++++++++++++++++ .../react-list-preview/docs/Spec.md | 16 +- 2 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 packages/react-components/react-list-preview/docs/MIGRATION.md diff --git a/packages/react-components/react-list-preview/docs/MIGRATION.md b/packages/react-components/react-list-preview/docs/MIGRATION.md new file mode 100644 index 00000000000000..1915c413383110 --- /dev/null +++ b/packages/react-components/react-list-preview/docs/MIGRATION.md @@ -0,0 +1,207 @@ +# List migration + +## Migration from v8 + +### Composition over configuration + +Compared to its v8 counterpart, the v9 `List` uses composition over configuration when it comes to rendering items, same as other components in Fluent UI React v9. This means that instead of passing an array of items to the `List` component, it's up to you to render `ListItem` components with appropriate content. + +Take this example in v8: + +```js +const items = [{ name: 'John' }, { name: 'Alice' }]; + +const MyList = () => { + return ; +}; +``` + +becomes this in v9: + +```js +const items = [{ name: 'John' }, { name: 'Alice' }]; + +const MyList = () => { + return ( + + {items.map(item => { + {item}; + })} + + ); +}; +``` + +### Virtualization approach + +Virtualization is **not part** of `List` in Fluent UI React v9. We don't want to force any particular solution for virtualization, but we provide [examples](https://react.fluentui.dev/?path=/story/preview-components-list--virtualized-list-with-actionable-items) how to use `List` with a popular library `react-window` to get the desired effect. + +This makes the API of `List` much simpler. + +### v8 Property mapping + +Most of the v8 props are for it's virtualization functionality. Since the v9 `List` takes a different approach, most of the props cannot be directly migrated. + +| v8 List | v9 List | +| ------------------------- | -------------------------------- | +| `className` | `className` | +| `componentRef` | `componentRef` | +| `getItemCountForPage` | N/A | +| `getKey` | N/A as you control the ListItems | +| `getPageHeight` | N/A | +| `getPageSpecification` | N/A | +| `getPageStyle` | N/A | +| `ignoreScrollingState` | N/A | +| `items` | render `` instead | +| `onPageAdded` | N/A | +| `onPagesUpdated` | N/A | +| `onRenderCell` | N/A | +| `onRenderCellConditional` | N/A | +| `onRenderPage` | N/A | +| `onRenderRoot` | N/A | +| `onRenderSurface` | N/A | +| `onShouldVirtualize` | N/A | +| `renderCount` | N/A | +| `renderEarly` | N/A | +| `renderedWindowsAhead` | N/A | +| `renderedWindowsBehind` | N/A | +| `role` | `role` | +| `startIndex` | N/A | +| `usePageCache` | N/A | +| `version` | N/A | +| - | `customArrowNavigationOptions` | +| - | `defaultSelectedItems` | +| - | `focusableItems` | +| - | `layout` | +| - | `onSelectionChange` | +| - | `selectable` | +| - | `selectionMode` | + +## Migration from v0 + +### Composition, also known as "Children API" + +In Fluent UI React v9 we prefer to use composition over configuration where possible. List is no exception. the v0 list also supports composition API under a name of "Children API". + +#### Children API component mapping + +Migrating from a v9 Children API to v9 composition API is quite straighforward. You can replace the components like this: + +- Use v9 `List` instead of v0 `List` +- Use v9 `ListItem` instead of v0 `List.Item` + +For props please refer to [Property mapping](#v0-property-mapping) section. + +#### Shorthand API + +For Shorthand API things are a bit more complicated, as your code needs to me updated to use composition. + +Take this example in v0: + +```js +const items = [ + { + key: 'robert', + header: 'Robert Tolbert', + content: 'Program the sensor to the SAS alarm through the haptic SQL card!', + }, + { + key: 'celeste', + header: 'Celeste Burton', + content: 'Use the online FTP application to input the multi-byte application!', + }, +]; + +const MyList = () => { + return ; +}; +``` + +becomes this in v9: + +```js +const items = [ + { + key: 'robert', + header: 'Robert Tolbert', + content: 'Program the sensor to the SAS alarm through the haptic SQL card!', + }, + { + key: 'celeste', + header: 'Celeste Burton', + content: 'Use the online FTP application to input the multi-byte application!', + }, +]; + +const MyList = () => { + return ( + + {items.map(item => { + +

{item.header}

+

{item.content}>

+
; + })} +
+ ); +}; +``` + +### v0 Property mapping + +Compared to its v0 counterpart, the v9 List implementation is much more generic and it **doesn't have any opinion** on how it's content should look like. This means that you will **not** find layout specific props like `header`, `headerMedia`, `content` or layout specific components. This allows for much more flexible use of the component. + +We recommend using a component like `Persona` where possible, or creating a custom layout component where necessary. + +#### List + +| v0 List | v9 List | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `accessibility` | `customArrowNavigationOptions` from `tabster` | +| `as` | `as` | +| `className` | `className` | +| `debug` | N/A | +| `defaultSelectedIndex` | `defaultSelectedItems` | +| `design` | N/A | +| `horizontal` | use `layout` with a value `ListLayout.horizontal` | +| `items` | N/A - use `ListItem` components as Children | +| `navigable` | `focusableItems` if the `ListItem` contains more than 1 actionable element, otherwise use `ListItemButton` component as a child. | +| `onSelectedIndexChange` | `onSelectionChange` | +| `ref` | `ref` | +| `selectable` | `selectable` | +| `selectedIndex` | only in controlled mode, use `selection` state; see [example](https://react.fluentui.dev/?path=/story/preview-components-list--list-selection-controlled). | +| `styles` | use slots in combination with `className` | +| `truncateContent` | N/A - the `List` is not concerned about it's content | +| `truncateHeader` | N/A - the `List` is not concerned about it's content | +| `variables` | N/A - use slots in combination with `className` | +| `wrap` | N/A - the `List` is not concerned about it's content | + +#### ListItem + +| v0 ListItem | v9 ListItem | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `accessibility` | N/A | +| `as` | `as` | +| `className` | `className` | +| `content` | N/A - use children | +| `contentMedia` | N/A - use children | +| `debug` | N/A | +| `design` | N/A | +| `endMedia` | N/A - use children | +| `header` | N/A - use children | +| `headerMedia` | N/A - use children | +| `important` | N/A | +| `index` | N/A | +| `media` | N/A - use children | +| `navigable` | N/A - use `List` property `focusableItems` or render `ListItemButton` if the `ListItem` only has one action | +| `onClick` | `onClick` | +| `ref` | ref | +| `selectable` | N/A - use `List` property `selectable` for uncontrolled selection, or use `useListSelection` hook to control the state | +| `selected` | N/A - when in controlled state, use `selection.getListItemProps` [example](https://react.fluentui.dev/?path=/story/preview-components-list--list-selection-controlled) to pass proper role and aria attributes. | +| `styles` | N/A - use `className` for any slot | +| `truncateContent` | N/A - the `List` is not concerned about it's content | +| `truncateHeader` | N/A - the `List` is not concerned about it's content | + +#### Other + +Other components like `ListItemContent`, `ListItemContentMedia`, `ListItemEndMedia`, `ListItemHeader`,`ListItemHeaderMedia` and `ListItemMedia` are _not_ currently present in v9 `List` implementation for the reasons mentioned above. diff --git a/packages/react-components/react-list-preview/docs/Spec.md b/packages/react-components/react-list-preview/docs/Spec.md index a3a755a6aa0de6..e7f815c204cbc6 100644 --- a/packages/react-components/react-list-preview/docs/Spec.md +++ b/packages/react-components/react-list-preview/docs/Spec.md @@ -2,15 +2,19 @@ ## Background -_Description and use cases of this component_ +A List is a component that displays a group of sequential components in one dimension. The layout can be vertical (default), horizontal or grid with completely custom template. -## Prior Art +If you are displaying more than one dimension of the data, the List probably isn't the proper component to use, instead, consider using Table or DataGrid. + +The List supports plain list items, interactive list items with one action or multiple actions. It also has support for single and multi selection built in. This can be utilized in either uncontrolled or controlled way. -_Include background research done for this component_ +All of the List scenarios are also accessible, as the whole component was built with accessibility in mind. It is easily navigable with a keyboard and supports different screen reader applications. + +## Prior Art -- _Link to Open UI research_ -- _Link to comparison of v7 and v0_ -- _Link to GitHub epic issue for the converged component_ +- [Fluent UI v0 docs](https://fluentsite.z22.web.core.windows.net/components/list/definition) +- [Fluent UI v8 docs](https://developer.microsoft.com/en-us/fluentui#/controls/web/list) +- [Open UI research](https://open-ui.org/components/list.research/) ## Sample Code From 7b79c8c74ea3031e8727c1877d82f8879825a083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 9 Nov 2023 10:29:53 +0100 Subject: [PATCH 025/136] revert change in react/utilities/index.ts --- ...act-utilities-18dc963b-51db-49d1-aed0-04054579847f.json | 7 ------- packages/react-components/react-utilities/src/index.ts | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json diff --git a/change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json b/change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json deleted file mode 100644 index 6efe5d856ffde3..00000000000000 --- a/change/@fluentui-react-utilities-18dc963b-51db-49d1-aed0-04054579847f.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "feat: Export selection utils", - "packageName": "@fluentui/react-utilities", - "email": "jirivyhnalek@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index 4016ce4987d80a..d6369be45ab3cb 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -75,14 +75,14 @@ export type { FluentTriggerComponent, TriggerProps } from './trigger/index'; export type { NativeTouchOrMouseEvent, ReactTouchOrMouseEvent, TouchOrMouseEvent } from './events/index'; export { isTouchEvent, isMouseEvent, getEventClientCoords } from './events/index'; -export { useSelection } from './selection/index'; export type { + SelectionMode, OnSelectionChangeCallback, OnSelectionChangeData, - SelectionHookParams, SelectionItemId, + SelectionHookParams, SelectionMethods, - SelectionMode, } from './selection/index'; +export { useSelection } from './selection/index'; export { elementContains, setVirtualParent, getParent } from './virtualParent/index'; From 6116734e235eeafcecdfb1bf82581468808e335f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 9 Nov 2023 10:55:50 +0100 Subject: [PATCH 026/136] unused import --- .../stories/List/ListSelectionControlled.stories.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index 669c286c3eac15..fb1b14d2213363 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -1,11 +1,4 @@ -import { - Button, - makeStyles, - Persona, - PresenceBadgeProps, - shorthands, - useEventCallback, -} from '@fluentui/react-components'; +import { Button, makeStyles, Persona, shorthands, useEventCallback } from '@fluentui/react-components'; import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; import * as React from 'react'; From dcba5bb43eb5fa9e5465af0f9feedfb6c4788adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 9 Nov 2023 10:56:15 +0100 Subject: [PATCH 027/136] remove unused file --- .../react-list-preview/.npmignore | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 packages/react-components/react-list-preview/.npmignore diff --git a/packages/react-components/react-list-preview/.npmignore b/packages/react-components/react-list-preview/.npmignore deleted file mode 100644 index a5817be2414dec..00000000000000 --- a/packages/react-components/react-list-preview/.npmignore +++ /dev/null @@ -1,38 +0,0 @@ -.storybook/ -.vscode/ -bundle-size/ -config/ -coverage/ -docs/ -etc/ -node_modules/ -src/ -stories/ -dist/types/ -temp/ -__fixtures__ -__mocks__ -__tests__ - -*.api.json -*.log -*.spec.* -*.cy.* -*.test.* -*.yml - -# config files -*config.* -*rc.* -.editorconfig -.eslint* -.git* -.prettierignore -.swcrc -project.json - -# exclude gitignore patterns explicitly -!lib -!lib-commonjs -!lib-amd -!dist/*.d.ts From 8f4b7839b1556a26b2f73e86e0d539e4d40c2e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 11:10:46 +0100 Subject: [PATCH 028/136] Replace ListLayout enum with a string literal --- .../react-list-preview/docs/MIGRATION.md | 2 +- .../src/components/List/List.types.ts | 6 +----- .../react-list-preview/src/components/List/useList.ts | 6 +++--- .../src/components/List/useListStyles.styles.ts | 8 ++++---- .../react-components/react-list-preview/src/index.ts | 11 ++--------- .../stories/List/ListGrid.stories.tsx | 4 ++-- .../stories/List/ListHorizontal.stories.tsx | 4 ++-- .../stories/List/ListMultipleActions.stories.tsx | 4 ++-- 8 files changed, 17 insertions(+), 28 deletions(-) diff --git a/packages/react-components/react-list-preview/docs/MIGRATION.md b/packages/react-components/react-list-preview/docs/MIGRATION.md index 1915c413383110..feddd4fa98b8fe 100644 --- a/packages/react-components/react-list-preview/docs/MIGRATION.md +++ b/packages/react-components/react-list-preview/docs/MIGRATION.md @@ -163,7 +163,7 @@ We recommend using a component like `Persona` where possible, or creating a cust | `debug` | N/A | | `defaultSelectedIndex` | `defaultSelectedItems` | | `design` | N/A | -| `horizontal` | use `layout` with a value `ListLayout.horizontal` | +| `horizontal` | use `layout` with a value `horizontal` | | `items` | N/A - use `ListItem` components as Children | | `navigable` | `focusableItems` if the `ListItem` contains more than 1 actionable element, otherwise use `ListItemButton` component as a child. | | `onSelectedIndexChange` | `onSelectionChange` | diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 188962a2fc528d..f94295ec38ef32 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -10,11 +10,7 @@ import type { import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; import { ListSelectionState } from '../../hooks/types'; -export enum ListLayout { - Horizontal = 'horizontal', - Vertical = 'vertical', - Grid = 'grid', -} +export type ListLayout = 'horizontal' | 'vertical' | 'grid'; // Component ref interface export type IList = { diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index caf28884824885..330d3db25c2447 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot, useEventCallback } from '@fluentui/react-utilities'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; -import { ListLayout, ListProps, ListState, IList } from './List.types'; +import { ListProps, ListState, IList } from './List.types'; import { useListFeatures } from '../../hooks/useListFeatures'; import { useListSelection } from '../../hooks/useListSelection'; @@ -26,7 +26,7 @@ const useComponentRef = (componentRef: React.Ref | undefined, selection: */ export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { const { - layout = ListLayout.Vertical, + layout = 'vertical', focusableItems = false, customArrowNavigationOptions, selectable = false, @@ -37,7 +37,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): } = props; const arrowNavigationAttributes = useArrowNavigationGroup({ - axis: layout === ListLayout.Grid ? 'grid-linear' : 'both', + axis: layout === 'grid' ? 'grid-linear' : 'both', memorizeCurrent: true, ...(customArrowNavigationOptions || {}), }); diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts index 97eb9beca890b2..a26b05470c5155 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -1,6 +1,6 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; -import { ListLayout, ListSlots, ListState } from './List.types'; +import { ListSlots, ListState } from './List.types'; export const listClassNames: SlotClassNames = { root: 'fui-List', @@ -37,9 +37,9 @@ export const useListStyles_unstable = (state: ListState): ListState => { const styles = useStyles(); const layoutToStyles = { - [ListLayout.Vertical]: styles.root, - [ListLayout.Horizontal]: styles.rootHorizontal, - [ListLayout.Grid]: styles.rootGrid, + ['vertical']: styles.root, + ['horizontal']: styles.rootHorizontal, + ['grid']: styles.rootGrid, }; state.root.className = mergeClasses( diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 13210067963e56..764f930995232a 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -1,13 +1,6 @@ -export { - List, - listClassNames, - renderList_unstable, - useListStyles_unstable, - useList_unstable, - ListLayout, -} from './List'; +export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List'; -export type { ListProps, ListSlots, ListState, IList } from './List'; +export type { ListProps, ListSlots, ListState, IList, ListLayout } from './List'; export { ListItem, listItemClassNames, diff --git a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx index 587aff2b5bd088..28b92a6d0ca957 100644 --- a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { List, ListProps, ListItem, ListLayout, ListItemButton } from '@fluentui/react-list-preview'; +import { List, ListProps, ListItem, ListItemButton } from '@fluentui/react-list-preview'; import { makeStyles } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -11,7 +11,7 @@ const useStyles = makeStyles({ export const ListGrid = (props: Partial) => { const classes = useStyles(); return ( - + alert('Asia')}>Asia diff --git a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx index 10f262797346a4..1d1d7b34f7b257 100644 --- a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { List, ListItem, ListLayout } from '@fluentui/react-list-preview'; +import { List, ListItem } from '@fluentui/react-list-preview'; import { makeStyles, shorthands } from '@fluentui/react-components'; const useStyles = makeStyles({ @@ -11,7 +11,7 @@ const useStyles = makeStyles({ export const ListHorizontal = () => { const classes = useStyles(); return ( - + Asia Africa Europe diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index 074872611f0814..a680229e634e2b 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -16,7 +16,7 @@ import { tokens, } from '@fluentui/react-components'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; -import { List, ListItem, ListLayout, ListProps } from '@fluentui/react-list-preview'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; import * as React from 'react'; const resolveAsset = (asset: string) => { @@ -153,7 +153,7 @@ export const ListMultipleActions = (props: Partial) => { return ( Date: Mon, 20 Nov 2023 11:19:20 +0100 Subject: [PATCH 029/136] Replace IList with ListComponentRef --- .../react-list-preview/etc/react-list-preview.api.md | 4 ++-- .../react-list-preview/src/components/List/List.types.ts | 4 ++-- .../react-list-preview/src/components/List/useList.ts | 4 ++-- packages/react-components/react-list-preview/src/index.ts | 2 +- .../stories/List/ListSelectionUncontrolled.stories.tsx | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index e53511470c8a34..69e536d6e59d9b 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -20,7 +20,7 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; // @public (undocumented) -export type IList = { +export type ListComponentRef = { selection: ListState['selection']; }; @@ -86,7 +86,7 @@ export type ListProps = ComponentProps & { selectionMode?: SelectionMode_2; defaultSelectedItems?: SelectionItemId[]; onSelectionChange?: (event: React_2.SyntheticEvent, data: OnSelectionChangeData) => void; - componentRef?: React_2.Ref; + componentRef?: React_2.Ref; }; // @public (undocumented) diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index f94295ec38ef32..4d3cd9f6d65e3c 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -13,7 +13,7 @@ import { ListSelectionState } from '../../hooks/types'; export type ListLayout = 'horizontal' | 'vertical' | 'grid'; // Component ref interface -export type IList = { +export type ListComponentRef = { selection: ListState['selection']; }; @@ -32,7 +32,7 @@ export type ListProps = ComponentProps & { selectionMode?: SelectionMode; defaultSelectedItems?: SelectionItemId[]; onSelectionChange?: (event: React.SyntheticEvent, data: OnSelectionChangeData) => void; - componentRef?: React.Ref; + componentRef?: React.Ref; }; export type ListContextValue = { diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 330d3db25c2447..ce4f1c7b540774 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot, useEventCallback } from '@fluentui/react-utilities'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; -import { ListProps, ListState, IList } from './List.types'; +import { ListProps, ListState, ListComponentRef } from './List.types'; import { useListFeatures } from '../../hooks/useListFeatures'; import { useListSelection } from '../../hooks/useListSelection'; -const useComponentRef = (componentRef: React.Ref | undefined, selection: ListState['selection']) => { +const useComponentRef = (componentRef: React.Ref | undefined, selection: ListState['selection']) => { React.useImperativeHandle( componentRef, () => ({ diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 764f930995232a..026e4784c4403a 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -1,6 +1,6 @@ export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List'; -export type { ListProps, ListSlots, ListState, IList, ListLayout } from './List'; +export type { ListProps, ListSlots, ListState, ListComponentRef, ListLayout } from './List'; export { ListItem, listItemClassNames, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index 591c90f41e30cc..3f6fda7eb0cb53 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -1,5 +1,5 @@ import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { IList, List, ListItem } from '@fluentui/react-list-preview'; +import { ListComponentRef, List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; import names from './names'; @@ -42,7 +42,7 @@ export const ListSelectionUncontrolled = () => { const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; - const ref = React.useRef(null); + const ref = React.useRef(null); return (
From a51d0481ba121f8b974bf312fc2afaf7e075e7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 12:20:56 +0100 Subject: [PATCH 030/136] remove todos, replace styles with resetStyles --- .../src/components/List/List.tsx | 2 - .../components/List/useListStyles.styles.ts | 25 ++++----- .../src/components/ListItem/ListItem.tsx | 5 +- .../src/components/ListItem/ListItem.types.ts | 2 - .../src/components/ListItem/useListItem.ts | 31 ----------- .../src/components/ListItem/useListItem.tsx | 6 +-- .../ListItem/useListItemStyles.styles.ts | 51 ++++++++++--------- .../ListItemButton/ListItemButton.tsx | 3 -- 8 files changed, 42 insertions(+), 83 deletions(-) delete mode 100644 packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts diff --git a/packages/react-components/react-list-preview/src/components/List/List.tsx b/packages/react-components/react-list-preview/src/components/List/List.tsx index 4316be9d149910..2cb94dd4762af3 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.tsx @@ -15,8 +15,6 @@ export const List: ForwardRefComponent = React.forwardRef((props, ref const listContext = useListContextValues_unstable(state); useListStyles_unstable(state); - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md useCustomStyleHook_unstable('useListStyles_unstable')(state); return renderList_unstable(state, listContext); diff --git a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts index a26b05470c5155..5433ed2582eb94 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListStyles.styles.ts @@ -1,24 +1,22 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; -import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { makeStyles, makeResetStyles, mergeClasses, shorthands } from '@griffel/react'; import { ListSlots, ListState } from './List.types'; export const listClassNames: SlotClassNames = { root: 'fui-List', - // TODO: add class names for all slots on ListSlots. - // Should be of the form `: 'fui-List__` }; +const useRootBaseStyles = makeResetStyles({ + ...shorthands.padding(0), + ...shorthands.margin(0), + textIndent: 0, + listStyleType: 'none', +}); + /** * Styles for the root slot */ const useStyles = makeStyles({ - root: { - ...shorthands.padding(0), - ...shorthands.margin(0), - textIndent: 0, - listStyleType: 'none', - }, - rootHorizontal: { display: 'flex', }, @@ -26,25 +24,24 @@ const useStyles = makeStyles({ rootGrid: { display: 'grid', }, - - // TODO add additional classes for different states and/or slots }); /** * Apply styling to the List slots based on the state */ export const useListStyles_unstable = (state: ListState): ListState => { + const rootStyles = useRootBaseStyles(); const styles = useStyles(); const layoutToStyles = { - ['vertical']: styles.root, ['horizontal']: styles.rootHorizontal, ['grid']: styles.rootGrid, + ['vertical']: undefined, // no extra styles needed, keep it in for completeness and type safety }; state.root.className = mergeClasses( listClassNames.root, - styles.root, + rootStyles, layoutToStyles[state.layout], state.root.className, ); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx index e850346d78f47b..382b79dcca49ab 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx @@ -9,13 +9,10 @@ import type { ListItemProps } from './ListItem.types'; /** * ListItem component - TODO: add more docs */ -export const ListItem: ForwardRefComponent = React.forwardRef((props, ref) => { +export const ListItem: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useListItem_unstable(props, ref); useListItemStyles_unstable(state); - - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md useCustomStyleHook_unstable('useListItemStyles_unstable')(state); return renderListItem_unstable(state); }); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 705d9abc149486..256cfa7be65b32 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -16,5 +16,3 @@ export type ListItemProps = ComponentProps & { * State used in rendering ListItem */ export type ListItemState = ComponentState & {}; -// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from ListItemProps. -// & Required> diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts deleted file mode 100644 index 9130a31345c752..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; -import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import type { ListItemProps, ListItemState } from './ListItem.types'; - -/** - * Create the state required to render ListItem. - * - * The returned state can be modified with hooks such as useListItemStyles_unstable, - * before being passed to renderListItem_unstable. - * - * @param props - props from this instance of ListItem - * @param ref - reference to root HTMLDivElement of ListItem - */ -export const useListItem_unstable = (props: ListItemProps, ref: React.Ref): ListItemState => { - return { - // TODO add appropriate props/defaults - components: { - // TODO add each slot's element type or component - root: 'div', - }, - // TODO add appropriate slots, for example: - // mySlot: resolveShorthand(props.mySlot), - root: slot.always( - getIntrinsicElementProps('div', { - ref, - ...props, - }), - { elementType: 'div' }, - ), - }; -}; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index ec9281d5dc78fb..150319e6120ab7 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; -import { getNativeElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; import { Checkmark16Filled } from '@fluentui/react-icons'; @@ -68,11 +68,11 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref = { checkmark: 'fui-ListItem__checkmark', }; +const useRootBaseStyles = makeResetStyles({ + ...shorthands.padding(0), + ...shorthands.margin(0), + textIndent: 0, + listStyleType: 'none', + ...createCustomFocusIndicatorStyle( + { + ...shorthands.outline(tokens.strokeWidthThick, 'solid', tokens.colorStrokeFocus2), + ...shorthands.borderRadius(tokens.borderRadiusMedium), + }, + { selector: 'focus' }, + ), +}); + +const useCheckmarkBaseStyles = makeResetStyles({ + alignSelf: 'center', + marginRight: tokens.spacingHorizontalS, + width: tokens.spacingHorizontalL, + height: tokens.spacingVerticalL, +}); + /** * Styles for the root slot */ const useStyles = makeStyles({ - root: { - ...shorthands.padding(0), - ...shorthands.margin(0), - textIndent: 0, - listStyleType: 'none', - ...createCustomFocusIndicatorStyle( - { - ...shorthands.outline(tokens.strokeWidthThick, 'solid', tokens.colorStrokeFocus2), - ...shorthands.borderRadius(tokens.borderRadiusMedium), - }, - { selector: 'focus' }, - ), - }, - rootClickable: { display: 'flex', cursor: 'pointer', }, - - checkmark: { - alignSelf: 'center', - marginRight: tokens.spacingHorizontalS, - width: tokens.spacingHorizontalL, - height: tokens.spacingVerticalL, - }, }); /** * Apply styling to the ListItem slots based on the state */ export const useListItemStyles_unstable = (state: ListItemState): ListItemState => { + const rootBaseStyles = useRootBaseStyles(); + const checkmarkBaseStyles = useCheckmarkBaseStyles(); const styles = useStyles(); + state.root.className = mergeClasses( listItemClassNames.root, - styles.root, + rootBaseStyles, state.root.onClick && styles.rootClickable, state.root.className, ); @@ -55,7 +58,7 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState if (state.checkmark) { state.checkmark.className = mergeClasses( listItemClassNames.checkmark, - styles.checkmark, + checkmarkBaseStyles, state.checkmark?.className, ); } diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx index b293377c115c60..1293bdbf7ab00c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx @@ -13,9 +13,6 @@ export const ListItemButton: ForwardRefComponent = React.fo const state = useListItemButton_unstable(props, ref); useListItemButtonStyles_unstable(state); - - // TODO update types in packages/react-components/react-shared-contexts/src/CustomStyleHooksContext/CustomStyleHooksContext.ts - // https://github.com/microsoft/fluentui/blob/master/rfcs/react-components/convergence/custom-styling.md useCustomStyleHook_unstable('useListItemButtonStyles_unstable')(state); return renderListItemButton_unstable(state); }); From 782fe5b4f0227999858e07f55baa07e8194e1028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 12:22:40 +0100 Subject: [PATCH 031/136] remove more todos --- .../react-list-preview/src/components/List/List.tsx | 3 --- .../react-list-preview/src/components/ListItem/ListItem.tsx | 3 --- .../src/components/ListItemButton/ListItemButton.tsx | 3 --- 3 files changed, 9 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.tsx b/packages/react-components/react-list-preview/src/components/List/List.tsx index 2cb94dd4762af3..19e1bf327ece00 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.tsx @@ -7,9 +7,6 @@ import { useListStyles_unstable } from './useListStyles.styles'; import type { ListProps } from './List.types'; import { useListContextValues_unstable } from './useListContextValues'; -/** - * List component - TODO: add more docs - */ export const List: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useList_unstable(props, ref); const listContext = useListContextValues_unstable(state); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx index 382b79dcca49ab..91e07576d025bb 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx @@ -6,9 +6,6 @@ import { renderListItem_unstable } from './renderListItem'; import { useListItemStyles_unstable } from './useListItemStyles.styles'; import type { ListItemProps } from './ListItem.types'; -/** - * ListItem component - TODO: add more docs - */ export const ListItem: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useListItem_unstable(props, ref); diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx index 1293bdbf7ab00c..ec79fb24c837bf 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx @@ -6,9 +6,6 @@ import { renderListItemButton_unstable } from './renderListItemButton'; import { useListItemButtonStyles_unstable } from './useListItemButtonStyles.styles'; import type { ListItemButtonProps } from './ListItemButton.types'; -/** - * ListItemButton component - TODO: add more docs - */ export const ListItemButton: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useListItemButton_unstable(props, ref); From d9743e240ca9b0a9074553d676fd12b385a31061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 12:34:36 +0100 Subject: [PATCH 032/136] dont spread selection props unless selection is enabled --- .../react-list-preview/src/components/List/useList.ts | 2 +- .../react-list-preview/src/components/ListItem/useListItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index ce4f1c7b540774..0218f5b2bf72bd 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -69,7 +69,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): getIntrinsicElementProps('ul', { ref, tabIndex: -1, - ...selection.getListProps(), + ...(selectable ? selection.getListProps() : {}), ...arrowNavigationAttributes, ...props, }), diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 150319e6120ab7..ca6549da02cdd4 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -73,7 +73,7 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref Date: Mon, 20 Nov 2023 12:36:17 +0100 Subject: [PATCH 033/136] fix unnecessary ul/li in exmample --- .../stories/List/ListDefault.stories.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx index 4ff1cb2fdf38f3..8cc470b3449c24 100644 --- a/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListDefault.stories.tsx @@ -2,13 +2,13 @@ import * as React from 'react'; import { List, ListItem } from '@fluentui/react-list-preview'; export const Default = () => ( - - Asia - Africa - Europe - North America - South America - Australia/Oceania - Antarctica + + Asia + Africa + Europe + North America + South America + Australia/Oceania + Antarctica ); From e9379f24381d929396d393fb5a1a364ce7096df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 12:41:49 +0100 Subject: [PATCH 034/136] remove dl,dt,dd --- .../react-list-preview/src/components/List/List.types.ts | 2 +- .../src/components/ListItem/ListItem.types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 4d3cd9f6d65e3c..43718963897162 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -18,7 +18,7 @@ export type ListComponentRef = { }; export type ListSlots = { - root: NonNullable>; + root: NonNullable>; }; /** diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 256cfa7be65b32..364fd132498f98 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -1,7 +1,7 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; export type ListItemSlots = { - root: NonNullable>; + root: NonNullable>; checkmark?: Slot<'div'>; }; From d492c926e30f42b70156774d39e5174262751238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 12:43:16 +0100 Subject: [PATCH 035/136] fix story --- .../stories/List/VirtualizedList.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx index 16d44cf5cbde21..23a0876e39939f 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx @@ -4,9 +4,9 @@ import { List, ListItem } from '@fluentui/react-list-preview'; import countries from './countries'; -const CountriesList = (props: React.ComponentProps) => ( - -); +const CountriesList = React.forwardRef((props: React.ComponentProps, ref) => ( + +)); export const VirtualizedList = () => { return ( From 6e86bed43a044a617cb4a95af3d44fe980a4cdf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 20 Nov 2023 12:58:15 +0100 Subject: [PATCH 036/136] remove external data provders for names and countries --- .../src/components/List/useList.ts | 1 - .../List/ListSelectionControlled.stories.tsx | 17 +- .../ListSelectionUncontrolled.stories.tsx | 16 +- .../stories/List/ListWithAction.stories.tsx | 18 +- .../List/ListWithMultipleActions.stories.tsx | 16 +- .../stories/List/VirtualizedList.stories.tsx | 208 +++++++++++++++++- ...ualizedListWithActionableItems.stories.tsx | 199 ++++++++++++++++- .../stories/List/countries.js | 198 ----------------- .../react-list-preview/stories/List/names.js | 54 ----- 9 files changed, 467 insertions(+), 260 deletions(-) delete mode 100644 packages/react-components/react-list-preview/stories/List/countries.js delete mode 100644 packages/react-components/react-list-preview/stories/List/names.js diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 0218f5b2bf72bd..e3583b224eab6b 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -68,7 +68,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): root: slot.always( getIntrinsicElementProps('ul', { ref, - tabIndex: -1, ...(selectable ? selection.getListProps() : {}), ...arrowNavigationAttributes, ...props, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index fb1b14d2213363..7a36b1d39f78f4 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -3,7 +3,22 @@ import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/rea import * as React from 'react'; import { ListSelectionState } from '../../src/hooks/types'; -import names from './names'; + +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; type Item = { name: string; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index 3f6fda7eb0cb53..1e495239004cb0 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -1,7 +1,21 @@ import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; import { ListComponentRef, List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; -import names from './names'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; type Item = { name: string; diff --git a/packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx index ea45b5ba702fc0..a0eac62b8da44e 100644 --- a/packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx @@ -1,7 +1,21 @@ -import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { makeStyles, Persona, shorthands } from '@fluentui/react-components'; import { List, ListItem, ListItemButton } from '@fluentui/react-list-preview'; import * as React from 'react'; -import names from './names'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; const useStyles = makeStyles({ wrapper: { diff --git a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx index b5997c4ec5d33f..d1bd02b6fffca0 100644 --- a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx @@ -1,7 +1,21 @@ import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; -import names from './names'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; const useStyles = makeStyles({ wrapper: { diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx index 23a0876e39939f..cae0c9448968ef 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx @@ -2,7 +2,204 @@ import * as React from 'react'; import { FixedSizeList } from 'react-window'; import { List, ListItem } from '@fluentui/react-list-preview'; -import countries from './countries'; +const countries = [ + 'Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Antigua & Deps', + 'Argentina', + 'Armenia', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bhutan', + 'Bolivia', + 'Bosnia Herzegovina', + 'Botswana', + 'Brazil', + 'Brunei', + 'Bulgaria', + 'Burkina', + 'Burundi', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Cape Verde', + 'Central African Rep', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Comoros', + 'Congo', + 'Congo {Democratic Rep}', + 'Costa Rica', + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'East Timor', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Ethiopia', + 'Fiji', + 'Finland', + 'France', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Greece', + 'Grenada', + 'Guatemala', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Honduras', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland {Republic}', + 'Israel', + 'Italy', + 'Ivory Coast', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Korea North', + 'Korea South', + 'Kosovo', + 'Kuwait', + 'Kyrgyzstan', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Macedonia', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Morocco', + 'Mozambique', + 'Myanmar, {Burma}', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Qatar', + 'Romania', + 'Russian Federation', + 'Rwanda', + 'St Kitts & Nevis', + 'St Lucia', + 'Saint Vincent & the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome & Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Swaziland', + 'Sweden', + 'Switzerland', + 'Syria', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Togo', + 'Tonga', + 'Trinidad & Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Vatican City', + 'Venezuela', + 'Vietnam', + 'Yemen', + 'Zambia', + 'Zimbabwe', +]; const CountriesList = React.forwardRef((props: React.ComponentProps, ref) => ( @@ -43,3 +240,12 @@ VirtualizedList.parameters = { }, }, }; + +VirtualizedList.decorators = [ + Story => ( +
+ dfaeafasefase + +
+ ), +]; diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx index 047e5ad4000d85..1fca6505e8ff90 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx @@ -2,7 +2,204 @@ import * as React from 'react'; import { FixedSizeList } from 'react-window'; import { List, ListItem, ListItemButton } from '@fluentui/react-list-preview'; -import countries from './countries'; +const countries = [ + 'Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Antigua & Deps', + 'Argentina', + 'Armenia', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bhutan', + 'Bolivia', + 'Bosnia Herzegovina', + 'Botswana', + 'Brazil', + 'Brunei', + 'Bulgaria', + 'Burkina', + 'Burundi', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Cape Verde', + 'Central African Rep', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Comoros', + 'Congo', + 'Congo {Democratic Rep}', + 'Costa Rica', + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'East Timor', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Ethiopia', + 'Fiji', + 'Finland', + 'France', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Greece', + 'Grenada', + 'Guatemala', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Honduras', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland {Republic}', + 'Israel', + 'Italy', + 'Ivory Coast', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Korea North', + 'Korea South', + 'Kosovo', + 'Kuwait', + 'Kyrgyzstan', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Macedonia', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Morocco', + 'Mozambique', + 'Myanmar, {Burma}', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Qatar', + 'Romania', + 'Russian Federation', + 'Rwanda', + 'St Kitts & Nevis', + 'St Lucia', + 'Saint Vincent & the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome & Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Swaziland', + 'Sweden', + 'Switzerland', + 'Syria', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Togo', + 'Tonga', + 'Trinidad & Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Vatican City', + 'Venezuela', + 'Vietnam', + 'Yemen', + 'Zambia', + 'Zimbabwe', +]; const CountriesList = (props: React.ComponentProps) => ; diff --git a/packages/react-components/react-list-preview/stories/List/countries.js b/packages/react-components/react-list-preview/stories/List/countries.js deleted file mode 100644 index e4719d4e30335a..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/countries.js +++ /dev/null @@ -1,198 +0,0 @@ -export default [ - ' Afghanistan', - 'Albania', - 'Algeria', - 'Andorra', - 'Angola', - 'Antigua & Deps', - 'Argentina', - 'Armenia', - 'Australia', - 'Austria', - 'Azerbaijan', - 'Bahamas', - 'Bahrain', - 'Bangladesh', - 'Barbados', - 'Belarus', - 'Belgium', - 'Belize', - 'Benin', - 'Bhutan', - 'Bolivia', - 'Bosnia Herzegovina', - 'Botswana', - 'Brazil', - 'Brunei', - 'Bulgaria', - 'Burkina', - 'Burundi', - 'Cambodia', - 'Cameroon', - 'Canada', - 'Cape Verde', - 'Central African Rep', - 'Chad', - 'Chile', - 'China', - 'Colombia', - 'Comoros', - 'Congo', - 'Congo {Democratic Rep}', - 'Costa Rica', - 'Croatia', - 'Cuba', - 'Cyprus', - 'Czech Republic', - 'Denmark', - 'Djibouti', - 'Dominica', - 'Dominican Republic', - 'East Timor', - 'Ecuador', - 'Egypt', - 'El Salvador', - 'Equatorial Guinea', - 'Eritrea', - 'Estonia', - 'Ethiopia', - 'Fiji', - 'Finland', - 'France', - 'Gabon', - 'Gambia', - 'Georgia', - 'Germany', - 'Ghana', - 'Greece', - 'Grenada', - 'Guatemala', - 'Guinea', - 'Guinea-Bissau', - 'Guyana', - 'Haiti', - 'Honduras', - 'Hungary', - 'Iceland', - 'India', - 'Indonesia', - 'Iran', - 'Iraq', - 'Ireland {Republic}', - 'Israel', - 'Italy', - 'Ivory Coast', - 'Jamaica', - 'Japan', - 'Jordan', - 'Kazakhstan', - 'Kenya', - 'Kiribati', - 'Korea North', - 'Korea South', - 'Kosovo', - 'Kuwait', - 'Kyrgyzstan', - 'Laos', - 'Latvia', - 'Lebanon', - 'Lesotho', - 'Liberia', - 'Libya', - 'Liechtenstein', - 'Lithuania', - 'Luxembourg', - 'Macedonia', - 'Madagascar', - 'Malawi', - 'Malaysia', - 'Maldives', - 'Mali', - 'Malta', - 'Marshall Islands', - 'Mauritania', - 'Mauritius', - 'Mexico', - 'Micronesia', - 'Moldova', - 'Monaco', - 'Mongolia', - 'Montenegro', - 'Morocco', - 'Mozambique', - 'Myanmar, {Burma}', - 'Namibia', - 'Nauru', - 'Nepal', - 'Netherlands', - 'New Zealand', - 'Nicaragua', - 'Niger', - 'Nigeria', - 'Norway', - 'Oman', - 'Pakistan', - 'Palau', - 'Panama', - 'Papua New Guinea', - 'Paraguay', - 'Peru', - 'Philippines', - 'Poland', - 'Portugal', - 'Qatar', - 'Romania', - 'Russian Federation', - 'Rwanda', - 'St Kitts & Nevis', - 'St Lucia', - 'Saint Vincent & the Grenadines', - 'Samoa', - 'San Marino', - 'Sao Tome & Principe', - 'Saudi Arabia', - 'Senegal', - 'Serbia', - 'Seychelles', - 'Sierra Leone', - 'Singapore', - 'Slovakia', - 'Slovenia', - 'Solomon Islands', - 'Somalia', - 'South Africa', - 'South Sudan', - 'Spain', - 'Sri Lanka', - 'Sudan', - 'Suriname', - 'Swaziland', - 'Sweden', - 'Switzerland', - 'Syria', - 'Taiwan', - 'Tajikistan', - 'Tanzania', - 'Thailand', - 'Togo', - 'Tonga', - 'Trinidad & Tobago', - 'Tunisia', - 'Turkey', - 'Turkmenistan', - 'Tuvalu', - 'Uganda', - 'Ukraine', - 'United Arab Emirates', - 'United Kingdom', - 'United States', - 'Uruguay', - 'Uzbekistan', - 'Vanuatu', - 'Vatican City', - 'Venezuela', - 'Vietnam', - 'Yemen', - 'Zambia', - 'Zimbabwe', -]; diff --git a/packages/react-components/react-list-preview/stories/List/names.js b/packages/react-components/react-list-preview/stories/List/names.js deleted file mode 100644 index bebc9bc7aacec6..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/names.js +++ /dev/null @@ -1,54 +0,0 @@ -//source: https://gist.githubusercontent.com/daramasala/37fe19924b1e0bd8979b6becc4eb0c1b/raw/120afae8d52c88d6b48c32fa5dfbb2a37c919216/names.txt - -export default [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', - 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', - 'Leanna Vadnais', - 'Latasha Tang', - 'Liliana Newlon', - 'Dorthy Leon', - 'Gigi Sin', - 'Jeneva Gratz', - 'Benedict Ramer', - 'Dee Rahn', - 'Bridget Cavallaro', - 'Dominga Follette', - 'Robyn Bernat', - 'Freeda Borg', - 'Margarett Rocamora', - 'Mazie Joly', - 'Matha Vore', - 'Yoko Brobst', - 'Ernesto Mccullum', - 'Maire Capel', - 'Ashely Housand', - 'Eleonor Sesco', - 'Gerardo Biggers', - 'Shanon Rosati', - 'Genevive Oldham', - 'Morgan Bevacqua', - 'Steve Howle', - 'Jenny Brenner', - 'Meagan Ponder', - 'Georgeann Vachon', - 'Arica Oloughlin', - 'Lekisha Chilcote', - 'Wilfred Demaris', - 'Jarrett Silas', - 'Keiko Varner', - 'Brittni Schwebach', - 'Rafael Patten', - 'Carson Cottone', - 'Onie Koga', -]; From 77ea0ef3774f7c574ea421d1caf65bb13572026e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 21 Nov 2023 13:42:53 +0100 Subject: [PATCH 037/136] remove decorator --- .../stories/List/VirtualizedList.stories.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx index cae0c9448968ef..7b5d3de34ef2d3 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx @@ -240,12 +240,3 @@ VirtualizedList.parameters = { }, }, }; - -VirtualizedList.decorators = [ - Story => ( -
- dfaeafasefase - -
- ), -]; From 937d61a520ec974f0a8277d19b5a79f4a0a1e9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 21 Nov 2023 16:41:39 +0100 Subject: [PATCH 038/136] update selection logic, controlled/uncontrolled --- .../src/components/List/List.types.ts | 18 +-- .../src/components/List/useList.ts | 48 ++++--- .../react-list-preview/src/hooks/types.ts | 2 +- .../src/hooks/useListSelection.tsx | 52 ++++---- .../List/ListSelectionControlled.stories.tsx | 21 ++- .../ListSelectionControlledBasic.stories.tsx | 110 ++++++++++++++++ ...stSelectionControlledWithState.stories.tsx | 122 ++++++++++++++++++ .../ListSelectionUncontrolled.stories.tsx | 12 +- .../stories/List/index.stories.tsx | 2 + 9 files changed, 309 insertions(+), 78 deletions(-) create mode 100644 packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 43718963897162..736aa33d8f9f51 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,22 +1,10 @@ import * as React from 'react'; -import type { - ComponentProps, - ComponentState, - OnSelectionChangeData, - Slot, - SelectionMode, - SelectionItemId, -} from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot, SelectionMode, SelectionItemId } from '@fluentui/react-utilities'; import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; import { ListSelectionState } from '../../hooks/types'; export type ListLayout = 'horizontal' | 'vertical' | 'grid'; -// Component ref interface -export type ListComponentRef = { - selection: ListState['selection']; -}; - export type ListSlots = { root: NonNullable>; }; @@ -30,9 +18,9 @@ export type ListProps = ComponentProps & { focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode; + selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; - onSelectionChange?: (event: React.SyntheticEvent, data: OnSelectionChangeData) => void; - componentRef?: React.Ref; + onSelectionChange?: (event: React.SyntheticEvent, data: { selectedItems: SelectionItemId[] }) => void; }; export type ListContextValue = { diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index e3583b224eab6b..e643e8a71a085e 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -1,20 +1,16 @@ import * as React from 'react'; -import { getIntrinsicElementProps, slot, useEventCallback } from '@fluentui/react-utilities'; +import { + getIntrinsicElementProps, + OnSelectionChangeData, + slot, + useControllableState, + useEventCallback, +} from '@fluentui/react-utilities'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; -import { ListProps, ListState, ListComponentRef } from './List.types'; +import { ListProps, ListState } from './List.types'; import { useListFeatures } from '../../hooks/useListFeatures'; import { useListSelection } from '../../hooks/useListSelection'; -const useComponentRef = (componentRef: React.Ref | undefined, selection: ListState['selection']) => { - React.useImperativeHandle( - componentRef, - () => ({ - selection, - }), - [selection], - ); -}; - /** * Create the state required to render List. * @@ -28,26 +24,40 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): const { layout = 'vertical', focusableItems = false, - customArrowNavigationOptions, + // customArrowNavigationOptions, selectable = false, selectionMode = 'multiselect', + selectedItems, defaultSelectedItems, - componentRef, onSelectionChange, } = props; const arrowNavigationAttributes = useArrowNavigationGroup({ axis: layout === 'grid' ? 'grid-linear' : 'both', memorizeCurrent: true, - ...(customArrowNavigationOptions || {}), + // ...(customArrowNavigationOptions || {}), }); const [items, setItems] = React.useState>([]); - const { selection } = useListFeatures({ items }, [ - useListSelection({ defaultSelectedItems, onSelectionChange, selectionMode }), - ]); - useComponentRef(componentRef, selection); + const [selectionState, setSelectionState] = useControllableState({ + state: selectedItems, + defaultState: defaultSelectedItems, + initialState: [], + }); + + const onChange = useEventCallback((e: React.SyntheticEvent, data: OnSelectionChangeData) => { + const selectedItemsAsArray = Array.from(data.selectedItems); + setSelectionState(selectedItemsAsArray); + onSelectionChange?.(e, { selectedItems: selectedItemsAsArray }); + }); + + const selection = useListSelection({ + onSelectionChange: onChange, + selectionMode, + selectedItems: selectionState, + defaultSelectedItems, + }); const registerItem = useEventCallback((id: string | number) => { if (!items.find(item => item.id === id)) { diff --git a/packages/react-components/react-list-preview/src/hooks/types.ts b/packages/react-components/react-list-preview/src/hooks/types.ts index ab41cc56431b2c..b9d02c467d2651 100644 --- a/packages/react-components/react-list-preview/src/hooks/types.ts +++ b/packages/react-components/react-list-preview/src/hooks/types.ts @@ -12,7 +12,7 @@ export type ListSelectionState = { deselectItem: (e: React.SyntheticEvent, id: string | number) => void; selectItem: (e: React.SyntheticEvent, id: string | number) => void; clearSelection: (e: React.SyntheticEvent) => void; - toggleAllItems: (e: React.SyntheticEvent) => void; + toggleAllItems: (e: React.SyntheticEvent, itemIds: string[] | number[]) => void; getListProps: () => Pick, 'role' | 'aria-multiselectable'>; getListItemProps: ( value: string | number, diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 6f40a2abcdde69..57cc20c9dee582 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -1,7 +1,7 @@ import { Checkmark16Filled } from '@fluentui/react-icons'; -import { SelectionHookParams, useEventCallback, useSelection } from '@fluentui/react-utilities'; +import { SelectionHookParams, useControllableState, useEventCallback, useSelection } from '@fluentui/react-utilities'; import * as React from 'react'; -import { ListFeaturesState, ListSelectionState } from './types'; +import { ListSelectionState } from './types'; export const defaultListSelectionState: ListSelectionState = { isSelected: () => false, @@ -17,18 +17,14 @@ export const defaultListSelectionState: ListSelectionState = { export function useListSelection( options: SelectionHookParams = { selectionMode: 'multiselect' }, -) { - // False positive, these plugin hooks are intended to be run on every render - // eslint-disable-next-line react-hooks/rules-of-hooks - return (listState: ListFeaturesState) => useListSelectionState(listState, options); -} - -export function useListSelectionState( - listState: ListFeaturesState, - options: SelectionHookParams, ) { const { selectionMode, defaultSelectedItems, onSelectionChange } = options; - const [selectedItems, setSelectedItems] = React.useState(() => new Set(defaultSelectedItems || [])); + + const [selectedItems, setSelectedItems] = useControllableState({ + state: options.selectedItems, + defaultState: defaultSelectedItems || [], + initialState: [], + }); const [selected, selectionMethods] = useSelection({ selectionMode, @@ -44,11 +40,8 @@ export function useListSelectionState( selectionMethods.toggleItem(e, itemId), ); - const toggleAllItems: ListSelectionState['toggleAllItems'] = useEventCallback(e => { - selectionMethods.toggleAllItems( - e, - listState.items.map(item => item.id), - ); + const toggleAllItems: ListSelectionState['toggleAllItems'] = useEventCallback((e, itemIds) => { + selectionMethods.toggleAllItems(e, itemIds); }); const deselectItem: ListSelectionState['deselectItem'] = useEventCallback((e, itemId: string | number) => @@ -95,19 +88,18 @@ export function useListSelectionState( [listPropsForNotSelected, listPropsForSelected, selectionMethods], ); + const selectedArray = React.useMemo(() => Array.from(selected), [selected]); + return { - ...listState, - selection: { - selectedItems: selected, - toggleItem, - toggleAllItems, - deselectItem, - selectItem, - setSelectedItems, - isSelected: (id: string | number) => selectionMethods.isSelected(id), - clearSelection, - getListProps, - getListItemProps, - }, + selectedItems: selectedArray, + toggleItem, + toggleAllItems, + deselectItem, + selectItem, + setSelectedItems, + isSelected: (id: string | number) => selectionMethods.isSelected(id), + clearSelection, + getListProps, + getListItemProps, }; } diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index 7a36b1d39f78f4..f012e95adc288e 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -92,18 +92,25 @@ export const ListSelectionControlled = () => { return origItems.slice(0, currentIndex); }, [currentIndex]); - const { selection } = useListFeatures({ items }, [ - useListSelection({ - selectionMode: 'multiselect', - onSelectionChange: (_, data) => console.log(data.selectedItems), - }), - ]); + const selection = useListSelection({ + selectionMode: 'multiselect', + onSelectionChange: (_, data) => console.log(data.selectedItems), + }); return (
- +
diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx new file mode 100644 index 00000000000000..5545591161feaa --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx @@ -0,0 +1,110 @@ +import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; + +import * as React from 'react'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const origItems: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + buttonControls: { + display: 'flex', + columnGap: '8px', + marginBottom: '16px', + }, + button: { + ...shorthands.padding(0), + }, +}); + +export const ListSelectionControlledBasic = () => { + const classes = useStyles(); + const [currentIndex, setCurrentIndex] = React.useState(4); + + const items = React.useMemo(() => { + return origItems.slice(0, currentIndex); + }, [currentIndex]); + + const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; + + const [selectedItems, setSelectedItems] = React.useState(defaultSelectedItems); + + return ( +
+
+ + +
+ + setSelectedItems(data.selectedItems)} + > + {items.map(({ name, avatar }) => { + return ( + + + + ); + })} + +
Selected people: {selectedItems.join(', ')}
+
+ ); +}; + +ListSelectionControlledBasic.parameters = { + docs: { + description: { + story: [ + 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', + '', + 'You can pass `selectable` prop inside of the List component to get built-in selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state.', + '', + "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", + 'your list items and even store and retrieve them separately.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx new file mode 100644 index 00000000000000..640c355fd8e9c8 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -0,0 +1,122 @@ +import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { List, ListItem, useListSelection } from '@fluentui/react-list-preview'; + +import * as React from 'react'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const origItems: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + buttonControls: { + display: 'flex', + columnGap: '8px', + marginBottom: '16px', + }, + button: { + ...shorthands.padding(0), + }, +}); + +export const ListSelectionControlledWithState = () => { + const classes = useStyles(); + const [currentIndex, setCurrentIndex] = React.useState(4); + + const items = React.useMemo(() => { + return origItems.slice(0, currentIndex); + }, [currentIndex]); + + const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; + + const selection = useListSelection({ + selectionMode: 'multiselect', + defaultSelectedItems, + }); + + return ( +
+
+ + +
+ + selection.setSelectedItems(data.selectedItems)} + > + {items.map(({ name, avatar }) => { + return ( + + + + ); + })} + +
Selected people: {selection.selectedItems.join(', ')}
+
+ ); +}; + +ListSelectionControlledWithState.parameters = { + docs: { + description: { + story: [ + 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', + '', + 'You can pass `selectable` prop inside of the List component to get built-in selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state.', + '', + "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", + 'your list items and even store and retrieve them separately.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index 1e495239004cb0..fce2f59d069450 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -1,5 +1,6 @@ -import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; import { ListComponentRef, List, ListItem } from '@fluentui/react-list-preview'; + import * as React from 'react'; const names = [ 'Melda Bevel', @@ -56,20 +57,18 @@ export const ListSelectionUncontrolled = () => { const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; - const ref = React.useRef(null); + const [selectedItems, setSelectedItems] = React.useState(defaultSelectedItems); return (
-
+ console.log(data.selectedItems)} + onSelectionChange={(_, data) => setSelectedItems(data.selectedItems)} > {items.map(({ name, avatar }) => { return ( @@ -88,6 +87,7 @@ export const ListSelectionUncontrolled = () => { ); })} +
Selected people: {selectedItems.join(', ')}
); }; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 10cb51b2dd69c2..3ef518c4eb7787 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -9,6 +9,8 @@ export { ListGrid } from './ListGrid.stories'; export { ListWithAction } from './ListWithAction.stories'; export { ListMultipleActions } from './ListMultipleActions.stories'; export { ListSelectionUncontrolled } from './ListSelectionUncontrolled.stories'; +export { ListSelectionControlledBasic } from './ListSelectionControlledBasic.stories'; +export { ListSelectionControlledWithState } from './ListSelectionControlledWithState.stories'; export { ListSelectionControlled } from './ListSelectionControlled.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; From 14fb255de532b80258d5ae1bdae2b251abac2ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 21 Nov 2023 16:51:22 +0100 Subject: [PATCH 039/136] remove useListFeatures --- .../etc/react-list-preview.api.md | 8 -------- .../react-list-preview/src/hooks/index.ts | 1 - .../react-list-preview/src/hooks/types.ts | 13 ------------- .../src/hooks/useListFeatures.ts | 16 ---------------- .../react-list-preview/src/index.ts | 2 +- .../List/ListSelectionControlled.stories.tsx | 6 ++---- .../ListSelectionControlledWithState.stories.tsx | 1 + 7 files changed, 4 insertions(+), 43 deletions(-) delete mode 100644 packages/react-components/react-list-preview/src/hooks/useListFeatures.ts diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 69e536d6e59d9b..9fb47d11ffd1a8 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -109,14 +109,6 @@ export const renderListItemButton_unstable: (state: ListItemButtonState) => JSX. // @public export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; -// @public (undocumented) -export function useListFeatures(options: UseListFeaturesOptions, plugins?: ListFeaturePlugin[]): { - items: TItem[]; - selection: ListSelectionState; -}; - // @public export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref) => ListItemState; diff --git a/packages/react-components/react-list-preview/src/hooks/index.ts b/packages/react-components/react-list-preview/src/hooks/index.ts index 9940887a557c9f..01a6959c7c21fd 100644 --- a/packages/react-components/react-list-preview/src/hooks/index.ts +++ b/packages/react-components/react-list-preview/src/hooks/index.ts @@ -1,2 +1 @@ -export * from './useListFeatures'; export * from './useListSelection'; diff --git a/packages/react-components/react-list-preview/src/hooks/types.ts b/packages/react-components/react-list-preview/src/hooks/types.ts index b9d02c467d2651..eea7b3739d2ad6 100644 --- a/packages/react-components/react-list-preview/src/hooks/types.ts +++ b/packages/react-components/react-list-preview/src/hooks/types.ts @@ -2,10 +2,6 @@ import * as React from 'react'; import { List } from '../List'; import { ListItem } from '../ListItem'; -export type UseListFeaturesOptions = { - items: TItem[]; -}; - export type ListSelectionState = { isSelected: (item: string | number) => boolean; toggleItem: (e: React.SyntheticEvent, id: string | number) => void; @@ -21,12 +17,3 @@ export type ListSelectionState = { }; export type UseListSelectionOptions = {}; // multiselect etc - -export interface ListFeaturesState - extends Pick, 'items'> { - selection: ListSelectionState; -} - -export type ListFeaturePlugin = ( - listState: ListFeaturesState, -) => ListFeaturesState; diff --git a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts b/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts deleted file mode 100644 index a02696ea324d95..00000000000000 --- a/packages/react-components/react-list-preview/src/hooks/useListFeatures.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ListFeaturePlugin, UseListFeaturesOptions } from './types'; -import { defaultListSelectionState } from './useListSelection'; - -export function useListFeatures( - options: UseListFeaturesOptions, - plugins: ListFeaturePlugin[] = [], -) { - const { items } = options; - - const initialState = { - items, - selection: defaultListSelectionState, - }; - - return plugins.reduce((state, plugin) => plugin(state), initialState); -} diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 026e4784c4403a..73deee131547a4 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -18,4 +18,4 @@ export { } from './ListItemButton'; export type { ListItemButtonProps, ListItemButtonSlots, ListItemButtonState } from './ListItemButton'; -export { useListFeatures, useListSelection } from './hooks'; +export { useListSelection } from './hooks'; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index f012e95adc288e..fc6924826f9ae7 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -1,5 +1,5 @@ import { Button, makeStyles, Persona, shorthands, useEventCallback } from '@fluentui/react-components'; -import { List, ListItem, useListFeatures, useListSelection } from '@fluentui/react-list-preview'; +import { List, ListItem, useListSelection } from '@fluentui/react-list-preview'; import * as React from 'react'; import { ListSelectionState } from '../../src/hooks/types'; @@ -132,9 +132,7 @@ ListSelectionControlled.parameters = { docs: { description: { story: [ - 'In the controlled approach you are in charge of the selection state. First, you create the state using `useListFeatures` in combination with `useListSelection` hooks.', - '', - 'This will return a List state object with `selection` property. You can then use the `selection` object to control the selection state.', + 'In the controlled approach you are in charge of the selection state. First, you create the state using ``useListSelection` hook. This will return a selection state object with a handful of useful methods to control the selection state.', '', 'In this case, you are in control of deciding what item should be selected and when, including listening on events and calling the `selection` methods.', '', diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx index 640c355fd8e9c8..acfb97365895d5 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -76,6 +76,7 @@ export const ListSelectionControlledWithState = () => { > Toggle all +
Date: Tue, 21 Nov 2023 17:05:25 +0100 Subject: [PATCH 040/136] get rid of getListPros and getListItemProps --- .../react-list-preview/docs/MIGRATION.md | 46 +++++++++---------- .../etc/react-list-preview.api.md | 2 - .../src/components/List/useList.ts | 13 +++++- .../src/components/ListItem/useListItem.tsx | 23 +++++++++- .../react-list-preview/src/hooks/types.ts | 6 +-- .../src/hooks/useListSelection.tsx | 44 +----------------- .../List/ListSelectionControlled.stories.tsx | 19 ++------ 7 files changed, 62 insertions(+), 91 deletions(-) diff --git a/packages/react-components/react-list-preview/docs/MIGRATION.md b/packages/react-components/react-list-preview/docs/MIGRATION.md index feddd4fa98b8fe..138fbcf445a8d2 100644 --- a/packages/react-components/react-list-preview/docs/MIGRATION.md +++ b/packages/react-components/react-list-preview/docs/MIGRATION.md @@ -178,29 +178,29 @@ We recommend using a component like `Persona` where possible, or creating a cust #### ListItem -| v0 ListItem | v9 ListItem | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `accessibility` | N/A | -| `as` | `as` | -| `className` | `className` | -| `content` | N/A - use children | -| `contentMedia` | N/A - use children | -| `debug` | N/A | -| `design` | N/A | -| `endMedia` | N/A - use children | -| `header` | N/A - use children | -| `headerMedia` | N/A - use children | -| `important` | N/A | -| `index` | N/A | -| `media` | N/A - use children | -| `navigable` | N/A - use `List` property `focusableItems` or render `ListItemButton` if the `ListItem` only has one action | -| `onClick` | `onClick` | -| `ref` | ref | -| `selectable` | N/A - use `List` property `selectable` for uncontrolled selection, or use `useListSelection` hook to control the state | -| `selected` | N/A - when in controlled state, use `selection.getListItemProps` [example](https://react.fluentui.dev/?path=/story/preview-components-list--list-selection-controlled) to pass proper role and aria attributes. | -| `styles` | N/A - use `className` for any slot | -| `truncateContent` | N/A - the `List` is not concerned about it's content | -| `truncateHeader` | N/A - the `List` is not concerned about it's content | +| v0 ListItem | v9 ListItem | +| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `accessibility` | N/A | +| `as` | `as` | +| `className` | `className` | +| `content` | N/A - use children | +| `contentMedia` | N/A - use children | +| `debug` | N/A | +| `design` | N/A | +| `endMedia` | N/A - use children | +| `header` | N/A - use children | +| `headerMedia` | N/A - use children | +| `important` | N/A | +| `index` | N/A | +| `media` | N/A - use children | +| `navigable` | N/A - use `List` property `focusableItems` or render `ListItemButton` if the `ListItem` only has one action | +| `onClick` | `onClick` | +| `ref` | ref | +| `selectable` | N/A - use `List` property `selectable` for uncontrolled selection, or use `useListSelection` hook to control the state | +| `selected` | N/A - the selection state takes care of this automatically | +| `styles` | N/A - use `className` for any slot | +| `truncateContent` | N/A - the `List` is not concerned about it's content | +| `truncateHeader` | N/A - the `List` is not concerned about it's content | #### Other diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 9fb47d11ffd1a8..13a3e66737b415 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -134,8 +134,6 @@ export function useListSelection>>; isSelected: (id: string | number) => boolean; clearSelection: (e: React_2.SyntheticEvent) => void; - getListProps: () => Pick, "role" | "aria-multiselectable">; - getListItemProps: (value: string | number) => Pick, "tabIndex" | "role" | "aria-selected">; }; items: TItem[]; }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index e643e8a71a085e..6f3c3b9a2ec03a 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -8,9 +8,10 @@ import { } from '@fluentui/react-utilities'; import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { ListProps, ListState } from './List.types'; -import { useListFeatures } from '../../hooks/useListFeatures'; import { useListSelection } from '../../hooks/useListSelection'; +const EMPTY_OBJECT = {}; + /** * Create the state required to render List. * @@ -71,6 +72,14 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): } }); + const selectableListProps = React.useMemo( + () => ({ + role: 'listbox', + 'aria-multiselectable': selectionMode === 'multiselect' ? true : undefined, + }), + [selectionMode], + ); + return { components: { root: 'ul', @@ -78,7 +87,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): root: slot.always( getIntrinsicElementProps('ul', { ref, - ...(selectable ? selection.getListProps() : {}), + ...(selectable ? selectableListProps : EMPTY_OBJECT), ...arrowNavigationAttributes, ...props, }), diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index ca6549da02cdd4..2bfe76b9d6183f 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -7,6 +7,24 @@ import { Checkmark16Filled } from '@fluentui/react-icons'; const EMPTY_OBJECT = {}; +const listPropsForSelected = { + tabIndex: 0, + role: 'option', + 'aria-selected': true, + checkmark: { + children: , + }, +}; + +const listPropsForNotSelected = { + tabIndex: 0, + role: 'option', + 'aria-selected': false, + checkmark: { + children: null, + }, +}; + /** * Create the state required to render ListItem. * @@ -26,7 +44,8 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref ctx.selection?.toggleItem); const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); - const selectionProps = useListContext_unstable(ctx => ctx.selection?.getListItemProps(value) || EMPTY_OBJECT); + + const listItemProps = isSelected ? listPropsForSelected : listPropsForNotSelected; const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); @@ -73,7 +92,7 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref void; clearSelection: (e: React.SyntheticEvent) => void; toggleAllItems: (e: React.SyntheticEvent, itemIds: string[] | number[]) => void; - getListProps: () => Pick, 'role' | 'aria-multiselectable'>; - getListItemProps: ( - value: string | number, - ) => Pick, 'tabIndex' | 'role' | 'aria-selected'>; - selectedItems: Set; + selectedItems: string[] | number[]; }; export type UseListSelectionOptions = {}; // multiselect etc diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 57cc20c9dee582..c31df7b4afd8f0 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -10,14 +10,10 @@ export const defaultListSelectionState: ListSelectionState = { deselectItem: () => undefined, clearSelection: () => undefined, toggleAllItems: () => undefined, - getListProps: () => ({}), - getListItemProps: () => ({}), - selectedItems: new Set(), + selectedItems: [], }; -export function useListSelection( - options: SelectionHookParams = { selectionMode: 'multiselect' }, -) { +export function useListSelection(options: SelectionHookParams = { selectionMode: 'multiselect' }): ListSelectionState { const { selectionMode, defaultSelectedItems, onSelectionChange } = options; const [selectedItems, setSelectedItems] = useControllableState({ @@ -54,40 +50,6 @@ export function useListSelection( const clearSelection: ListSelectionState['clearSelection'] = useEventCallback(e => selectionMethods.clearItems(e)); - const getListProps: ListSelectionState['getListProps'] = () => { - return { - role: 'listbox', - 'aria-multiselectable': selectionMode === 'multiselect' ? true : undefined, - }; - }; - - const listPropsForSelected = React.useMemo(() => { - return { - tabIndex: 0, - role: 'option', - 'aria-selected': true, - checkmark: { - children: , - }, - }; - }, []); - - const listPropsForNotSelected = React.useMemo(() => { - return { - tabIndex: 0, - role: 'option', - 'aria-selected': false, - checkmark: { - children: null, - }, - }; - }, []); - - const getListItemProps: ListSelectionState['getListItemProps'] = React.useCallback( - value => (selectionMethods.isSelected(value) ? listPropsForSelected : listPropsForNotSelected), - [listPropsForNotSelected, listPropsForSelected, selectionMethods], - ); - const selectedArray = React.useMemo(() => Array.from(selected), [selected]); return { @@ -99,7 +61,5 @@ export function useListSelection( setSelectedItems, isSelected: (id: string | number) => selectionMethods.isSelected(id), clearSelection, - getListProps, - getListItemProps, }; } diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx index fc6924826f9ae7..e2dbb29c0112d9 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx @@ -56,8 +56,7 @@ const MyListItem: React.FC<{ name: string; avatar: string; toggleItem: (e: React.SyntheticEvent, id: string) => void; - selectionProps: ReturnType; -}> = React.memo(({ name, avatar, toggleItem, selectionProps }) => { +}> = React.memo(({ name, avatar, toggleItem }) => { const onClick = useEventCallback((e: React.MouseEvent) => toggleItem(e, name)); const onKeyDown = useEventCallback((e: React.KeyboardEvent) => { if (e.key === ' ') { @@ -67,7 +66,7 @@ const MyListItem: React.FC<{ }); return ( - + {
- + {items.map(({ name, avatar }) => ( - + ))} @@ -135,10 +128,6 @@ ListSelectionControlled.parameters = { 'In the controlled approach you are in charge of the selection state. First, you create the state using ``useListSelection` hook. This will return a selection state object with a handful of useful methods to control the selection state.', '', 'In this case, you are in control of deciding what item should be selected and when, including listening on events and calling the `selection` methods.', - '', - 'The `selection` object also provides utility functions like `getListProps` and `getListItemProps`. These functions return props that should be applied to the List and ListItems respectively to ensure the right accessibility attributes are passed.', - '', - 'The `getListItemProps` also configures the `checkmark` hook to visualize the selection state of the item. Feel free to override this behavior by passing your own `checkmark` prop to the ListItem.', ].join('\n'), }, }, From 2a4fa8c0968ecea0fe1c10c8f18deed059625e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 12:05:18 +0100 Subject: [PATCH 041/136] Add validation for proper use of elements --- .../src/components/List/List.types.ts | 1 + .../src/components/List/listContext.ts | 1 + .../src/components/List/useList.ts | 9 +++++--- .../components/List/useListContextValues.ts | 5 +++-- .../src/components/ListItem/useListItem.tsx | 21 ++++++++++++++++--- .../react-list-preview/src/hooks/types.ts | 6 +++--- .../src/hooks/useListSelection.tsx | 2 +- 7 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 736aa33d8f9f51..ad0c9775c26396 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -29,6 +29,7 @@ export type ListContextValue = { selection?: ListSelectionState; registerItem?: (id: string | number, ref: React.RefObject) => void; deregisterItem?: (id: string | number, ref: React.RefObject) => void; + as?: 'div' | 'ol' | 'ul'; }; export type ListContextValues = { diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index 9e626dda76408d..fe9dc32b38a305 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -12,6 +12,7 @@ export const listContextDefaultValue: ListContextValue = { registerItem: noop, deregisterItem: noop, selection: undefined, + as: undefined, }; const listContext = createContext(undefined); diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 6f3c3b9a2ec03a..791c3f48fb4fbe 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -11,6 +11,7 @@ import { ListProps, ListState } from './List.types'; import { useListSelection } from '../../hooks/useListSelection'; const EMPTY_OBJECT = {}; +const DEFAULT_ROOT_EL_TYPE = 'ul'; /** * Create the state required to render List. @@ -30,6 +31,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): selectionMode = 'multiselect', selectedItems, defaultSelectedItems, + as, onSelectionChange, } = props; @@ -82,20 +84,21 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): return { components: { - root: 'ul', + root: DEFAULT_ROOT_EL_TYPE, }, root: slot.always( - getIntrinsicElementProps('ul', { + getIntrinsicElementProps(DEFAULT_ROOT_EL_TYPE, { ref, ...(selectable ? selectableListProps : EMPTY_OBJECT), ...arrowNavigationAttributes, ...props, }), - { elementType: 'ul' }, + { elementType: DEFAULT_ROOT_EL_TYPE }, ), layout, // context: focusableItems, + as: as || DEFAULT_ROOT_EL_TYPE, items, registerItem: selectable ? registerItem : undefined, deregisterItem: selectable ? deregisterItem : undefined, diff --git a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts index 01c8d65bf340ec..de43305389bda1 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { ListContextValues, ListState } from './List.types'; export function useListContextValues_unstable(state: ListState): ListContextValues { - const { focusableItems, items, registerItem, deregisterItem, selection } = state; + const { focusableItems, items, registerItem, deregisterItem, selection, as } = state; const listContext = React.useMemo( () => ({ @@ -11,8 +11,9 @@ export function useListContextValues_unstable(state: ListState): ListContextValu registerItem, deregisterItem, selection, + as, }), - [focusableItems, items, registerItem, deregisterItem, selection], + [focusableItems, items, registerItem, deregisterItem, selection, as], ); return { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 2bfe76b9d6183f..82a8b4fab3ba5c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -6,6 +6,7 @@ import { useListContext_unstable } from '../List/listContext'; import { Checkmark16Filled } from '@fluentui/react-icons'; const EMPTY_OBJECT = {}; +const DEFAULT_ROOT_EL_TYPE = 'li'; const listPropsForSelected = { tabIndex: 0, @@ -25,6 +26,15 @@ const listPropsForNotSelected = { }, }; +function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', renderedAs?: 'div' | 'li') { + if (renderedAs === 'div' && parentRenderedAs !== 'div') { + throw new Error('ListItem cannot be rendered as a div when its parent is not a div.'); + } + if (renderedAs === 'li' && parentRenderedAs === 'div') { + throw new Error('ListItem cannot be rendered as a li when its parent is a div.'); + } +} + /** * Create the state required to render ListItem. * @@ -45,6 +55,11 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); + const parentRenderedAs = useListContext_unstable(ctx => ctx.as); + const renderedAs = props.as || DEFAULT_ROOT_EL_TYPE; + + validateProperElementTypes(parentRenderedAs, renderedAs); + const listItemProps = isSelected ? listPropsForSelected : listPropsForNotSelected; const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); @@ -87,7 +102,7 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref boolean; @@ -9,7 +8,8 @@ export type ListSelectionState = { selectItem: (e: React.SyntheticEvent, id: string | number) => void; clearSelection: (e: React.SyntheticEvent) => void; toggleAllItems: (e: React.SyntheticEvent, itemIds: string[] | number[]) => void; - selectedItems: string[] | number[]; + setSelectedItems: React.Dispatch>>; + selectedItems: SelectionItemId[]; }; export type UseListSelectionOptions = {}; // multiselect etc diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index c31df7b4afd8f0..00745456ca8a2c 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -1,4 +1,3 @@ -import { Checkmark16Filled } from '@fluentui/react-icons'; import { SelectionHookParams, useControllableState, useEventCallback, useSelection } from '@fluentui/react-utilities'; import * as React from 'react'; import { ListSelectionState } from './types'; @@ -10,6 +9,7 @@ export const defaultListSelectionState: ListSelectionState = { deselectItem: () => undefined, clearSelection: () => undefined, toggleAllItems: () => undefined, + setSelectedItems: () => undefined, selectedItems: [], }; From a6006ec14f962cc08ec4a82d658436e12d2eb60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 12:08:15 +0100 Subject: [PATCH 042/136] remove customArrowNavigationOptions, let the user have full responsibility over the tabster attributes if necessary --- packages/react-components/react-list-preview/docs/MIGRATION.md | 3 +-- .../react-list-preview/etc/react-list-preview.api.md | 1 - .../react-list-preview/src/components/List/List.types.ts | 2 -- .../react-list-preview/src/components/List/useList.ts | 2 -- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/react-components/react-list-preview/docs/MIGRATION.md b/packages/react-components/react-list-preview/docs/MIGRATION.md index 138fbcf445a8d2..31475d17a7c85e 100644 --- a/packages/react-components/react-list-preview/docs/MIGRATION.md +++ b/packages/react-components/react-list-preview/docs/MIGRATION.md @@ -69,7 +69,6 @@ Most of the v8 props are for it's virtualization functionality. Since the v9 `Li | `startIndex` | N/A | | `usePageCache` | N/A | | `version` | N/A | -| - | `customArrowNavigationOptions` | | - | `defaultSelectedItems` | | - | `focusableItems` | | - | `layout` | @@ -157,7 +156,7 @@ We recommend using a component like `Persona` where possible, or creating a cust | v0 List | v9 List | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `accessibility` | `customArrowNavigationOptions` from `tabster` | +| `accessibility` | built in, customize with `useArrowNavigationGroup` from `tabster` | | `as` | `as` | | `className` | `className` | | `debug` | N/A | diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 13a3e66737b415..adfaec756c2461 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -80,7 +80,6 @@ export enum ListLayout { // @public export type ListProps = ComponentProps & { layout?: ListLayout; - customArrowNavigationOptions?: Partial; focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode_2; diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index ad0c9775c26396..8c1a35056c8e11 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,6 +1,5 @@ import * as React from 'react'; import type { ComponentProps, ComponentState, Slot, SelectionMode, SelectionItemId } from '@fluentui/react-utilities'; -import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; import { ListSelectionState } from '../../hooks/types'; export type ListLayout = 'horizontal' | 'vertical' | 'grid'; @@ -14,7 +13,6 @@ export type ListSlots = { */ export type ListProps = ComponentProps & { layout?: ListLayout; - customArrowNavigationOptions?: Partial; focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 791c3f48fb4fbe..591a410ee77635 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -26,7 +26,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): const { layout = 'vertical', focusableItems = false, - // customArrowNavigationOptions, selectable = false, selectionMode = 'multiselect', selectedItems, @@ -38,7 +37,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): const arrowNavigationAttributes = useArrowNavigationGroup({ axis: layout === 'grid' ? 'grid-linear' : 'both', memorizeCurrent: true, - // ...(customArrowNavigationOptions || {}), }); const [items, setItems] = React.useState>([]); From e9e72981259de6ffcd33c3e7577eb1b5d1497b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 12:22:33 +0100 Subject: [PATCH 043/136] remove item registration/deregistration logic --- .../src/components/List/List.types.ts | 2 -- .../src/components/List/listContext.ts | 6 ------ .../src/components/List/useList.ts | 14 -------------- .../src/components/List/useListContextValues.ts | 6 ++---- .../src/components/ListItem/useListItem.tsx | 12 ------------ 5 files changed, 2 insertions(+), 38 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 8c1a35056c8e11..e30a8c7071b200 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -25,8 +25,6 @@ export type ListContextValue = { focusableItems: boolean; items: Array<{ id: string | number }>; selection?: ListSelectionState; - registerItem?: (id: string | number, ref: React.RefObject) => void; - deregisterItem?: (id: string | number, ref: React.RefObject) => void; as?: 'div' | 'ol' | 'ul'; }; diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index fe9dc32b38a305..270a9c26bd1da1 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -2,15 +2,9 @@ import { createContext, useContextSelector } from '@fluentui/react-context-selec import type { ContextSelector } from '@fluentui/react-context-selector'; import { ListContextValue } from './List.types'; -const noop = () => { - /*noop*/ -}; - export const listContextDefaultValue: ListContextValue = { focusableItems: false, items: [], - registerItem: noop, - deregisterItem: noop, selection: undefined, as: undefined, }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 591a410ee77635..6ff3f525b16976 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -60,18 +60,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): defaultSelectedItems, }); - const registerItem = useEventCallback((id: string | number) => { - if (!items.find(item => item.id === id)) { - setItems(current => [...current, { id }]); - } - }); - - const deregisterItem = useEventCallback((id: string | number) => { - if (items.find(k => k.id === id)) { - setItems(current => current.filter(item => item.id !== id)); - } - }); - const selectableListProps = React.useMemo( () => ({ role: 'listbox', @@ -98,8 +86,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): focusableItems, as: as || DEFAULT_ROOT_EL_TYPE, items, - registerItem: selectable ? registerItem : undefined, - deregisterItem: selectable ? deregisterItem : undefined, // only pass down selection state if its handled internally, otherwise just report the events selection: selectable ? selection : undefined, }; diff --git a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts index de43305389bda1..2a9b6eccca9f9f 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts @@ -2,18 +2,16 @@ import * as React from 'react'; import { ListContextValues, ListState } from './List.types'; export function useListContextValues_unstable(state: ListState): ListContextValues { - const { focusableItems, items, registerItem, deregisterItem, selection, as } = state; + const { focusableItems, items, selection, as } = state; const listContext = React.useMemo( () => ({ focusableItems, items, - registerItem, - deregisterItem, selection, as, }), - [focusableItems, items, registerItem, deregisterItem, selection, as], + [focusableItems, items, selection, as], ); return { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 82a8b4fab3ba5c..f4164803105549 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -49,8 +49,6 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref ctx.focusableItems); - const registerItem = useListContext_unstable(ctx => ctx.registerItem); - const deregisterItem = useListContext_unstable(ctx => ctx.deregisterItem); const toggleItem = useListContext_unstable(ctx => ctx.selection?.toggleItem); const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); @@ -66,16 +64,6 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref(null); - React.useEffect(() => { - registerItem?.(value, innerRef); - - return () => { - deregisterItem?.(value, innerRef); - }; - // Always make sure the dependencies are stable across re-renders, otherwise we go - // in a loop of registering and deregistering. - }, [innerRef, value, registerItem, deregisterItem]); - const handleKeyDown: typeof onKeyDown = useEventCallback(e => { onKeyDown?.(e); From dcbb0af4a6959878e33aef1e3e9cc50867350372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 12:25:53 +0100 Subject: [PATCH 044/136] remove unused items --- .../react-list-preview/src/components/List/List.types.ts | 1 - .../react-list-preview/src/components/List/listContext.ts | 1 - .../react-list-preview/src/components/List/useList.ts | 3 --- 3 files changed, 5 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index e30a8c7071b200..9e566fa8529f97 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -23,7 +23,6 @@ export type ListProps = ComponentProps & { export type ListContextValue = { focusableItems: boolean; - items: Array<{ id: string | number }>; selection?: ListSelectionState; as?: 'div' | 'ol' | 'ul'; }; diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index 270a9c26bd1da1..98a46318b403e6 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -4,7 +4,6 @@ import { ListContextValue } from './List.types'; export const listContextDefaultValue: ListContextValue = { focusableItems: false, - items: [], selection: undefined, as: undefined, }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 6ff3f525b16976..1f7c5c614f3315 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -39,8 +39,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): memorizeCurrent: true, }); - const [items, setItems] = React.useState>([]); - const [selectionState, setSelectionState] = useControllableState({ state: selectedItems, defaultState: defaultSelectedItems, @@ -85,7 +83,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref): // context: focusableItems, as: as || DEFAULT_ROOT_EL_TYPE, - items, // only pass down selection state if its handled internally, otherwise just report the events selection: selectable ? selection : undefined, }; From fa315de2e57875d98029c0b3843ef0b9fcf2f0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 14:07:19 +0100 Subject: [PATCH 045/136] remove unused items, fix compiler --- .../etc/react-list-preview.api.md | 50 +++++-------------- .../src/components/List/useList.ts | 2 +- .../components/List/useListContextValues.ts | 5 +- .../src/components/ListItem/ListItem.tsx | 14 +++--- .../src/components/ListItem/useListItem.tsx | 25 +++++----- .../react-list-preview/src/index.ts | 2 +- .../ListSelectionUncontrolled.stories.tsx | 2 +- 7 files changed, 38 insertions(+), 62 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index adfaec756c2461..8b048ee8c660a9 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -10,30 +10,23 @@ import { Button } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import type { OnSelectionChangeData } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import { SelectionHookParams } from '@fluentui/react-utilities'; import { SelectionItemId } from '@fluentui/react-utilities'; import type { SelectionMode as SelectionMode_2 } from '@fluentui/react-utilities'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import type { UseArrowNavigationGroupOptions } from '@fluentui/react-components'; // @public (undocumented) -export type ListComponentRef = { - selection: ListState['selection']; -}; - -// @public export const List: ForwardRefComponent; // @public (undocumented) export const listClassNames: SlotClassNames; -// @public +// @public (undocumented) export const ListItem: ForwardRefComponent; -// @public +// @public (undocumented) export const ListItemButton: ForwardRefComponent; // @public (undocumented) @@ -60,7 +53,7 @@ export type ListItemProps = ComponentProps & { // @public (undocumented) export type ListItemSlots = { - root: NonNullable>; + root: NonNullable>; checkmark?: Slot<'div'>; }; @@ -68,14 +61,7 @@ export type ListItemSlots = { export type ListItemState = ComponentState & {}; // @public (undocumented) -export enum ListLayout { - // (undocumented) - Grid = "grid", - // (undocumented) - Horizontal = "horizontal", - // (undocumented) - Vertical = "vertical" -} +export type ListLayout = 'horizontal' | 'vertical' | 'grid'; // @public export type ListProps = ComponentProps & { @@ -83,14 +69,16 @@ export type ListProps = ComponentProps & { focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode_2; + selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; - onSelectionChange?: (event: React_2.SyntheticEvent, data: OnSelectionChangeData) => void; - componentRef?: React_2.Ref; + onSelectionChange?: (event: React_2.SyntheticEvent, data: { + selectedItems: SelectionItemId[]; + }) => void; }; // @public (undocumented) export type ListSlots = { - root: NonNullable>; + root: NonNullable>; }; // @public @@ -106,10 +94,10 @@ export const renderListItem_unstable: (state: ListItemState) => JSX.Element; export const renderListItemButton_unstable: (state: ListItemButtonState) => JSX.Element; // @public -export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; +export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; // @public -export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref) => ListItemState; +export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref) => ListItemState; // @public export const useListItemButton_unstable: (props: ListItemButtonProps, ref: React_2.Ref) => ListItemButtonState; @@ -121,21 +109,7 @@ export const useListItemButtonStyles_unstable: (state: ListItemButtonState) => L export const useListItemStyles_unstable: (state: ListItemState) => ListItemState; // @public (undocumented) -export function useListSelection(options?: SelectionHookParams): (listState: ListFeaturesState) => { - selection: { - selectedItems: Set; - toggleItem: (e: React_2.SyntheticEvent, id: string | number) => void; - toggleAllItems: (e: React_2.SyntheticEvent) => void; - deselectItem: (e: React_2.SyntheticEvent, id: string | number) => void; - selectItem: (e: React_2.SyntheticEvent, id: string | number) => void; - setSelectedItems: React_2.Dispatch>>; - isSelected: (id: string | number) => boolean; - clearSelection: (e: React_2.SyntheticEvent) => void; - }; - items: TItem[]; -}; +export function useListSelection(options?: SelectionHookParams): ListSelectionState; // @public export const useListStyles_unstable: (state: ListState) => ListState; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 1f7c5c614f3315..c44468284a5e01 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -22,7 +22,7 @@ const DEFAULT_ROOT_EL_TYPE = 'ul'; * @param props - props from this instance of List * @param ref - reference to root HTMLElement of List */ -export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { +export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { const { layout = 'vertical', focusableItems = false, diff --git a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts index 2a9b6eccca9f9f..b402c92e5b273b 100644 --- a/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts +++ b/packages/react-components/react-list-preview/src/components/List/useListContextValues.ts @@ -2,16 +2,15 @@ import * as React from 'react'; import { ListContextValues, ListState } from './List.types'; export function useListContextValues_unstable(state: ListState): ListContextValues { - const { focusableItems, items, selection, as } = state; + const { focusableItems, selection, as } = state; const listContext = React.useMemo( () => ({ focusableItems, - items, selection, as, }), - [focusableItems, items, selection, as], + [focusableItems, selection, as], ); return { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx index 91e07576d025bb..c9ce02e04c8ac5 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.tsx @@ -6,12 +6,14 @@ import { renderListItem_unstable } from './renderListItem'; import { useListItemStyles_unstable } from './useListItemStyles.styles'; import type { ListItemProps } from './ListItem.types'; -export const ListItem: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useListItem_unstable(props, ref); +export const ListItem: ForwardRefComponent = React.forwardRef( + (props, ref) => { + const state = useListItem_unstable(props, ref); - useListItemStyles_unstable(state); - useCustomStyleHook_unstable('useListItemStyles_unstable')(state); - return renderListItem_unstable(state); -}); + useListItemStyles_unstable(state); + useCustomStyleHook_unstable('useListItemStyles_unstable')(state); + return renderListItem_unstable(state); + }, +); ListItem.displayName = 'ListItem'; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index f4164803105549..049b1d85cedb37 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; -import { getIntrinsicElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps, slot, useEventCallback, useId } from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; import { Checkmark16Filled } from '@fluentui/react-icons'; @@ -42,9 +42,12 @@ function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', rend * before being passed to renderListItem_unstable. * * @param props - props from this instance of ListItem - * @param ref - reference to root HTMLElement of ListItem + * @param ref - reference to root HTMLLIElement | HTMLDivElementof ListItem */ -export const useListItem_unstable = (props: ListItemProps, ref: React.Ref): ListItemState => { +export const useListItem_unstable = ( + props: ListItemProps, + ref: React.Ref, +): ListItemState => { const id = useId('listItem'); const { value = id, onKeyDown, onClick } = props; @@ -62,14 +65,12 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref(null); - - const handleKeyDown: typeof onKeyDown = useEventCallback(e => { + const handleKeyDown: React.KeyboardEventHandler = useEventCallback(e => { onKeyDown?.(e); // Compare targets to make sure this only triggers when the event is fired on the list item // and not on a button inside - if (e.defaultPrevented || e.target !== e.currentTarget) { + if (!isSelectionEnabled || e.defaultPrevented || e.target !== e.currentTarget) { return; } @@ -79,10 +80,10 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref { + const handleClick: React.MouseEventHandler = useEventCallback(e => { onClick?.(e); - if (e.defaultPrevented) { + if (!isSelectionEnabled || e.defaultPrevented) { return; } @@ -91,15 +92,15 @@ export const useListItem_unstable = (props: ListItemProps, ref: React.Ref, tabIndex: focusableItems ? 0 : undefined, role: 'listitem', id: String(value), ...(isSelectionEnabled ? listItemProps : EMPTY_OBJECT), ...focusableGroupAttrs, ...props, - onKeyDown: isSelectionEnabled ? handleKeyDown : onKeyDown, - onClick: isSelectionEnabled ? handleClick : onClick, + onKeyDown: handleKeyDown, + onClick: handleClick, }), { elementType: DEFAULT_ROOT_EL_TYPE }, ); diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index 73deee131547a4..ad2d035d41417c 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -1,6 +1,6 @@ export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List'; -export type { ListProps, ListSlots, ListState, ListComponentRef, ListLayout } from './List'; +export type { ListProps, ListSlots, ListState, ListLayout } from './List'; export { ListItem, listItemClassNames, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index fce2f59d069450..da022620fcba9d 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -1,5 +1,5 @@ import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; -import { ListComponentRef, List, ListItem } from '@fluentui/react-list-preview'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; const names = [ From e3bcb1d0289a0d2174900d7853c2031a9c01b8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 14:12:19 +0100 Subject: [PATCH 046/136] bump dep versions to latest --- packages/react-components/react-list-preview/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 5843c940def2b0..9eb48a8a7c2695 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -37,12 +37,12 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-button": "^9.3.56", + "@fluentui/react-checkbox": "^9.2.0", + "@fluentui/react-context-selector": "^9.1.42", "@fluentui/react-icons": "^2.0.217", "@fluentui/react-jsx-runtime": "^9.0.19", - "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-tabster": "^9.14.6", "@fluentui/react-theme": "^9.1.16", "@fluentui/react-utilities": "^9.15.2", "@fluentui/react-shared-contexts": "^9.13.0", From b3204766d8ec96af90aaa3b2d570cc0bb7ed145c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 14:13:12 +0100 Subject: [PATCH 047/136] tsconfig.base update --- tsconfig.base.all.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index 32221b1630da16..270a2459490cb3 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -92,6 +92,7 @@ "@fluentui/react-aria": ["packages/react-components/react-aria/src/index.ts"], "@fluentui/react-avatar": ["packages/react-components/react-avatar/src/index.ts"], "@fluentui/react-badge": ["packages/react-components/react-badge/src/index.ts"], + "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/src/index.ts"], "@fluentui/react-button": ["packages/react-components/react-button/src/index.ts"], "@fluentui/react-calendar-compat": ["packages/react-components/react-calendar-compat/src/index.ts"], "@fluentui/react-card": ["packages/react-components/react-card/src/index.ts"], @@ -120,6 +121,7 @@ "@fluentui/react-jsx-runtime/jsx-runtime": ["packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], "@fluentui/react-label": ["packages/react-components/react-label/src/index.ts"], "@fluentui/react-link": ["packages/react-components/react-link/src/index.ts"], + "@fluentui/react-list-preview": ["packages/react-components/react-list-preview/src/index.ts"], "@fluentui/react-menu": ["packages/react-components/react-menu/src/index.ts"], "@fluentui/react-message-bar": ["packages/react-components/react-message-bar/src/index.ts"], "@fluentui/react-migration-v0-v9": ["packages/react-components/react-migration-v0-v9/src/index.ts"], @@ -154,6 +156,9 @@ "@fluentui/react-tabs": ["packages/react-components/react-tabs/src/index.ts"], "@fluentui/react-tabster": ["packages/react-components/react-tabster/src/index.ts"], "@fluentui/react-tags": ["packages/react-components/react-tags/src/index.ts"], + "@fluentui/react-teaching-popover-preview": [ + "packages/react-components/react-teaching-popover-preview/src/index.ts" + ], "@fluentui/react-text": ["packages/react-components/react-text/src/index.ts"], "@fluentui/react-textarea": ["packages/react-components/react-textarea/src/index.ts"], "@fluentui/react-theme": ["packages/react-components/react-theme/src/index.ts"], @@ -169,11 +174,7 @@ "@fluentui/react-virtualizer": ["packages/react-components/react-virtualizer/src/index.ts"], "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], "@fluentui/tokens": ["packages/tokens/src/index.ts"], - "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"], - "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/src/index.ts"], - "@fluentui/react-teaching-popover-preview": [ - "packages/react-components/react-teaching-popover-preview/src/index.ts" - ] + "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] } } } From 59647048ac10aaa9c865b249da222437849a966e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 14:18:00 +0100 Subject: [PATCH 048/136] remove gitkeep --- packages/react-components/react-list-preview/stories/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/react-components/react-list-preview/stories/.gitkeep diff --git a/packages/react-components/react-list-preview/stories/.gitkeep b/packages/react-components/react-list-preview/stories/.gitkeep deleted file mode 100644 index e69de29bb2d1d6..00000000000000 From a9a459c77f7a8943b777f3f0298fce966da7bc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 14:52:48 +0100 Subject: [PATCH 049/136] bad merge --- .../src/index.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/react-components/react-teaching-popover-preview/src/index.ts b/packages/react-components/react-teaching-popover-preview/src/index.ts index 927208354bf44f..97f3b2ab0b6e0c 100644 --- a/packages/react-components/react-teaching-popover-preview/src/index.ts +++ b/packages/react-components/react-teaching-popover-preview/src/index.ts @@ -1,16 +1,8 @@ -export { TeachingPopoverHeader, renderTeachingPopoverHeader_unstable, teachingPopoverHeaderClassNames, useTeachingPopoverHeaderStyles_unstable, useTeachingPopoverHeader_unstable } from './TeachingPopoverHeader'; -export type { TeachingPopoverHeaderProps, TeachingPopoverHeaderSlots, TeachingPopoverHeaderState } from './TeachingPopoverHeader'; -export { TeachingPopoverActions, renderTeachingPopoverActions_unstable, teachingPopoverActionsClassNames, useTeachingPopoverActionsStyles_unstable, useTeachingPopoverActions_unstable } from './TeachingPopoverActions'; -export type { TeachingPopoverActionsProps, TeachingPopoverActionsSlots, TeachingPopoverActionsState } from './TeachingPopoverActions'; -export { TeachingPopoverBody, renderTeachingPopoverBody_unstable, teachingPopoverBodyClassNames, useTeachingPopoverBodyStyles_unstable, useTeachingPopoverBody_unstable } from './TeachingPopoverBody'; -export type { TeachingPopoverBodyProps, TeachingPopoverBodySlots, TeachingPopoverBodyState } from './TeachingPopoverBody'; -export { TeachingPopoverButton, renderTeachingPopoverButton_unstable, teachingPopoverButtonClassNames, useTeachingPopoverButtonStyles_unstable, useTeachingPopoverButton_unstable } from './TeachingPopoverButton'; -export type { TeachingPopoverButtonProps, TeachingPopoverButtonSlots, TeachingPopoverButtonState } from './TeachingPopoverButton'; -export { TeachingPopoverCarousel, renderTeachingPopoverCarousel_unstable, teachingPopoverCarouselClassNames, useTeachingPopoverCarouselStyles_unstable, useTeachingPopoverCarousel_unstable } from './TeachingPopoverCarousel'; -export type { TeachingPopoverCarouselProps, TeachingPopoverCarouselSlots, TeachingPopoverCarouselState } from './TeachingPopoverCarousel'; -export { TeachingPopoverPageCount, renderTeachingPopoverPageCount_unstable, teachingPopoverPageCountClassNames, useTeachingPopoverPageCountStyles_unstable, useTeachingPopoverPageCount_unstable } from './TeachingPopoverPageCount'; -export type { TeachingPopoverPageCountProps, TeachingPopoverPageCountSlots, TeachingPopoverPageCountState } from './TeachingPopoverPageCount'; -export { TeachingPopoverSurface, renderTeachingPopoverSurface_unstable, teachingPopoverSurfaceClassNames, useTeachingPopoverSurfaceStyles_unstable, useTeachingPopoverSurface_unstable } from './TeachingPopoverSurface'; -export type { TeachingPopoverSurfaceProps, TeachingPopoverSurfaceSlots, TeachingPopoverSurfaceState } from './TeachingPopoverSurface'; -export { TeachingPopoverTitle, renderTeachingPopoverTitle_unstable, teachingPopoverTitleClassNames, useTeachingPopoverTitleStyles_unstable, useTeachingPopoverTitle_unstable } from './TeachingPopoverTitle'; -export type { TeachingPopoverTitleProps, TeachingPopoverTitleSlots, TeachingPopoverTitleState } from './TeachingPopoverTitle'; +export * from './TeachingPopoverHeader'; +export * from './TeachingPopoverActions'; +export * from './TeachingPopoverBody'; +export * from './TeachingPopoverButton'; +export * from './TeachingPopoverCarousel'; +export * from './TeachingPopoverPageCount'; +export * from './TeachingPopoverSurface'; +export * from './TeachingPopoverTitle'; From 48ff239bddc49dd27188ec22964d62be0fbcb08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 22 Nov 2023 15:24:47 +0100 Subject: [PATCH 050/136] fix story --- .../List/VirtualizedListWithActionableItems.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx index 1fca6505e8ff90..4dc0903dff1ab1 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx @@ -201,7 +201,9 @@ const countries = [ 'Zimbabwe', ]; -const CountriesList = (props: React.ComponentProps) => ; +const CountriesList = React.forwardRef((props: React.ComponentProps, ref) => ( + +)); export const VirtualizedListWithActionableItems = () => { return ( From c860acc48ebe005096028bcaa9fd56fdb77e8ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 12:09:55 +0100 Subject: [PATCH 051/136] Redesign completely the ListItemButton component, fix tests --- .../src/components/List/List.test.tsx | 6 +++++ .../List/__snapshots__/List.test.tsx.snap | 5 ++-- .../src/components/ListItem/ListItem.test.tsx | 9 +++++++ .../__snapshots__/ListItem.test.tsx.snap | 7 +++-- .../ListItemButton/ListItemButton.test.tsx | 1 + .../ListItemButton/ListItemButton.tsx | 10 +++---- .../ListItemButton/ListItemButton.types.ts | 16 ----------- .../ListItemButton.test.tsx.snap | 7 ++--- .../src/components/ListItemButton/index.ts | 3 --- .../ListItemButton/renderListItemButton.tsx | 13 --------- .../ListItemButton/useListItemButton.ts | 25 ----------------- .../useListItemButtonStyles.styles.ts | 27 ++++++++----------- 12 files changed, 43 insertions(+), 86 deletions(-) delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.types.ts delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/renderListItemButton.tsx delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButton.ts diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 91866c402c08d3..b3d0d0a739038f 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -7,6 +7,12 @@ describe('List', () => { isConformant({ Component: List, displayName: 'List', + testOptions: { + 'consistent-callback-args': { + // onSelectionChange has an eventArguent which is React.SyntheticEvent. This throws an error during testing + ignoreProps: ['onSelectionChange'], + }, + }, }); // TODO add more tests here, and create visual regression tests in /apps/vr-tests diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap index c7e02b09599b43..f85fc4cca0f365 100644 --- a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -2,10 +2,11 @@ exports[`List renders a default state 1`] = `
-
Default List -
+
`; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx index 0b23f1dc97ad24..1047b85577690c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx @@ -7,6 +7,15 @@ describe('ListItem', () => { isConformant({ Component: ListItem, displayName: 'ListItem', + testOptions: { + 'has-static-classnames': [ + { + props: { + checkmark: 'test checkmark', + }, + }, + ], + }, }); // TODO add more tests here, and create visual regression tests in /apps/vr-tests diff --git a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap index 21a24d5f673ba6..dc5d44d8058f10 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap @@ -2,10 +2,13 @@ exports[`ListItem renders a default state 1`] = `
-
Default ListItem -
+
`; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx index 7889695b1cf03c..0f48393b580a1f 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx @@ -7,6 +7,7 @@ describe('ListItemButton', () => { isConformant({ Component: ListItemButton, displayName: 'ListItemButton', + disabledTests: ['component-has-static-classnames-object'], }); // TODO add more tests here, and create visual regression tests in /apps/vr-tests diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx index ec79fb24c837bf..67ec32607ef57d 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx @@ -1,17 +1,15 @@ import * as React from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; -import { useListItemButton_unstable } from './useListItemButton'; -import { renderListItemButton_unstable } from './renderListItemButton'; import { useListItemButtonStyles_unstable } from './useListItemButtonStyles.styles'; -import type { ListItemButtonProps } from './ListItemButton.types'; +import { ButtonProps, renderButton_unstable, useButton_unstable } from '@fluentui/react-button'; -export const ListItemButton: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useListItemButton_unstable(props, ref); +export const ListItemButton: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useButton_unstable({ appearance: 'transparent', ...props }, ref); useListItemButtonStyles_unstable(state); useCustomStyleHook_unstable('useListItemButtonStyles_unstable')(state); - return renderListItemButton_unstable(state); + return renderButton_unstable(state); }); ListItemButton.displayName = 'ListItemButton'; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.types.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.types.ts deleted file mode 100644 index bb9c3d3d484b6e..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Button } from '@fluentui/react-button'; -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; - -export type ListItemButtonSlots = { - root: Slot; -}; - -/** - * ListItemButton Props - */ -export type ListItemButtonProps = ComponentProps & {}; - -/** - * State used in rendering ListItemButton - */ -export type ListItemButtonState = ComponentState; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap b/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap index 2345bfffaad677..d039935d43acd5 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap @@ -2,10 +2,11 @@ exports[`ListItemButton renders a default state 1`] = `
-
Default ListItemButton -
+
`; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts index d0e6b911f3b00f..f2b857ac4363d5 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts @@ -1,5 +1,2 @@ export * from './ListItemButton'; -export * from './ListItemButton.types'; -export * from './renderListItemButton'; -export * from './useListItemButton'; export * from './useListItemButtonStyles.styles'; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/renderListItemButton.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/renderListItemButton.tsx deleted file mode 100644 index 5978374191d74e..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/renderListItemButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** @jsxRuntime automatic */ -/** @jsxImportSource @fluentui/react-jsx-runtime */ - -import { assertSlots } from '@fluentui/react-utilities'; -import type { ListItemButtonState, ListItemButtonSlots } from './ListItemButton.types'; - -/** - * Render the final JSX of ListItemButton - */ -export const renderListItemButton_unstable = (state: ListItemButtonState) => { - assertSlots(state); - return ; -}; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButton.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButton.ts deleted file mode 100644 index 3bc767a8459768..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButton.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { slot } from '@fluentui/react-utilities'; -import type { ListItemButtonProps, ListItemButtonState } from './ListItemButton.types'; -import { Button } from '@fluentui/react-button'; - -/** - * Create the state required to render ListItemButton. - * - * The returned state can be modified with hooks such as useListItemButtonStyles_unstable, - * before being passed to renderListItemButton_unstable. - * - * @param props - props from this instance of ListItemButton - * @param ref - reference to root HTMLElement of ListItemButton - */ -export const useListItemButton_unstable = ( - props: ListItemButtonProps, - ref: React.Ref, -): ListItemButtonState => { - return { - components: { - root: Button, - }, - root: slot.always(props, { elementType: Button, defaultProps: { appearance: 'transparent' } }), - }; -}; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts index e5250510c2372c..b45035107851f1 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts @@ -1,29 +1,24 @@ -import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; -import type { SlotClassNames } from '@fluentui/react-utilities'; -import type { ListItemButtonSlots, ListItemButtonState } from './ListItemButton.types'; - -export const listItemButtonClassNames: SlotClassNames = { - root: 'fui-ListItemButton', -}; +import { ButtonState, useButtonStyles_unstable } from '@fluentui/react-button'; +import { makeResetStyles, mergeClasses, shorthands } from '@griffel/react'; /** * Styles for the root slot */ -const useStyles = makeStyles({ - root: { - display: 'block', - width: '100%', - textAlign: 'left', - ...shorthands.padding(0), - }, +const useStyles = makeResetStyles({ + display: 'block', + width: '100%', + textAlign: 'left', + ...shorthands.padding(0), }); /** * Apply styling to the ListItemButton slots based on the state */ -export const useListItemButtonStyles_unstable = (state: ListItemButtonState): ListItemButtonState => { +export const useListItemButtonStyles_unstable = (state: ButtonState): ButtonState => { + useButtonStyles_unstable(state); + const styles = useStyles(); - state.root.className = mergeClasses(listItemButtonClassNames.root, styles.root, state.root.className); + state.root.className = mergeClasses(state.root.className, styles); return state; }; From a6151fc4e943e0081c6012f958161f1229e38d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 12:11:47 +0100 Subject: [PATCH 052/136] API fix --- .../etc/react-list-preview.api.md | 27 +++---------------- .../react-list-preview/src/index.ts | 9 +------ 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 8b048ee8c660a9..4e022c76ccfa9b 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -6,7 +6,8 @@ /// -import { Button } from '@fluentui/react-button'; +import { ButtonProps } from '@fluentui/react-button'; +import { ButtonState } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; @@ -27,21 +28,7 @@ export const listClassNames: SlotClassNames; export const ListItem: ForwardRefComponent; // @public (undocumented) -export const ListItemButton: ForwardRefComponent; - -// @public (undocumented) -export const listItemButtonClassNames: SlotClassNames; - -// @public -export type ListItemButtonProps = ComponentProps & {}; - -// @public (undocumented) -export type ListItemButtonSlots = { - root: Slot; -}; - -// @public -export type ListItemButtonState = ComponentState; +export const ListItemButton: ForwardRefComponent; // @public (undocumented) export const listItemClassNames: SlotClassNames; @@ -90,9 +77,6 @@ export const renderList_unstable: (state: ListState, contextValues: ListContextV // @public export const renderListItem_unstable: (state: ListItemState) => JSX.Element; -// @public -export const renderListItemButton_unstable: (state: ListItemButtonState) => JSX.Element; - // @public export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; @@ -100,10 +84,7 @@ export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListItemState; // @public -export const useListItemButton_unstable: (props: ListItemButtonProps, ref: React_2.Ref) => ListItemButtonState; - -// @public -export const useListItemButtonStyles_unstable: (state: ListItemButtonState) => ListItemButtonState; +export const useListItemButtonStyles_unstable: (state: ButtonState) => ButtonState; // @public export const useListItemStyles_unstable: (state: ListItemState) => ListItemState; diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index ad2d035d41417c..b6927b3c0a40be 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -9,13 +9,6 @@ export { useListItem_unstable, } from './ListItem'; export type { ListItemProps, ListItemSlots, ListItemState } from './ListItem'; -export { - ListItemButton, - listItemButtonClassNames, - renderListItemButton_unstable, - useListItemButtonStyles_unstable, - useListItemButton_unstable, -} from './ListItemButton'; -export type { ListItemButtonProps, ListItemButtonSlots, ListItemButtonState } from './ListItemButton'; +export { ListItemButton, useListItemButtonStyles_unstable } from './ListItemButton'; export { useListSelection } from './hooks'; From 0413fa499316c721e68c59da5c526ff642d0a591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 13:19:43 +0100 Subject: [PATCH 053/136] remove outdated story --- .../List/ListSelectionControlled.stories.tsx | 134 ------------------ 1 file changed, 134 deletions(-) delete mode 100644 packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx deleted file mode 100644 index e2dbb29c0112d9..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlled.stories.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { Button, makeStyles, Persona, shorthands, useEventCallback } from '@fluentui/react-components'; -import { List, ListItem, useListSelection } from '@fluentui/react-list-preview'; - -import * as React from 'react'; -import { ListSelectionState } from '../../src/hooks/types'; - -const names = [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', - 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', -]; - -type Item = { - name: string; - id: string; - avatar: string; -}; - -const origItems: Item[] = names.map(name => ({ - name, - id: name, - avatar: - 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', -})); - -const useStyles = makeStyles({ - wrapper: { - maxHeight: '300px', - overflowY: 'auto', - ...shorthands.padding('2px'), - }, - buttonControls: { - display: 'flex', - columnGap: '8px', - marginBottom: '16px', - }, - button: { - ...shorthands.padding(0), - }, -}); - -// This component is memoized, i.e. it will only re-render if the props change. -// This is important for performance, as we don't want to re-render the entire list -// when the selection state changes. -const MyListItem: React.FC<{ - name: string; - avatar: string; - toggleItem: (e: React.SyntheticEvent, id: string) => void; -}> = React.memo(({ name, avatar, toggleItem }) => { - const onClick = useEventCallback((e: React.MouseEvent) => toggleItem(e, name)); - const onKeyDown = useEventCallback((e: React.KeyboardEvent) => { - if (e.key === ' ') { - e.preventDefault(); - toggleItem(e, name); - } - }); - - return ( - - - - ); -}); - -export const ListSelectionControlled = () => { - const classes = useStyles(); - const [currentIndex, setCurrentIndex] = React.useState(4); - - const items = React.useMemo(() => { - return origItems.slice(0, currentIndex); - }, [currentIndex]); - - const selection = useListSelection({ - selectionMode: 'multiselect', - onSelectionChange: (_, data) => console.log(data.selectedItems), - }); - - return ( -
-
- - -
- - - {items.map(({ name, avatar }) => ( - - ))} - -
- ); -}; - -ListSelectionControlled.parameters = { - docs: { - description: { - story: [ - 'In the controlled approach you are in charge of the selection state. First, you create the state using ``useListSelection` hook. This will return a selection state object with a handful of useful methods to control the selection state.', - '', - 'In this case, you are in control of deciding what item should be selected and when, including listening on events and calling the `selection` methods.', - ].join('\n'), - }, - }, -}; From d5780de9412d9f88c2656fd6dbdc4bd1f7d01cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 14:11:41 +0100 Subject: [PATCH 054/136] fix tsc --- .../react-list-preview/src/components/List/List.test.tsx | 3 ++- .../src/components/ListItem/ListItem.test.tsx | 3 ++- .../src/components/ListItemButton/ListItemButton.test.tsx | 3 ++- .../List/ListSelectionControlledWithState.stories.tsx | 2 +- .../react-list-preview/stories/List/index.stories.tsx | 1 - .../stories/ListItem/ListItemDefault.stories.tsx | 2 +- .../stories/ListItemButton/ListItemButtonDefault.stories.tsx | 5 +++-- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index b3d0d0a739038f..1a8874c8465b67 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { List } from './List'; +import { ListProps } from './List.types'; describe('List', () => { isConformant({ - Component: List, + Component: List as React.FunctionComponent, displayName: 'List', testOptions: { 'consistent-callback-args': { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx index 1047b85577690c..55058f3b6d3802 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { ListItem } from './ListItem'; +import { ListItemProps } from './ListItem.types'; describe('ListItem', () => { isConformant({ - Component: ListItem, + Component: ListItem as React.FunctionComponent, displayName: 'ListItem', testOptions: { 'has-static-classnames': [ diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx index 0f48393b580a1f..41d75873d76eef 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { ListItemButton } from './ListItemButton'; +import { ButtonProps } from '../../../../react-button/src/Button'; describe('ListItemButton', () => { isConformant({ - Component: ListItemButton, + Component: ListItemButton as React.FunctionComponent, displayName: 'ListItemButton', disabledTests: ['component-has-static-classnames-object'], }); diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx index acfb97365895d5..94af3c57f26304 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -1,4 +1,4 @@ -import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; import { List, ListItem, useListSelection } from '@fluentui/react-list-preview'; import * as React from 'react'; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 3ef518c4eb7787..aa1bcbb846a23e 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -11,7 +11,6 @@ export { ListMultipleActions } from './ListMultipleActions.stories'; export { ListSelectionUncontrolled } from './ListSelectionUncontrolled.stories'; export { ListSelectionControlledBasic } from './ListSelectionControlledBasic.stories'; export { ListSelectionControlledWithState } from './ListSelectionControlledWithState.stories'; -export { ListSelectionControlled } from './ListSelectionControlled.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx b/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx index e38a36d8cbcfcb..2845b17e2e625a 100644 --- a/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx +++ b/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx @@ -1,4 +1,4 @@ import * as React from 'react'; import { ListItem, ListItemProps } from '@fluentui/react-list-preview'; -export const Default = (props: Partial) => ; +export const Default = (props: ListItemProps) => ; diff --git a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx b/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx index 7ab8767f6cdcf1..a566a6b6b2b56d 100644 --- a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx +++ b/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; -import { ListItemButton, ListItemButtonProps } from '@fluentui/react-list-preview'; +import { ListItemButton } from '@fluentui/react-list-preview'; +import { ButtonProps } from '@fluentui/react-components'; -export const Default = (props: Partial) => ; +export const Default = (props: ButtonProps) => ; From 00e777ffb30199408bbd78a5f739be79aa6c70cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 14:58:43 +0100 Subject: [PATCH 055/136] update stories to be memoized --- .../ListSelectionControlledBasic.stories.tsx | 37 +++++++++++-------- ...stSelectionControlledWithState.stories.tsx | 37 +++++++++++-------- .../ListSelectionUncontrolled.stories.tsx | 37 +++++++++++-------- 3 files changed, 63 insertions(+), 48 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx index 5545591161feaa..ac966f3ee1bfef 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx @@ -47,6 +47,24 @@ const useStyles = makeStyles({ }, }); +// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. +const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string }) => { + return ( + + + + ); +}); + export const ListSelectionControlledBasic = () => { const classes = useStyles(); const [currentIndex, setCurrentIndex] = React.useState(4); @@ -72,22 +90,9 @@ export const ListSelectionControlledBasic = () => { selectedItems={selectedItems} onSelectionChange={(_, data) => setSelectedItems(data.selectedItems)} > - {items.map(({ name, avatar }) => { - return ( - - - - ); - })} + {items.map(({ name, avatar }) => ( + + ))}
Selected people: {selectedItems.join(', ')}
diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx index 94af3c57f26304..4c95efff230062 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -47,6 +47,24 @@ const useStyles = makeStyles({ }, }); +// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. +const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string }) => { + return ( + + + + ); +}); + export const ListSelectionControlledWithState = () => { const classes = useStyles(); const [currentIndex, setCurrentIndex] = React.useState(4); @@ -85,22 +103,9 @@ export const ListSelectionControlledWithState = () => { selectedItems={selection.selectedItems} onSelectionChange={(_, data) => selection.setSelectedItems(data.selectedItems)} > - {items.map(({ name, avatar }) => { - return ( - - - - ); - })} + {items.map(({ name, avatar }) => ( + + ))}
Selected people: {selection.selectedItems.join(', ')}
diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index da022620fcba9d..4cd6d3b3861faf 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -47,6 +47,24 @@ const useStyles = makeStyles({ }, }); +// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. +const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string }) => { + return ( + + + + ); +}); + export const ListSelectionUncontrolled = () => { const classes = useStyles(); const [currentIndex, setCurrentIndex] = React.useState(4); @@ -70,22 +88,9 @@ export const ListSelectionUncontrolled = () => { defaultSelectedItems={defaultSelectedItems} onSelectionChange={(_, data) => setSelectedItems(data.selectedItems)} > - {items.map(({ name, avatar }) => { - return ( - - - - ); - })} + {items.map(({ name, avatar }) => ( + + ))}
Selected people: {selectedItems.join(', ')}
From cb1aae6ab99ccbb8bbb162c09dc525b1c274c4e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 15:10:48 +0100 Subject: [PATCH 056/136] selection stories --- .../List/ListSelectionControlledBasic.stories.tsx | 8 +++----- .../List/ListSelectionControlledWithState.stories.tsx | 9 ++++----- .../stories/List/ListSelectionUncontrolled.stories.tsx | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx index ac966f3ee1bfef..603e877efd559c 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx @@ -103,12 +103,10 @@ ListSelectionControlledBasic.parameters = { docs: { description: { story: [ - 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', + 'This example is similar to the previous one, but shows how to use the `selectedItems` and `onSelectionChange`', + 'props to control the selection state.', '', - 'You can pass `selectable` prop inside of the List component to get built-in selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state.', - '', - "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", - 'your list items and even store and retrieve them separately.', + 'This is a basic example how selection can be controlled with a simple array of selected values in a state.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx index 4c95efff230062..06fd6a3d932ca6 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -116,12 +116,11 @@ ListSelectionControlledWithState.parameters = { docs: { description: { story: [ - 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', + 'This example is an extension of the previous examle of controlled selection. ', + 'It shows how to use the `useListSelection` hook to control the selection state.', '', - 'You can pass `selectable` prop inside of the List component to get built-in selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state.', - '', - "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", - 'your list items and even store and retrieve them separately.', + 'The `useListSelection` hook is by no means required for the selection to work, but it provides a convenient ', + 'way to control the selection state by providing selection specific helper functions.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx index 4cd6d3b3861faf..3b0f530625a735 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx @@ -103,7 +103,7 @@ ListSelectionUncontrolled.parameters = { story: [ 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', '', - 'You can pass `selectable` prop inside of the List component to get built-in selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state.', + 'You can pass `selectable` prop inside of the List component to get support for selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state. While we are saving the results of `onSelectionChange`, it is purely for keeping track of the selection state and is not used to control the selection.', '', "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", 'your list items and even store and retrieve them separately.', From b73ef788ba277347a1735c60fffae6eeed7f3205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 27 Nov 2023 16:59:36 +0100 Subject: [PATCH 057/136] Update .github/CODEOWNERS Co-authored-by: Oleksandr Fediashov --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e579546c09e603..f8d13b6cc1b3e3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -244,7 +244,6 @@ packages/react-components/react-search-preview @microsoft/cxe-red packages/react-components/react-colorpicker-compat @microsoft/cxe-red @sopranopillow packages/react-components/react-nav-preview @microsoft/cxe-red @mltejera packages/react-components/react-motion-preview @microsoft/cxe-prg @marcosmoura -packages/react-components/react-list-preview @microsoft/teams-prg packages/react-components/react-message-bar @microsoft/teams-prg packages/react-components/react-timepicker-compat-preview @microsoft/teams-prg packages/react-components/react-rating-preview @microsoft/cxe-red @tomi-msft From ee81b17d4802a2f996db4920ce9263a722a850f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 28 Nov 2023 15:20:42 +0100 Subject: [PATCH 058/136] selectable determines mouse cursor instead of onClick --- .../src/components/ListItem/ListItem.types.ts | 2 +- .../src/components/ListItem/useListItem.tsx | 1 + .../ListItem/useListItemStyles.styles.ts | 2 +- .../useListItemButtonStyles.styles.ts | 18 +++++++++++++++--- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 364fd132498f98..a980fe2d602e72 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -15,4 +15,4 @@ export type ListItemProps = ComponentProps & { /** * State used in rendering ListItem */ -export type ListItemState = ComponentState & {}; +export type ListItemState = ComponentState & { selectable?: boolean }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 049b1d85cedb37..2033dd25113120 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -118,6 +118,7 @@ export const useListItem_unstable = ( }, root, checkmark, + selectable: isSelectionEnabled, }; return state; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 902a4601bde3ad..aa4ff5e7581dc8 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -51,7 +51,7 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState state.root.className = mergeClasses( listItemClassNames.root, rootBaseStyles, - state.root.onClick && styles.rootClickable, + state.selectable && styles.rootClickable, state.root.className, ); diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts index b45035107851f1..73f37273120075 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts @@ -1,24 +1,36 @@ import { ButtonState, useButtonStyles_unstable } from '@fluentui/react-button'; -import { makeResetStyles, mergeClasses, shorthands } from '@griffel/react'; +import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react'; /** * Styles for the root slot */ -const useStyles = makeResetStyles({ +const useRootBaseStyles = makeResetStyles({ display: 'block', width: '100%', textAlign: 'left', ...shorthands.padding(0), }); +const useStyles = makeStyles({ + root: { + '&:hover': { + color: 'unset', + }, + '&:hover:active': { + color: 'unset', + }, + }, +}); + /** * Apply styling to the ListItemButton slots based on the state */ export const useListItemButtonStyles_unstable = (state: ButtonState): ButtonState => { useButtonStyles_unstable(state); + const baseStyles = useRootBaseStyles(); const styles = useStyles(); - state.root.className = mergeClasses(state.root.className, styles); + state.root.className = mergeClasses(state.root.className, baseStyles, styles.root); return state; }; From e33c2f405f2c236a8f49736b17db199c93aa36ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 28 Nov 2023 15:43:55 +0100 Subject: [PATCH 059/136] PR feedback --- .../react-list-preview/package.json | 1 + .../src/components/List/useList.ts | 3 +-- .../src/components/List/useListContextValues.ts | 14 +++++--------- .../src/components/ListItem/useListItem.tsx | 10 +++++++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 9eb48a8a7c2695..b00ca7b7ffe777 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -42,6 +42,7 @@ "@fluentui/react-context-selector": "^9.1.42", "@fluentui/react-icons": "^2.0.217", "@fluentui/react-jsx-runtime": "^9.0.19", + "@fluentui/keyboard-keys": "^9.0.7", "@fluentui/react-tabster": "^9.14.6", "@fluentui/react-theme": "^9.1.16", "@fluentui/react-utilities": "^9.15.2", diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index c44468284a5e01..f6cd4e61a3d746 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -10,7 +10,6 @@ import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { ListProps, ListState } from './List.types'; import { useListSelection } from '../../hooks/useListSelection'; -const EMPTY_OBJECT = {}; const DEFAULT_ROOT_EL_TYPE = 'ul'; /** @@ -73,7 +72,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref ({ - focusableItems, - selection, - as, - }), - [focusableItems, selection, as], - ); + const listContext = { + focusableItems, + selection, + as, + }; return { listContext, diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 2033dd25113120..23587d81179bc8 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useFocusableGroup } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, slot, useEventCallback, useId } from '@fluentui/react-utilities'; +import { Space } from '@fluentui/keyboard-keys'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; import { Checkmark16Filled } from '@fluentui/react-icons'; -const EMPTY_OBJECT = {}; const DEFAULT_ROOT_EL_TYPE = 'li'; const listPropsForSelected = { @@ -27,6 +27,10 @@ const listPropsForNotSelected = { }; function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', renderedAs?: 'div' | 'li') { + if (process.env.NODE_ENV === 'production') { + return; + } + if (renderedAs === 'div' && parentRenderedAs !== 'div') { throw new Error('ListItem cannot be rendered as a div when its parent is not a div.'); } @@ -74,7 +78,7 @@ export const useListItem_unstable = ( return; } - if (e.key === ' ') { + if (e.key === Space) { e.preventDefault(); toggleItem?.(e, value); } @@ -96,7 +100,7 @@ export const useListItem_unstable = ( tabIndex: focusableItems ? 0 : undefined, role: 'listitem', id: String(value), - ...(isSelectionEnabled ? listItemProps : EMPTY_OBJECT), + ...(isSelectionEnabled && listItemProps), ...focusableGroupAttrs, ...props, onKeyDown: handleKeyDown, From d8c9c25e50887cc287f0574a8b39b6cf838854f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 29 Nov 2023 17:29:37 +0100 Subject: [PATCH 060/136] pr feedback from Shift, round 1 --- .../src/components/List/List.test.tsx | 2 +- .../src/components/List/List.tsx | 4 ++-- .../src/components/List/List.types.ts | 4 ++-- .../src/components/List/useList.ts | 15 +++++-------- .../components/List/useListStyles.styles.ts | 8 +++---- .../useListItemButtonStyles.styles.ts | 22 +++++++------------ .../react-list-preview/src/hooks/types.ts | 2 -- .../src/hooks/useListSelection.tsx | 15 ++----------- .../stories/List/ListGrid.stories.tsx | 2 +- .../stories/List/ListHorizontal.stories.tsx | 2 +- .../List/ListMultipleActions.stories.tsx | 2 +- tsconfig.base.all.json | 10 ++++----- 12 files changed, 32 insertions(+), 56 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 1a8874c8465b67..e72298c81534c9 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -10,7 +10,7 @@ describe('List', () => { displayName: 'List', testOptions: { 'consistent-callback-args': { - // onSelectionChange has an eventArguent which is React.SyntheticEvent. This throws an error during testing + // onSelectionChange has an eventArgument which is React.SyntheticEvent. This throws an error during testing ignoreProps: ['onSelectionChange'], }, }, diff --git a/packages/react-components/react-list-preview/src/components/List/List.tsx b/packages/react-components/react-list-preview/src/components/List/List.tsx index 19e1bf327ece00..c3add40964ea9f 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.tsx @@ -9,12 +9,12 @@ import { useListContextValues_unstable } from './useListContextValues'; export const List: ForwardRefComponent = React.forwardRef((props, ref) => { const state = useList_unstable(props, ref); - const listContext = useListContextValues_unstable(state); + const contextValues = useListContextValues_unstable(state); useListStyles_unstable(state); useCustomStyleHook_unstable('useListStyles_unstable')(state); - return renderList_unstable(state, listContext); + return renderList_unstable(state, contextValues); }); List.displayName = 'List'; diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 9e566fa8529f97..a647e217010942 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,8 +1,8 @@ import * as React from 'react'; import type { ComponentProps, ComponentState, Slot, SelectionMode, SelectionItemId } from '@fluentui/react-utilities'; -import { ListSelectionState } from '../../hooks/types'; +import type { ListSelectionState } from '../../hooks/types'; -export type ListLayout = 'horizontal' | 'vertical' | 'grid'; +type ListLayout = 'horizontal' | 'vertical' | 'grid'; export type ListSlots = { root: NonNullable>; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index f6cd4e61a3d746..9d441a2661dc47 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -44,7 +44,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref, data: OnSelectionChangeData) => { + const onChange = useEventCallback((e: React.SyntheticEvent, data: OnSelectionChangeData) => { const selectedItemsAsArray = Array.from(data.selectedItems); setSelectionState(selectedItemsAsArray); onSelectionChange?.(e, { selectedItems: selectedItemsAsArray }); @@ -57,14 +57,6 @@ export const useList_unstable = (props: ListProps, ref: React.Ref ({ - role: 'listbox', - 'aria-multiselectable': selectionMode === 'multiselect' ? true : undefined, - }), - [selectionMode], - ); - return { components: { root: DEFAULT_ROOT_EL_TYPE, @@ -72,7 +64,10 @@ export const useList_unstable = (props: ListProps, ref: React.Ref = { root: 'fui-List', }; const useRootBaseStyles = makeResetStyles({ - ...shorthands.padding(0), - ...shorthands.margin(0), + padding: 0, + margin: 0, textIndent: 0, listStyleType: 'none', }); diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts index 73f37273120075..06e9ad87d3fa7f 100644 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts @@ -1,5 +1,5 @@ import { ButtonState, useButtonStyles_unstable } from '@fluentui/react-button'; -import { makeResetStyles, makeStyles, mergeClasses, shorthands } from '@griffel/react'; +import { makeResetStyles, mergeClasses } from '@griffel/react'; /** * Styles for the root slot @@ -8,17 +8,12 @@ const useRootBaseStyles = makeResetStyles({ display: 'block', width: '100%', textAlign: 'left', - ...shorthands.padding(0), -}); - -const useStyles = makeStyles({ - root: { - '&:hover': { - color: 'unset', - }, - '&:hover:active': { - color: 'unset', - }, + padding: 0, + '&:hover': { + color: 'unset', + }, + '&:hover:active': { + color: 'unset', }, }); @@ -29,8 +24,7 @@ export const useListItemButtonStyles_unstable = (state: ButtonState): ButtonStat useButtonStyles_unstable(state); const baseStyles = useRootBaseStyles(); - const styles = useStyles(); - state.root.className = mergeClasses(state.root.className, baseStyles, styles.root); + state.root.className = mergeClasses(state.root.className, baseStyles); return state; }; diff --git a/packages/react-components/react-list-preview/src/hooks/types.ts b/packages/react-components/react-list-preview/src/hooks/types.ts index 0eaf9e2ea000b0..da45dd1723fc72 100644 --- a/packages/react-components/react-list-preview/src/hooks/types.ts +++ b/packages/react-components/react-list-preview/src/hooks/types.ts @@ -11,5 +11,3 @@ export type ListSelectionState = { setSelectedItems: React.Dispatch>>; selectedItems: SelectionItemId[]; }; - -export type UseListSelectionOptions = {}; // multiselect etc diff --git a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx index 00745456ca8a2c..776b485c0be4cf 100644 --- a/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx +++ b/packages/react-components/react-list-preview/src/hooks/useListSelection.tsx @@ -1,24 +1,13 @@ import { SelectionHookParams, useControllableState, useEventCallback, useSelection } from '@fluentui/react-utilities'; import * as React from 'react'; -import { ListSelectionState } from './types'; - -export const defaultListSelectionState: ListSelectionState = { - isSelected: () => false, - toggleItem: () => undefined, - selectItem: () => undefined, - deselectItem: () => undefined, - clearSelection: () => undefined, - toggleAllItems: () => undefined, - setSelectedItems: () => undefined, - selectedItems: [], -}; +import type { ListSelectionState } from './types'; export function useListSelection(options: SelectionHookParams = { selectionMode: 'multiselect' }): ListSelectionState { const { selectionMode, defaultSelectedItems, onSelectionChange } = options; const [selectedItems, setSelectedItems] = useControllableState({ state: options.selectedItems, - defaultState: defaultSelectedItems || [], + defaultState: defaultSelectedItems, initialState: [], }); diff --git a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx index 28b92a6d0ca957..ea300eeb3b03f7 100644 --- a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx @@ -11,7 +11,7 @@ const useStyles = makeStyles({ export const ListGrid = (props: Partial) => { const classes = useStyles(); return ( - + alert('Asia')}>Asia diff --git a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx index 1d1d7b34f7b257..fa81e92e7a3628 100644 --- a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx @@ -11,7 +11,7 @@ const useStyles = makeStyles({ export const ListHorizontal = () => { const classes = useStyles(); return ( - + Asia Africa Europe diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index a680229e634e2b..ead6de1823a57a 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -153,7 +153,7 @@ export const ListMultipleActions = (props: Partial) => { return ( Date: Wed, 29 Nov 2023 17:47:53 +0100 Subject: [PATCH 061/136] pr feedback --- .../src/components/ListItem/useListItem.tsx | 27 ++++--------------- .../List/ListMultipleActions.stories.tsx | 2 +- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 23587d81179bc8..0b0f8d5957728f 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -8,24 +8,6 @@ import { Checkmark16Filled } from '@fluentui/react-icons'; const DEFAULT_ROOT_EL_TYPE = 'li'; -const listPropsForSelected = { - tabIndex: 0, - role: 'option', - 'aria-selected': true, - checkmark: { - children: , - }, -}; - -const listPropsForNotSelected = { - tabIndex: 0, - role: 'option', - 'aria-selected': false, - checkmark: { - children: null, - }, -}; - function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', renderedAs?: 'div' | 'li') { if (process.env.NODE_ENV === 'production') { return; @@ -65,8 +47,6 @@ export const useListItem_unstable = ( validateProperElementTypes(parentRenderedAs, renderedAs); - const listItemProps = isSelected ? listPropsForSelected : listPropsForNotSelected; - const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); const handleKeyDown: React.KeyboardEventHandler = useEventCallback(e => { @@ -97,10 +77,13 @@ export const useListItem_unstable = ( const root = slot.always( getIntrinsicElementProps(DEFAULT_ROOT_EL_TYPE, { ref: ref as React.Ref, - tabIndex: focusableItems ? 0 : undefined, + tabIndex: focusableItems || isSelectionEnabled ? 0 : undefined, role: 'listitem', id: String(value), - ...(isSelectionEnabled && listItemProps), + ...(isSelectionEnabled && { + role: 'option', + 'aria-selected': isSelected, + }), ...focusableGroupAttrs, ...props, onKeyDown: handleKeyDown, diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index ead6de1823a57a..e944549a6e19bf 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -157,7 +157,7 @@ export const ListMultipleActions = (props: Partial) => { className={classes.list} focusableItems selectable - onSelectionChange={(e, data) => setSelectedItems(Array.from(data.selectedItems))} + onSelectionChange={(e, data) => setSelectedItems(data.selectedItems)} selectionMode="multiselect" > From 0825b5fbb3e050b702a20ee727492d555b2de7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 29 Nov 2023 17:53:28 +0100 Subject: [PATCH 062/136] api --- .../react-list-preview/etc/react-list-preview.api.md | 7 +++---- packages/react-components/react-list-preview/src/index.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 4e022c76ccfa9b..fd41ceab51f1e7 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -45,10 +45,9 @@ export type ListItemSlots = { }; // @public -export type ListItemState = ComponentState & {}; - -// @public (undocumented) -export type ListLayout = 'horizontal' | 'vertical' | 'grid'; +export type ListItemState = ComponentState & { + selectable?: boolean; +}; // @public export type ListProps = ComponentProps & { diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index b6927b3c0a40be..ad430893364c47 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -1,6 +1,6 @@ export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List'; -export type { ListProps, ListSlots, ListState, ListLayout } from './List'; +export type { ListProps, ListSlots, ListState } from './List'; export { ListItem, listItemClassNames, From c49ca57b223e69b12dce2fda594123b9a6b0320a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 6 Feb 2024 11:33:22 +0100 Subject: [PATCH 063/136] big progress on complex lists --- .../src/components/List/useList.ts | 6 +- .../src/components/ListItem/ListItem.types.ts | 5 +- .../src/components/ListItem/useListItem.tsx | 139 ++++++++++-- .../ListItem/useListItemStyles.styles.ts | 10 +- .../List/ListActiveElement.stories.tsx | 128 +++++++++++ .../List/ListMultipleActions.stories.tsx | 80 ++++--- .../List/ListMultipleActionsGrid.stories.tsx | 214 ++++++++++++++++++ ...tipleActions_different_primary.stories.tsx | 206 +++++++++++++++++ .../MultipleActions_no_primary.stories.tsx | 209 +++++++++++++++++ ...ctions_no_primary_no_selection.stories.tsx | 189 ++++++++++++++++ ...tipleActions_primary_selection.stories.tsx | 210 +++++++++++++++++ ...n.stories.tsx => SingleAction.stories.tsx} | 4 +- ...tsx => SingleAction_selection.stories.tsx} | 6 +- .../stories/List/Untitled-2 | 0 .../stories/List/index.stories.tsx | 11 +- 15 files changed, 1346 insertions(+), 71 deletions(-) create mode 100644 packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary_no_selection.stories.tsx create mode 100644 packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx rename packages/react-components/react-list-preview/stories/List/{ListWithAction.stories.tsx => SingleAction.stories.tsx} (96%) rename packages/react-components/react-list-preview/stories/List/{ListSelectionUncontrolled.stories.tsx => SingleAction_selection.stories.tsx} (92%) create mode 100644 packages/react-components/react-list-preview/stories/List/Untitled-2 diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 9d441a2661dc47..542b6429806d4a 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -6,7 +6,7 @@ import { useControllableState, useEventCallback, } from '@fluentui/react-utilities'; -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; +import { useArrowNavigationGroup, useFocusableGroup } from '@fluentui/react-tabster'; import { ListProps, ListState } from './List.types'; import { useListSelection } from '../../hooks/useListSelection'; @@ -34,9 +34,10 @@ export const useList_unstable = (props: ListProps, ref: React.Ref>; - checkmark?: Slot<'div'>; + checkmark?: Slot; }; /** @@ -15,4 +16,4 @@ export type ListItemProps = ComponentProps & { /** * State used in rendering ListItem */ -export type ListItemState = ComponentState & { selectable?: boolean }; +export type ListItemState = ComponentState & { selectable?: boolean; hasCustomOnClick?: boolean }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 0b0f8d5957728f..1d8c6e5c2bb09f 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -1,10 +1,17 @@ import * as React from 'react'; -import { useFocusableGroup } from '@fluentui/react-tabster'; -import { getIntrinsicElementProps, slot, useEventCallback, useId } from '@fluentui/react-utilities'; -import { Space } from '@fluentui/keyboard-keys'; +import { + TabsterDOMAttribute, + useArrowNavigationGroup, + useFocusableGroup, + useFocusFinders, + useMergedTabsterAttributes_unstable, +} from '@fluentui/react-tabster'; +import { getIntrinsicElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; -import { Checkmark16Filled } from '@fluentui/react-icons'; +import { makeResetStyles } from '@griffel/react'; +import { Enter, Escape, keyCodes, Space } from '@fluentui/keyboard-keys'; +import { Checkbox, CheckboxOnChangeData } from '@fluentui/react-checkbox'; const DEFAULT_ROOT_EL_TYPE = 'li'; @@ -21,6 +28,20 @@ function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', rend } } +function validateNavigableWhenOnClickPresent(navigable: boolean, onClick?: React.MouseEventHandler) { + if (process.env.NODE_ENV === 'production') { + return; + } + + if (onClick && !navigable) { + throw new Error('ListItem must be navigable when onClick is present. Set navigable={true} on the List.'); + } +} + +const useIndicatorStyle = makeResetStyles({ + margin: 0, +}); + /** * Create the state required to render ListItem. * @@ -45,38 +66,111 @@ export const useListItem_unstable = ( const parentRenderedAs = useListContext_unstable(ctx => ctx.as); const renderedAs = props.as || DEFAULT_ROOT_EL_TYPE; + const rootRef = React.useRef(null); + validateProperElementTypes(parentRenderedAs, renderedAs); - const focusableGroupAttrs = useFocusableGroup({ tabBehavior: 'limited-trap-focus' }); + validateNavigableWhenOnClickPresent(focusableItems, onClick); + + const { findFirstFocusable, findPrevFocusable } = useFocusFinders(); + + const baseIndicatorStyles = useIndicatorStyle(); + + const focusableGroupAttrs = useFocusableGroup({ ignoreDefaultKeydown: { Enter: true } }); + + const handleClick: React.MouseEventHandler = useEventCallback(e => { + onClick?.(e); + + if (!isSelectionEnabled || e.defaultPrevented) { + return; + } + + toggleItem?.(e, value); + }); const handleKeyDown: React.KeyboardEventHandler = useEventCallback(e => { onKeyDown?.(e); - // Compare targets to make sure this only triggers when the event is fired on the list item - // and not on a button inside - if (!isSelectionEnabled || e.defaultPrevented || e.target !== e.currentTarget) { + // Return early if prevented default + if (e.defaultPrevented) { + return; + } + + // If the list items themselves are focusable and the event is fired from an element inside the list item + if (focusableItems && e.target !== e.currentTarget) { + // If it's one of the Arrows defined, jump out of the list item to focus on the ListItem itself + // The ArrowLeft will only trigger if the target element is the leftmost, otherwise the + // arrowNavigationAttributes handles it and prevents it from bubbling here. + if (['ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(e.key)) { + // Prevents scrolling for ArrowUp/ArowDown + e.preventDefault(); + e.target.dispatchEvent( + new KeyboardEvent('keydown', { + key: Escape, + keyCode: keyCodes.Escape, + }), + ); + } + } + + // Now return early if the event is not fired from the list item itself + // as the following code handles the events specifically coming from the list item + if (e.target !== e.currentTarget) { return; } - if (e.key === Space) { + // Handle selection for enter and space + if ((isSelectionEnabled || typeof props.onClick === 'function') && [Space, Enter].includes(e.key)) { e.preventDefault(); - toggleItem?.(e, value); + e.currentTarget.click(); + } + + // Handle entering the list item when user presses the ArrowRight + if (e.key === 'ArrowRight') { + const el = findFirstFocusable(e.currentTarget); + if (el) { + el.focus(); + } } }); - const handleClick: React.MouseEventHandler = useEventCallback(e => { - onClick?.(e); + const onCheckboxChange = useEventCallback((ev: React.ChangeEvent, data: CheckboxOnChangeData) => { + props.checkmark?.onChange?.(ev, data); - if (!isSelectionEnabled || e.defaultPrevented) { + if (!isSelectionEnabled || ev.defaultPrevented) { return; } - toggleItem?.(e, value); + toggleItem?.(ev, value); + }); + const onCheckboxClick = useEventCallback((ev: React.MouseEvent) => { + ev.stopPropagation(); + // toggleItem?.(ev, value); }); + // const handleKeyUp: React.KeyboardEventHandler = useEventCallback(e => { + // onKeyUp?.(e); + + // if (!isSelectionEnabled || e.defaultPrevented || e.target !== e.currentTarget) { + // // In this case prevent default prevents the useARIAButtonProps from toggling onClick + // e.preventDefault(); + // return; + // } + // }); + const arrowNavigationAttributes = useArrowNavigationGroup({ + axis: 'horizontal', + // tabbable: false, + // memorizeCurrent: true, + }); + + const tabsterAttributes = useMergedTabsterAttributes_unstable( + focusableItems ? arrowNavigationAttributes : ({} as TabsterDOMAttribute), + focusableGroupAttrs, + ); + const root = slot.always( getIntrinsicElementProps(DEFAULT_ROOT_EL_TYPE, { - ref: ref as React.Ref, + ref: useMergedRefs(rootRef, ref) as React.Ref, tabIndex: focusableItems || isSelectionEnabled ? 0 : undefined, role: 'listitem', id: String(value), @@ -84,7 +178,7 @@ export const useListItem_unstable = ( role: 'option', 'aria-selected': isSelected, }), - ...focusableGroupAttrs, + ...tabsterAttributes, ...props, onKeyDown: handleKeyDown, onClick: handleClick, @@ -93,15 +187,22 @@ export const useListItem_unstable = ( ); const checkmark = slot.optional(props.checkmark, { - defaultProps: { children: isSelected ? : null }, + defaultProps: { + // 'aria-hidden': true, + checked: isSelected, + onChange: onCheckboxChange, + onClick: onCheckboxClick, + // tabIndex: -1, + indicator: { className: baseIndicatorStyles }, + }, renderByDefault: isSelectionEnabled, - elementType: 'div', + elementType: Checkbox, }); const state: ListItemState = { components: { root: DEFAULT_ROOT_EL_TYPE, - checkmark: 'div', + checkmark: Checkbox, }, root, checkmark, diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index aa4ff5e7581dc8..a596ac97f87f6a 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -25,9 +25,9 @@ const useRootBaseStyles = makeResetStyles({ const useCheckmarkBaseStyles = makeResetStyles({ alignSelf: 'center', - marginRight: tokens.spacingHorizontalS, - width: tokens.spacingHorizontalL, - height: tokens.spacingVerticalL, + // marginRight: tokens.spacingHorizontalS, + // width: tokens.spacingHorizontalL, + // height: tokens.spacingVerticalL, }); /** @@ -51,7 +51,9 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState state.root.className = mergeClasses( listItemClassNames.root, rootBaseStyles, - state.selectable && styles.rootClickable, + // add the clickable root only if we know the items are selectable and there is no custom onClick + // because the custom onClick could be overriding the selection behavior. + state.selectable && !state.hasCustomOnClick && styles.rootClickable, state.root.className, ); diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx new file mode 100644 index 00000000000000..a72146c064cd50 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -0,0 +1,128 @@ +import { + Button, + makeStyles, + Persona, + shorthands, + mergeClasses, + Text, + tokens, + SelectionItemId, +} from '@fluentui/react-components'; +import { List, ListItem, useListSelection } from '@fluentui/react-list-preview'; + +import * as React from 'react'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', + 'Cythia Ignacio', + 'Gia Laura', + 'Dewayne Oda', + 'Lang Yeldell', + 'Kathlyn Brewer', + 'Nia Woodworth', +]; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const items: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + display: 'grid', + gridTemplateColumns: '200px 1fr', + columnGap: '16px', + }, + button: { + ...shorthands.padding(0), + }, + item: { + cursor: 'pointer', + ...shorthands.padding('2px', '6px'), + }, + itemSelected: { + backgroundColor: tokens.colorSubtleBackgroundSelected, + }, +}); + +// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. +const MyListItem = React.memo(({ name, avatar, ...rest }: { name: string; avatar: string; className?: string }) => { + return ( + + + + ); +}); + +export const ListActiveElement = () => { + const classes = useStyles(); + + const [selectedItems, setSelectedItems] = React.useState([]); + + return ( +
+ { + console.log(e); + setSelectedItems(data.selectedItems.slice(-1)); + }} + > + {items.map(({ name, avatar }) => ( + <> + + + ))} + +
+ + {selectedItems[0]} + + {selectedItems.length ? {selectedItems[0]} is a great person! : null} +
+
+ ); +}; + +ListActiveElement.parameters = { + docs: { + description: { + story: [ + 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', + '', + 'You can pass `selectable` prop inside of the List component to get support for selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state. While we are saving the results of `onSelectionChange`, it is purely for keeping track of the selection state and is not used to control the selection.', + '', + "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", + 'your list items and even store and retrieve them separately.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx index e944549a6e19bf..580ff466ac83e0 100644 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActions.stories.tsx @@ -28,12 +28,16 @@ const resolveAsset = (asset: string) => { const useStyles = makeStyles({ list: { - 'grid-template-columns': 'repeat(auto-fill, minmax(300px, 1fr))', + // 'grid-template-columns': 'repeat(auto-fill, minmax(300px, 1fr))', + display: 'flex', + width: '300px', + flexDirection: 'column', ...shorthands.gap('16px'), }, listItem: { position: 'relative', + flexGrow: '1', }, caption: { @@ -58,33 +62,49 @@ const useStyles = makeStyles({ display: 'flex', ...shorthands.gap('8px'), }, - checkmark: { - position: 'absolute', - left: '10px', - top: '10px', - zIndex: 1, - }, }); const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { const styles = useStyles(); + const { value } = props; return ( { + if (e.target.tagName === 'BUTTON') { + return; + } + alert('main action trigerred'); + }} + // aria-checked={props.selected ? 'true' : 'false'} + aria-label={'iOS App Prototype' + props.value} + + // aria-owns={owns} > - + } + logo={ + Figma app logo + } > - Presentation Preview + Presentation Preview iOS App Prototype} description={You created 53m ago} action={ @@ -149,26 +169,17 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = export const ListMultipleActions = (props: Partial) => { const classes = useStyles(); - const [selectedItems, setSelectedItems] = React.useState>([]); - return ( - setSelectedItems(data.selectedItems)} - selectionMode="multiselect" - > - - - - - - - - - + + + + + + + + + + ); }; @@ -176,12 +187,7 @@ export const ListMultipleActions = (props: Partial) => { ListMultipleActions.parameters = { docs: { description: { - story: [ - 'List can have a grid layout. What this means is that the wrapper will have `display: grid` applied to it. Also the arrow navigation will work horizontaly and vertically.', - "\nBy default, the grid doesn't have any columns defined. It is up to the user to define the columns using CSS. Please refer to the example below.", - '', - 'You can also select the items with a click or using a Spacebar key.', - ].join('\n'), + story: [].join('\n'), }, }, }; diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx new file mode 100644 index 00000000000000..674e15bafe4c36 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx @@ -0,0 +1,214 @@ +import { + Button, + Caption1, + Card, + CardHeader, + CardPreview, + CardProps, + makeResetStyles, + makeStyles, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + mergeClasses, + shorthands, + Text, + tokens, +} from '@fluentui/react-components'; +import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; +import * as React from 'react'; + +const useListItemRootStyles = makeResetStyles({ + position: 'relative', + flexGrow: '1', + gap: '8px', + border: '1px solid grey', + alignItems: 'center', + borderRadius: '8px', + gridTemplate: `"preview preview preview" auto + "header action secondary_action" auto / 1fr auto auto + `, +}); + +const useStyles = makeStyles({ + list: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap('16px'), + maxWidth: '300px', + }, + listItem: { display: 'grid', ...shorthands.padding('8px') }, + + caption: { + color: tokens.colorNeutralForeground3, + }, + + image: { + maxWidth: '100%', + ...shorthands.borderRadius('5px'), + }, + + grayBackground: { + backgroundColor: tokens.colorNeutralBackground3, + }, + + logoBadge: { + ...shorthands.padding('5px'), + ...shorthands.borderRadius(tokens.borderRadiusSmall), + backgroundColor: '#FFF', + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', + }, + actionsWrapper: { + display: 'flex', + ...shorthands.gap('8px'), + }, + checkmark: { + position: 'absolute', + left: '10px', + top: '10px', + zIndex: 1, + }, +}); + +const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { + const listItemStyles = useListItemRootStyles(); + const styles = useStyles(); + const { value } = props; + + return ( + { + // if (e.target !== e.currentTarget) { + // return; + // } + + // prevent selection + e.preventDefault(); + + alert('main action trigerred'); + }} + + // aria-owns={owns} + > +
+ Presentation Preview +
+
+ + {props.value} + + You created 53m ago +
+
+ +
+
+ + + +
+
+ ); +}; + +export const ListMultipleActionsGrid = (props: Partial) => { + const classes = useStyles(); + + const [selectedItems, setSelectedItems] = React.useState>([]); + + return ( + setSelectedItems(data.selectedItems)} + selectionMode="multiselect" + aria-setsize={9} + > + + + + + + + + + + + ); +}; + +ListMultipleActionsGrid.parameters = { + docs: { + description: { + story: ['You can select the items with a click or using a Spacebar key.'].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx new file mode 100644 index 00000000000000..4631ecb74a5c63 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx @@ -0,0 +1,206 @@ +import { + Button, + Caption1, + Card, + CardHeader, + CardPreview, + CardProps, + makeResetStyles, + makeStyles, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + mergeClasses, + shorthands, + Text, + tokens, +} from '@fluentui/react-components'; +import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; +import * as React from 'react'; + +const useListItemRootStyles = makeResetStyles({ + position: 'relative', + flexGrow: '1', + gap: '8px', + border: '1px solid grey', + alignItems: 'center', + borderRadius: '8px', + gridTemplate: `"preview preview preview" auto + "header action secondary_action" auto / 1fr auto auto + `, +}); + +const useStyles = makeStyles({ + list: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap('16px'), + maxWidth: '300px', + }, + listItem: { display: 'grid', ...shorthands.padding('8px') }, + + caption: { + color: tokens.colorNeutralForeground3, + }, + + image: { + maxWidth: '100%', + ...shorthands.borderRadius('5px'), + }, + + grayBackground: { + backgroundColor: tokens.colorNeutralBackground3, + }, + + logoBadge: { + ...shorthands.padding('5px'), + ...shorthands.borderRadius(tokens.borderRadiusSmall), + backgroundColor: '#FFF', + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', + }, + actionsWrapper: { + display: 'flex', + ...shorthands.gap('8px'), + }, + checkmark: { + position: 'absolute', + left: '10px', + top: '10px', + zIndex: 1, + }, +}); + +const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { + const listItemStyles = useListItemRootStyles(); + const styles = useStyles(); + const { value } = props; + + return ( + { + // Prevent the default behavior - toggling the selection + e.preventDefault(); + alert('Main action triggered!'); + }} + > +
+ Presentation Preview +
+
+ + {props.value} + + You created 53m ago +
+
+ +
+
+ + + +
+
+ ); +}; + +export const MultipleActionsDifferentPrimary = (props: Partial) => { + const classes = useStyles(); + + const [selectedItems, setSelectedItems] = React.useState>([]); + + return ( + setSelectedItems(data.selectedItems)} + selectionMode="multiselect" + aria-setsize={9} + > + + + + + + + + + + + ); +}; + +MultipleActionsDifferentPrimary.parameters = { + docs: { + description: { + story: [ + 'Item with multiple actions. It has a default action and selection, but the default action on the list item is different than selection. For selection, the checkbox needs to be toggled', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx new file mode 100644 index 00000000000000..b014518b59fbdf --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx @@ -0,0 +1,209 @@ +import { + Button, + Caption1, + CardProps, + makeResetStyles, + makeStyles, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + mergeClasses, + shorthands, + Text, + tokens, +} from '@fluentui/react-components'; +import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; +import * as React from 'react'; + +const useListItemRootStyles = makeResetStyles({ + position: 'relative', + flexGrow: '1', + gap: '8px', + border: '1px solid grey', + alignItems: 'center', + borderRadius: '8px', + gridTemplate: `"preview preview preview" auto + "header action secondary_action" auto / 1fr auto auto + `, +}); + +const useStyles = makeStyles({ + list: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap('16px'), + maxWidth: '300px', + }, + listItem: { display: 'grid', ...shorthands.padding('8px'), cursor: 'default' }, + + caption: { + color: tokens.colorNeutralForeground3, + }, + + image: { + maxWidth: '100%', + ...shorthands.borderRadius('5px'), + }, + + grayBackground: { + backgroundColor: tokens.colorNeutralBackground3, + }, + + logoBadge: { + ...shorthands.padding('5px'), + ...shorthands.borderRadius(tokens.borderRadiusSmall), + backgroundColor: '#FFF', + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', + }, + actionsWrapper: { + display: 'flex', + ...shorthands.gap('8px'), + }, + checkmark: { + position: 'absolute', + left: '10px', + top: '10px', + zIndex: 1, + }, +}); + +const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { + const listItemStyles = useListItemRootStyles(); + const styles = useStyles(); + const { value } = props; + + return ( + { + e.preventDefault(); + }} + > +
+ Presentation Preview +
+
+ + {props.value} + + You created 53m ago +
+
+ +
+
+ + + +
+
+ ); +}; + +export const MultipleActionsNoPrimary = (props: Partial) => { + const classes = useStyles(); + + const [selectedItems, setSelectedItems] = React.useState>([]); + + return ( + setSelectedItems(data.selectedItems)} + selectionMode="multiselect" + aria-setsize={9} + > + + + + + + + + + + + ); +}; + +MultipleActionsNoPrimary.parameters = { + docs: { + description: { + story: [ + "Base item with multiple actions. Doesn't have a primary action on the list item, but supports selection by checking the checkbox.", + '', + 'The roles in this example are `list` and `listitem`, inherited from the ul/li tags used. List items themselves are focusable.', + '', + 'When the selection is enabled, the list item default click behavior is to toggle that selection. To remove ', + 'this behavior, we need to add a click handler to the list item that prevents the default behavior. ', + '', + 'Selection by clicking the checkbox still works and is properly narrated by screen readers.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary_no_selection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary_no_selection.stories.tsx new file mode 100644 index 00000000000000..ab6cfc86c0fd05 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary_no_selection.stories.tsx @@ -0,0 +1,189 @@ +import { + Button, + Caption1, + Card, + CardHeader, + CardPreview, + CardProps, + makeResetStyles, + makeStyles, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + mergeClasses, + shorthands, + Text, + tokens, +} from '@fluentui/react-components'; +import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; +import * as React from 'react'; + +const useListItemRootStyles = makeResetStyles({ + position: 'relative', + flexGrow: '1', + gap: '8px', + border: '1px solid grey', + alignItems: 'center', + borderRadius: '8px', + gridTemplate: `"preview preview preview" auto + "header action secondary_action" auto / 1fr auto auto + `, +}); + +const useStyles = makeStyles({ + list: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap('16px'), + maxWidth: '300px', + }, + listItem: { display: 'grid', ...shorthands.padding('8px') }, + + caption: { + color: tokens.colorNeutralForeground3, + }, + + image: { + maxWidth: '100%', + ...shorthands.borderRadius('5px'), + }, + + grayBackground: { + backgroundColor: tokens.colorNeutralBackground3, + }, + + logoBadge: { + ...shorthands.padding('5px'), + ...shorthands.borderRadius(tokens.borderRadiusSmall), + backgroundColor: '#FFF', + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', + }, + actionsWrapper: { + display: 'flex', + ...shorthands.gap('8px'), + }, + checkmark: { + position: 'absolute', + left: '10px', + top: '10px', + zIndex: 1, + }, +}); + +const CardExample = (props: CardProps & { value: string }) => { + const listItemStyles = useListItemRootStyles(); + const styles = useStyles(); + const { value } = props; + + return ( + +
+ Presentation Preview +
+
+ + {props.value} + + You created 53m ago +
+
+ +
+
+ + + +
+
+ ); +}; + +export const MultipleActionsNoPrimaryNoSelection = (props: Partial) => { + const classes = useStyles(); + + return ( + + + + + + + + + + + + ); +}; + +MultipleActionsNoPrimaryNoSelection.parameters = { + docs: { + description: { + story: [ + "Base item with multiple actions. Doesn't have a primary action on the list item, doesn't have selection.", + '', + 'The roles in this example are `list` and `listitem`, inherited from the ul/li tags used. List items themselves are focusable.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx new file mode 100644 index 00000000000000..bdba115fcff955 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx @@ -0,0 +1,210 @@ +import { + Button, + Caption1, + Card, + CardHeader, + CardPreview, + CardProps, + makeResetStyles, + makeStyles, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + mergeClasses, + shorthands, + Text, + tokens, +} from '@fluentui/react-components'; +import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; +import * as React from 'react'; + +const useListItemRootStyles = makeResetStyles({ + position: 'relative', + flexGrow: '1', + gap: '8px', + border: '1px solid grey', + alignItems: 'center', + borderRadius: '8px', + gridTemplate: `"preview preview preview" auto + "header action secondary_action" auto / 1fr auto auto + `, +}); + +const useStyles = makeStyles({ + list: { + display: 'flex', + flexDirection: 'column', + ...shorthands.gap('16px'), + maxWidth: '300px', + }, + listItem: { display: 'grid', ...shorthands.padding('8px') }, + + caption: { + color: tokens.colorNeutralForeground3, + }, + + image: { + maxWidth: '100%', + ...shorthands.borderRadius('5px'), + }, + + grayBackground: { + backgroundColor: tokens.colorNeutralBackground3, + }, + + logoBadge: { + ...shorthands.padding('5px'), + ...shorthands.borderRadius(tokens.borderRadiusSmall), + backgroundColor: '#FFF', + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', + }, + actionsWrapper: { + display: 'flex', + ...shorthands.gap('8px'), + }, + checkmark: { + position: 'absolute', + left: '10px', + top: '10px', + zIndex: 1, + }, +}); + +const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { + const listItemStyles = useListItemRootStyles(); + const styles = useStyles(); + const { value } = props; + + return ( + +
+ Presentation Preview +
+
+ + {props.value} + + You created 53m ago +
+
+ +
+
+ + + +
+
+ ); +}; + +export const MultipleActionsPrimarySelection = (props: Partial) => { + const classes = useStyles(); + + const [selectedItems, setSelectedItems] = React.useState>([]); + + return ( + setSelectedItems(data.selectedItems)} + selectionMode="multiselect" + aria-setsize={9} + > + + + + + + + + + + + ); +}; + +MultipleActionsPrimarySelection.parameters = { + docs: { + description: { + story: [ + 'Item with multiple actions. It has a primary action on the list item and the action is selection.', + '', + 'Because the selection is the action on the item, to properly narrate the state of selection', + 'we are using the role grid / row / gridcell here to properly announce when the selection on the', + 'item is toggled.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx similarity index 96% rename from packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx rename to packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index a0eac62b8da44e..a47cb5396dbcf9 100644 --- a/packages/react-components/react-list-preview/stories/List/ListWithAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -30,7 +30,7 @@ const useStyles = makeStyles({ }, }); -export const ListWithAction = () => { +export const SingleAction = () => { const classes = useStyles(); return (
@@ -55,7 +55,7 @@ export const ListWithAction = () => {
); }; -ListWithAction.parameters = { +SingleAction.parameters = { docs: { description: { story: [ diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction_selection.stories.tsx similarity index 92% rename from packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx rename to packages/react-components/react-list-preview/stories/List/SingleAction_selection.stories.tsx index 3b0f530625a735..11180f89a33c6d 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionUncontrolled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction_selection.stories.tsx @@ -65,7 +65,7 @@ const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string ); }); -export const ListSelectionUncontrolled = () => { +export const SingleActionSelection = () => { const classes = useStyles(); const [currentIndex, setCurrentIndex] = React.useState(4); @@ -97,7 +97,7 @@ export const ListSelectionUncontrolled = () => { ); }; -ListSelectionUncontrolled.parameters = { +SingleActionSelection.parameters = { docs: { description: { story: [ @@ -107,6 +107,8 @@ ListSelectionUncontrolled.parameters = { '', "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", 'your list items and even store and retrieve them separately.', + '', + 'Also this example only has one action in the list item, and its for toggling the selection. The roles for this one are listbox and option, and the options themselves are focusable.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/Untitled-2 b/packages/react-components/react-list-preview/stories/List/Untitled-2 new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index aa1bcbb846a23e..2f91b204012c59 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -6,13 +6,18 @@ import bestPracticesMd from './ListBestPractices.md'; export { Default } from './ListDefault.stories'; export { ListHorizontal } from './ListHorizontal.stories'; export { ListGrid } from './ListGrid.stories'; -export { ListWithAction } from './ListWithAction.stories'; -export { ListMultipleActions } from './ListMultipleActions.stories'; -export { ListSelectionUncontrolled } from './ListSelectionUncontrolled.stories'; +export { SingleAction } from './SingleAction.stories'; +export { SingleActionSelection } from './SingleAction_selection.stories'; +// export { ListMultipleActions } from './ListMultipleActions.stories'; +export { MultipleActionsNoPrimaryNoSelection } from './MultipleActions_no_primary_no_selection.stories'; +export { MultipleActionsNoPrimary } from './MultipleActions_no_primary.stories'; +export { MultipleActionsPrimarySelection } from './MultipleActions_primary_selection.stories'; +export { MultipleActionsDifferentPrimary } from './MultipleActions_different_primary.stories'; export { ListSelectionControlledBasic } from './ListSelectionControlledBasic.stories'; export { ListSelectionControlledWithState } from './ListSelectionControlledWithState.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; +export { ListActiveElement } from './ListActiveElement.stories'; export default { title: 'Preview Components/List', From 34be3d165b5a99f981ab95f5ff7e5547f6320818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 6 Feb 2024 16:12:51 +0100 Subject: [PATCH 064/136] update stories --- .../src/components/ListItem/useListItem.tsx | 39 +++++++++++++++---- .../List/ListActiveElement.stories.tsx | 32 ++++++++++----- ...tipleActions_different_primary.stories.tsx | 5 --- .../MultipleActions_no_primary.stories.tsx | 3 -- ...ctions_no_primary_no_selection.stories.tsx | 10 +---- ...tipleActions_primary_selection.stories.tsx | 11 ------ 6 files changed, 54 insertions(+), 46 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 1d8c6e5c2bb09f..28ed15327a7308 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -72,7 +72,7 @@ export const useListItem_unstable = ( validateNavigableWhenOnClickPresent(focusableItems, onClick); - const { findFirstFocusable, findPrevFocusable } = useFocusFinders(); + const { findFirstFocusable, findPrevFocusable, findNextFocusable } = useFocusFinders(); const baseIndicatorStyles = useIndicatorStyle(); @@ -88,6 +88,15 @@ export const useListItem_unstable = ( toggleItem?.(e, value); }); + const pressEscape = useEventCallback((target: HTMLElement) => { + target.dispatchEvent( + new KeyboardEvent('keydown', { + key: Escape, + keyCode: keyCodes.Escape, + }), + ); + }); + const handleKeyDown: React.KeyboardEventHandler = useEventCallback(e => { onKeyDown?.(e); @@ -101,15 +110,28 @@ export const useListItem_unstable = ( // If it's one of the Arrows defined, jump out of the list item to focus on the ListItem itself // The ArrowLeft will only trigger if the target element is the leftmost, otherwise the // arrowNavigationAttributes handles it and prevents it from bubbling here. - if (['ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(e.key)) { + + if (e.key === 'ArrowLeft') { + pressEscape(e.target as HTMLElement); + } + + if (['ArrowUp', 'ArrowDown'].includes(e.key)) { // Prevents scrolling for ArrowUp/ArowDown e.preventDefault(); - e.target.dispatchEvent( - new KeyboardEvent('keydown', { - key: Escape, - keyCode: keyCodes.Escape, - }), - ); + + const prevItem = findPrevFocusable(e.currentTarget as HTMLElement); + const nextItem = findNextFocusable(e.currentTarget as HTMLElement); + + if (e.key === 'ArrowUp') { + prevItem && (e.currentTarget as HTMLElement).previousSibling === prevItem + ? prevItem.focus() + : pressEscape(e.target as HTMLElement); + } + if (e.key === 'ArrowDown' && nextItem) { + nextItem && (e.currentTarget as HTMLElement).nextSibling === nextItem + ? nextItem.focus() + : pressEscape(e.target as HTMLElement); + } } } @@ -207,6 +229,7 @@ export const useListItem_unstable = ( root, checkmark, selectable: isSelectionEnabled, + hasCustomOnClick: typeof props.onClick === 'function', }; return state; diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index a72146c064cd50..effe741b25eede 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -8,7 +8,8 @@ import { tokens, SelectionItemId, } from '@fluentui/react-components'; -import { List, ListItem, useListSelection } from '@fluentui/react-list-preview'; +import { Mic16Regular, SpeakerMuteRegular } from '@fluentui/react-icons'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; const names = [ @@ -43,15 +44,16 @@ const items: Item[] = names.map(name => ({ const useStyles = makeStyles({ wrapper: { display: 'grid', - gridTemplateColumns: '200px 1fr', + gridTemplateColumns: '240px 1fr', columnGap: '16px', }, button: { - ...shorthands.padding(0), + alignSelf: 'center', }, item: { cursor: 'pointer', ...shorthands.padding('2px', '6px'), + justifyContent: 'space-between', }, itemSelected: { backgroundColor: tokens.colorSubtleBackgroundSelected, @@ -60,6 +62,7 @@ const useStyles = makeStyles({ // Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. const MyListItem = React.memo(({ name, avatar, ...rest }: { name: string; avatar: string; className?: string }) => { + const styles = useStyles(); return ( + - - - - - } - /> -
-
- ); -}; - -export const ListMultipleActions = (props: Partial) => { - const classes = useStyles(); - - return ( - - - - - - - - - - - - ); -}; - -ListMultipleActions.parameters = { - docs: { - description: { - story: [].join('\n'), - }, - }, -}; diff --git a/packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx deleted file mode 100644 index 674e15bafe4c36..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/ListMultipleActionsGrid.stories.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { - Button, - Caption1, - Card, - CardHeader, - CardPreview, - CardProps, - makeResetStyles, - makeStyles, - Menu, - MenuItem, - MenuList, - MenuPopover, - MenuTrigger, - mergeClasses, - shorthands, - Text, - tokens, -} from '@fluentui/react-components'; -import { MoreHorizontal20Regular } from '@fluentui/react-icons'; -import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; -import * as React from 'react'; - -const useListItemRootStyles = makeResetStyles({ - position: 'relative', - flexGrow: '1', - gap: '8px', - border: '1px solid grey', - alignItems: 'center', - borderRadius: '8px', - gridTemplate: `"preview preview preview" auto - "header action secondary_action" auto / 1fr auto auto - `, -}); - -const useStyles = makeStyles({ - list: { - display: 'flex', - flexDirection: 'column', - ...shorthands.gap('16px'), - maxWidth: '300px', - }, - listItem: { display: 'grid', ...shorthands.padding('8px') }, - - caption: { - color: tokens.colorNeutralForeground3, - }, - - image: { - maxWidth: '100%', - ...shorthands.borderRadius('5px'), - }, - - grayBackground: { - backgroundColor: tokens.colorNeutralBackground3, - }, - - logoBadge: { - ...shorthands.padding('5px'), - ...shorthands.borderRadius(tokens.borderRadiusSmall), - backgroundColor: '#FFF', - boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', - }, - actionsWrapper: { - display: 'flex', - ...shorthands.gap('8px'), - }, - checkmark: { - position: 'absolute', - left: '10px', - top: '10px', - zIndex: 1, - }, -}); - -const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { - const listItemStyles = useListItemRootStyles(); - const styles = useStyles(); - const { value } = props; - - return ( - { - // if (e.target !== e.currentTarget) { - // return; - // } - - // prevent selection - e.preventDefault(); - - alert('main action trigerred'); - }} - - // aria-owns={owns} - > -
- Presentation Preview -
-
- - {props.value} - - You created 53m ago -
-
- -
-
- - - -
-
- ); -}; - -export const ListMultipleActionsGrid = (props: Partial) => { - const classes = useStyles(); - - const [selectedItems, setSelectedItems] = React.useState>([]); - - return ( - setSelectedItems(data.selectedItems)} - selectionMode="multiselect" - aria-setsize={9} - > - - - - - - - - - - - ); -}; - -ListMultipleActionsGrid.parameters = { - docs: { - description: { - story: ['You can select the items with a click or using a Spacebar key.'].join('\n'), - }, - }, -}; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 2f91b204012c59..90c837c76c9127 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -8,7 +8,6 @@ export { ListHorizontal } from './ListHorizontal.stories'; export { ListGrid } from './ListGrid.stories'; export { SingleAction } from './SingleAction.stories'; export { SingleActionSelection } from './SingleAction_selection.stories'; -// export { ListMultipleActions } from './ListMultipleActions.stories'; export { MultipleActionsNoPrimaryNoSelection } from './MultipleActions_no_primary_no_selection.stories'; export { MultipleActionsNoPrimary } from './MultipleActions_no_primary.stories'; export { MultipleActionsPrimarySelection } from './MultipleActions_primary_selection.stories'; From da29fc54f21d5a126f2f2558ab1d8501a1496c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 13 Feb 2024 09:46:23 +0100 Subject: [PATCH 073/136] fix build --- apps/public-docsite-v9/.storybook/main.js | 1 - .../react-components/package.json | 3 ++- .../react-components/src/index.ts | 26 +++++++++++++++++++ .../react-list-preview/package.json | 4 +-- .../src/components/ListItem/ListItem.test.tsx | 2 +- .../__snapshots__/ListItem.test.tsx.snap | 4 +-- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/public-docsite-v9/.storybook/main.js b/apps/public-docsite-v9/.storybook/main.js index 3eb118d6ae42d0..ef7da777e61ffc 100644 --- a/apps/public-docsite-v9/.storybook/main.js +++ b/apps/public-docsite-v9/.storybook/main.js @@ -12,7 +12,6 @@ module.exports = /** @type {Omit=16.14.0 <19.0.0", diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index a03697da252adf..1e1830e2e6c312 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -1571,3 +1571,29 @@ export { useAriaLiveAnnouncerContextValues_unstable, } from '@fluentui/react-aria'; export type { AriaLiveAnnouncerProps, AriaLiveAnnouncerState } from '@fluentui/react-aria'; + +// List preview just for building PR docsite +export { + List, + listClassNames, + renderList_unstable, + useListStyles_unstable, + useList_unstable, + ListItem, + listItemClassNames, + renderListItem_unstable, + useListItemStyles_unstable, + useListItem_unstable, + ListItemButton, + useListItemButtonStyles_unstable, + useListSelection, +} from '@fluentui/react-list-preview'; + +export type { + ListProps, + ListSlots, + ListState, + ListItemProps, + ListItemSlots, + ListItemState, +} from '@fluentui/react-list-preview'; diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 594ac7abec4bad..11bd211cbe6c36 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -1,8 +1,8 @@ { "name": "@fluentui/react-list-preview", - "version": "0.0.0", + "version": "0.0.1", "private": true, - "description": "New fluentui react package", + "description": "React List v9", "main": "lib-commonjs/index.js", "module": "lib/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx index 55058f3b6d3802..f62b09fc6f3046 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx @@ -12,7 +12,7 @@ describe('ListItem', () => { 'has-static-classnames': [ { props: { - checkmark: 'test checkmark', + checkmark: { renderByDefault: true }, }, }, ], diff --git a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap index dc5d44d8058f10..a9644caf36164c 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap @@ -4,8 +4,8 @@ exports[`ListItem renders a default state 1`] = `
  • Default ListItem From 165ac90d4e056eabb9209cecdbcbda834bed3299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 13 Feb 2024 10:42:32 +0100 Subject: [PATCH 074/136] api, change --- ...-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json | 7 +++ .../etc/react-components.api.md | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json diff --git a/change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json b/change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json new file mode 100644 index 00000000000000..db496dd07d2072 --- /dev/null +++ b/change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Feat: Export List Preview component", + "packageName": "@fluentui/react-components", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-components/etc/react-components.api.md b/packages/react-components/react-components/etc/react-components.api.md index 39899cf84cea2c..c3ad467eb80387 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -416,6 +416,7 @@ import { linkClassNames } from '@fluentui/react-link'; import { LinkProps } from '@fluentui/react-link'; import { LinkSlots } from '@fluentui/react-link'; import { LinkState } from '@fluentui/react-link'; +import { List } from '@fluentui/react-list-preview'; import { Listbox } from '@fluentui/react-combobox'; import { listboxClassNames } from '@fluentui/react-combobox'; import { ListboxContextValue } from '@fluentui/react-combobox'; @@ -424,6 +425,16 @@ import { ListboxProps } from '@fluentui/react-combobox'; import { ListboxProvider } from '@fluentui/react-combobox'; import { ListboxSlots } from '@fluentui/react-combobox'; import { ListboxState } from '@fluentui/react-combobox'; +import { listClassNames } from '@fluentui/react-list-preview'; +import { ListItem } from '@fluentui/react-list-preview'; +import { ListItemButton } from '@fluentui/react-list-preview'; +import { listItemClassNames } from '@fluentui/react-list-preview'; +import { ListItemProps } from '@fluentui/react-list-preview'; +import { ListItemSlots } from '@fluentui/react-list-preview'; +import { ListItemState } from '@fluentui/react-list-preview'; +import { ListProps } from '@fluentui/react-list-preview'; +import { ListSlots } from '@fluentui/react-list-preview'; +import { ListState } from '@fluentui/react-list-preview'; import { makeResetStyles } from '@griffel/react'; import { makeStaticStyles } from '@griffel/react'; import { makeStyles } from '@griffel/react'; @@ -691,7 +702,9 @@ import { renderInteractionTagPrimary_unstable } from '@fluentui/react-tags'; import { renderInteractionTagSecondary_unstable } from '@fluentui/react-tags'; import { renderLabel_unstable } from '@fluentui/react-label'; import { renderLink_unstable } from '@fluentui/react-link'; +import { renderList_unstable } from '@fluentui/react-list-preview'; import { renderListbox_unstable } from '@fluentui/react-combobox'; +import { renderListItem_unstable } from '@fluentui/react-list-preview'; import { renderMenu_unstable } from '@fluentui/react-menu'; import { renderMenuButton_unstable } from '@fluentui/react-button'; import { renderMenuDivider_unstable } from '@fluentui/react-menu'; @@ -1238,9 +1251,15 @@ import { useLabelStyles_unstable } from '@fluentui/react-label'; import { useLink_unstable } from '@fluentui/react-link'; import { useLinkState_unstable } from '@fluentui/react-link'; import { useLinkStyles_unstable } from '@fluentui/react-link'; +import { useList_unstable } from '@fluentui/react-list-preview'; import { useListbox_unstable } from '@fluentui/react-combobox'; import { useListboxContextValues } from '@fluentui/react-combobox'; import { useListboxStyles_unstable } from '@fluentui/react-combobox'; +import { useListItem_unstable } from '@fluentui/react-list-preview'; +import { useListItemButtonStyles_unstable } from '@fluentui/react-list-preview'; +import { useListItemStyles_unstable } from '@fluentui/react-list-preview'; +import { useListSelection } from '@fluentui/react-list-preview'; +import { useListStyles_unstable } from '@fluentui/react-list-preview'; import { useMenu_unstable } from '@fluentui/react-menu'; import { useMenuButton_unstable } from '@fluentui/react-button'; import { useMenuButtonStyles_unstable } from '@fluentui/react-button'; @@ -2255,6 +2274,8 @@ export { LinkSlots } export { LinkState } +export { List } + export { Listbox } export { listboxClassNames } @@ -2271,6 +2292,26 @@ export { ListboxSlots } export { ListboxState } +export { listClassNames } + +export { ListItem } + +export { ListItemButton } + +export { listItemClassNames } + +export { ListItemProps } + +export { ListItemSlots } + +export { ListItemState } + +export { ListProps } + +export { ListSlots } + +export { ListState } + export { makeResetStyles } export { makeStaticStyles } @@ -2805,8 +2846,12 @@ export { renderLabel_unstable } export { renderLink_unstable } +export { renderList_unstable } + export { renderListbox_unstable } +export { renderListItem_unstable } + export { renderMenu_unstable } export { renderMenuButton_unstable } @@ -3899,12 +3944,24 @@ export { useLinkState_unstable } export { useLinkStyles_unstable } +export { useList_unstable } + export { useListbox_unstable } export { useListboxContextValues } export { useListboxStyles_unstable } +export { useListItem_unstable } + +export { useListItemButtonStyles_unstable } + +export { useListItemStyles_unstable } + +export { useListSelection } + +export { useListStyles_unstable } + export { useMenu_unstable } export { useMenuButton_unstable } From 0ad45ead5707ebf06a6ce89ff6b2eedb4eb13c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 13 Feb 2024 10:46:59 +0100 Subject: [PATCH 075/136] unprivate list --- packages/react-components/react-list-preview/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 11bd211cbe6c36..e9a62bf994c663 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -1,7 +1,6 @@ { "name": "@fluentui/react-list-preview", "version": "0.0.1", - "private": true, "description": "React List v9", "main": "lib-commonjs/index.js", "module": "lib/index.js", From a6b633de6ccd92e52ee914ce542a01bf53ad22c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 13 Feb 2024 10:50:02 +0100 Subject: [PATCH 076/136] change - list unprivate --- ...-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json diff --git a/change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json b/change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json new file mode 100644 index 00000000000000..41a98e175bd637 --- /dev/null +++ b/change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "List: unmark private", + "packageName": "@fluentui/react-list-preview", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +} From 18c366ea54bc00a5b558c4ecee26028da36902d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 14 Feb 2024 10:50:12 +0100 Subject: [PATCH 077/136] update document, fix examples, add one more example --- .../react-list-preview/docs/ListA11y.md | 44 ++++++++++----- .../List/ListActiveElement.stories.tsx | 2 +- ...ons_no_selection_with_primary.stories.tsx} | 54 ++++++------------- ...tipleActions_primary_selection.stories.tsx | 5 +- .../stories/List/index.stories.tsx | 2 +- 5 files changed, 52 insertions(+), 55 deletions(-) rename packages/react-components/react-list-preview/stories/List/{MultipleActions_no_primary.stories.tsx => MultipleActions_no_selection_with_primary.stories.tsx} (64%) diff --git a/packages/react-components/react-list-preview/docs/ListA11y.md b/packages/react-components/react-list-preview/docs/ListA11y.md index b1eae77c242ac8..caee2005fc8587 100644 --- a/packages/react-components/react-list-preview/docs/ListA11y.md +++ b/packages/react-components/react-list-preview/docs/ListA11y.md @@ -1,5 +1,8 @@ # Accessibility of Lists on the web: why we can't have nice things +Rev. 1 - Initial draft +Rev. 2 - Added examples links, made conclusions in italic, explained bahavior for Space and Enter on lists with a primary action, some wording changed + Accessibility in browsers is hard in general. When it comes to modern web applications with complex UIs, that statement is more true then ever. And then there are Lists. For some reason, there is not a single aria role that would support proper position narration, selection narration, and complex widget elements with secondary actions. @@ -33,6 +36,8 @@ In the following section, I will go through the each funcional requirement and d ### Single action lists +[example](https://fluentuipr.z22.web.core.windows.net/pull/29760/public-docsite-v9/storybook/iframe.html?viewMode=docs&id=preview-components-list--default#single-action) + List with a single action is a collection of items with common action, specific to each item. One example would be a list of people, where clicking on a person will open a popup with details. For a List with a single action, there are generally 2 approaches we can take: @@ -66,7 +71,7 @@ We have multiple aria roles we can explore and see if any of those fill all of o - This can be **worked around** by using `aria-roledescription` - Selection: N/A -`listitem` role on it's own is not enough co communicate that there is an action that can be triggered by Enter, unless this fact is explicitely communicated in a `aria-label` or `aria-roledescription`. +_`listitem` role on it's own is not enough co communicate that there is an action that can be triggered by Enter, unless this fact is explicitely communicated in a `aria-label` or `aria-roledescription`._ ##### Menu/Menuitem @@ -80,13 +85,13 @@ We have multiple aria roles we can explore and see if any of those fill all of o - "Menu" is not semantically correct for our example use case. List is different from a Menu in a way that in a List, each List item is of the same "category" (list of people, emails, conversations, applications) and each list item action triggers the same action, while in a Menu the user expects each option to do something else. - Creates a communication barrier between the sighted user and user relying on a screen reader. If a sighted user instructs visually impaired user to go on "the list of people", they would only be able to find "a menu". -While the `menuitem` role seems to work, its semantically different from a list enough, that it would add confusion and noise. +_While the `menuitem` role seems to work, its semantically different from a list enough, that it would add confusion and noise._ #### Outcome Seems like for our "simple" use case of a single action in a list item, we don't have a perfect solution. Each of the three suggested variants have their cons. While some of the downsides of certain solutions are fundamental (confusion between listitem and menuitem), others can be worked around. -My suggestion for this usecase would to **make the List Item focusable, use `list` and `listitem` roles and add a translated string of "button" as `aria-roledescription` when an action on the list Item is present**. +_My suggestion for this usecase would to **make the List Item focusable, use `list` and `listitem` roles and add a translated string of "button" as `aria-roledescription` when an action on the list Item is present**._ This way we make sure that the user knows they are in a List, the position is properly announced and a translated "button" role description is present, making it clear you can press "Enter" to trigger it. @@ -109,6 +114,8 @@ Voice Over (using just arrow keys): ### Single action lists - selection +[Visit example](https://fluentuipr.z22.web.core.windows.net/pull/29760/public-docsite-v9/storybook/iframe.html?viewMode=docs&id=preview-components-list--default#single-action-selection) + List with a single action where the action is toggling the selection. One example would be a list of people to add to a call, where clicking on a row/person will add them to the selection. There is no other action that can be triggered on the list items. The whole list item can be focused and the selection can be toggled with **spacebar**. Also, the whole item is clickable with a mouse and triggers the same action. @@ -124,11 +131,13 @@ Counter-intuitively, this case is more straightforward to handle than the previo - Selection: ✅ - The `listbox`/`option` role support `aria-selected` attribute which is properly narrated as it changes. -This scenario is straightforward, there is an aria role which fits perfectly in our use case. It has one downside, the `option` role is [always presentational](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role#all_descendants_are_presentational), which means that `listbox` with `option` cannot be used in a scenario where there are other actions inside of the list item, but more on that later. +_This scenario is straightforward, there is an aria role which fits perfectly in our use case. It has one downside, the `option` role is [always presentational](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role#all_descendants_are_presentational), which means that `listbox` with `option` cannot be used in a scenario where there are other actions inside of the list item, but more on that later._ ### List with multiple actions - no primary action -When multiple actions are available in a list item, things become a bit more complicated, as some aria roles are not equipped to handle that at all (like `option`). +[example](https://fluentuipr.z22.web.core.windows.net/pull/29760/public-docsite-v9/storybook/index.html?path=/docs/preview-components-list--default#multiple-actions-no-primary-no-selection) + +When multiple actions are available in a list item, things become a bit more complicated, as some aria roles are not equipped to handle that at all (like `option` because of it's inherent property of [all descendants being presentational](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role#all_descendants_are_presentational)). For the following scenarios we can establish some basic keyboard navigation that should be supported regardless of any a11y role we add. @@ -152,7 +161,7 @@ In general, we have 2 options for this scenario: - Other considerations: - Would the user expect to click right arrow key to get inside the list item? -For this scenario, List/ListItem seems like a good choice, since we don't need support for selection or actionable rows (list items). +_For this scenario, List/ListItem seems like a good choice, since we don't need support for selection or actionable rows (list items)._ #### Grid / Row / Gridcell @@ -163,19 +172,28 @@ I'm writing about it here for completion and to contrast it with the listitem ro In grid, each list item is of role `row` and each actionable element inside should be in its own `cell` role element. - Position: ❌ - - The position is not properly narrated, we get `row` but not `row x of y` - - Actionable: N/A - Selection: N/A - - Other considerations: ⚠️ - The nature of `grid`/`row`/`gridcell` roles forces the developers to actually stick to this strict HTML layout. A cell should be wrapping actionable element, but it should be a direct child of the `row`, preventing users from building more complex widgets with custom HTML structure. ### List with multiple actions - with selection +[example](https://fluentuipr.z22.web.core.windows.net/pull/29760/public-docsite-v9/storybook/index.html?path=/docs/preview-components-list--default#multiple-actions-primary-selection) + Things become a bit more complicated when selection is involved, as we need the proper a11y announcements when the selection state is changed. This is not supported for the `listitem` role, which we deemed perfect for complex list without selection. Even if `aria-selected` is added to a `listitem`, the screen readers just ignore that property (since it's not valid for `listitem` role). +When the list supports selection, the main action of the list item is **to toggle the selection** by default. + +**Left mouse button** always triggers _onClick_, which toggles the selection, if enabled. A custom action can be triggered on click instead, by passing a custom `onClick` handler to the `ListItem` component and calling `preventDefault()` on the event. See how this works [here](https://fluentuipr.z22.web.core.windows.net/pull/29760/public-docsite-v9/storybook/index.html?path=/docs/preview-components-list--default#multiple-actions-different-primary). + +**Spacebar** on the `ListItem` always toggles the selection. + +**Enter** on the `ListItem` triggers the main action, which can be changed by passing the `onClick` handler, i.e. by default it triggers selection, but this behavior can be overriden (by changing the onClick handler). + +Both keys behavior can be changed by passing the `onKeyDown` handler and preventing teh default by calling `preventDefault()` on the event. Please note that the uncontrolled selection can no longer be utilized in this case and you have to take control. + #### Listbox / option Since [all descendats of option are presentational](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/option_role#all_descendants_are_presentational), this role is not viable for this case, since we need to allow the screen reader to go inside of the list item to focus on the secondary actions. @@ -188,16 +206,18 @@ While this role technically works on Windows using the screen reader Focus mode, - While we get announcement for `row` as "row", we don't get the row number `row x of y` in any of the screen readers tested. This is a big limitation of this role, could be a chromium bug. - This _can_ be worked around from the user world by passing the order as part of the `aria-label`. This is not without it's downsides though. - Actionable: ✅ - - Since the rows can be selected, it is reasonable to expect that the users would trigger the action on the list item (row). + - Since the rows can be selected, it is reasonable to expect that the users understand that they can trigger the action on the list item (row). - Selection: ✅ - Rows can be selected, and the selection is properly announced when it changes. - Other considerations: ⚠️ - As mentioned previously, when using this role, it is importand that the HTML structure is precisely `grid > row > gridcell > actionable element`. For some complex layouts, this may not be always easy / possible to do. -While grid role puts some constrain on the DOM structure to work properly, it is the only accessibility role I found that supports `aria-selected` and allows complex widgets inside. +_While grid role puts some constrain on the DOM structure to work properly, it is the only accessibility role I found that supports `aria-selected` and allows complex widgets inside._ ### List with multiple actions, without selection +[example](https://fluentuipr.z22.web.core.windows.net/pull/29760/public-docsite-v9/storybook/index.html?path=/docs/preview-components-list--default#multiple-actions-no-selection-with-primary) + When no selection is involved in the equation, we don't have to limit ourselves to using a11y roles that support `aria-selected`, which makes things a bit easier. #### Grid / Row / Gridcell @@ -218,4 +238,4 @@ When no selection is involved in the equation, we don't have to limit ourselves - Action can be put directly on the List Item. Discoverability might be a small issue again, but a context, updated label or supporting aria attributes like `aria-roledescription` can easily solve that - Selection: N/A -While grid would technically work for this scenario, there are some downsides. **List and Listitem roles are the clear winners in this case**. +_While grid would technically work for this scenario, there are some downsides. **List and Listitem roles are the clear winners in this case**. For consistency however we might want to go with grid._ diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index 7f96cb595f5a76..01443fe0181b63 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -8,7 +8,7 @@ import { tokens, SelectionItemId, } from '@fluentui/react-components'; -import { Mic16Regular, SpeakerMuteRegular } from '@fluentui/react-icons'; +import { Mic16Regular } from '@fluentui/react-icons'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_no_selection_with_primary.stories.tsx similarity index 64% rename from packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx rename to packages/react-components/react-list-preview/stories/List/MultipleActions_no_selection_with_primary.stories.tsx index 14904a980ec8bd..020db001d35678 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActions_no_primary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_no_selection_with_primary.stories.tsx @@ -37,7 +37,7 @@ const useStyles = makeStyles({ ...shorthands.gap('16px'), maxWidth: '300px', }, - listItem: { display: 'grid', ...shorthands.padding('8px'), cursor: 'default' }, + listItem: { display: 'grid', ...shorthands.padding('8px') }, caption: { color: tokens.colorNeutralForeground3, @@ -70,7 +70,7 @@ const useStyles = makeStyles({ }, }); -const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { +const CardExample = (props: CardProps & { value: string }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; @@ -79,12 +79,8 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = { - e.preventDefault(); - }} + onClick={() => alert('Hello!')} >
    Presentation Preview @@ -159,47 +155,29 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = ); }; -export const MultipleActionsNoPrimary = (props: Partial) => { +export const MultipleActionsNoSelectionWithPrimary = (props: Partial) => { const classes = useStyles(); - const [selectedItems, setSelectedItems] = React.useState>([]); - return ( - setSelectedItems(data.selectedItems)} - selectionMode="multiselect" - aria-setsize={9} - > - - - - - - - - - + + + + + + + + + + ); }; -MultipleActionsNoPrimary.parameters = { +MultipleActionsNoSelectionWithPrimary.parameters = { docs: { description: { story: [ - "Base item with multiple actions. Doesn't have a primary action on the list item, but supports selection by checking the checkbox.", - '', - 'The roles in this example are `list` and `listitem`, inherited from the ul/li tags used. List items themselves are focusable.', - '', - 'When the selection is enabled, the list item default click behavior is to toggle that selection. To remove ', - 'this behavior, we need to add a click handler to the list item that prevents the default behavior. ', - '', - 'Selection by clicking the checkbox still works and is properly narrated by screen readers.', + "Base item with multiple actions. Doesn't support selection, but the list items have another primary action.", ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx index 4db2d03db86bfe..dd0a0a29c698bc 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_primary_selection.stories.tsx @@ -80,7 +80,7 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = value={props.value} className={mergeClasses(listItemStyles, styles.listItem)} checkmark={{ className: styles.checkmark }} - role="option" + role="row" aria-label={value} >
    @@ -166,8 +166,7 @@ export const MultipleActionsPrimarySelection = (props: Partial) => { className={classes.list} focusableItems selectable - role="listbox" - aria-orientation="vertical" + role="grid" onSelectionChange={(e, data) => setSelectedItems(data.selectedItems)} selectionMode="multiselect" > diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 90c837c76c9127..56b4ba2e5a2d43 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -9,7 +9,7 @@ export { ListGrid } from './ListGrid.stories'; export { SingleAction } from './SingleAction.stories'; export { SingleActionSelection } from './SingleAction_selection.stories'; export { MultipleActionsNoPrimaryNoSelection } from './MultipleActions_no_primary_no_selection.stories'; -export { MultipleActionsNoPrimary } from './MultipleActions_no_primary.stories'; +export { MultipleActionsNoSelectionWithPrimary } from './MultipleActions_no_selection_with_primary.stories'; export { MultipleActionsPrimarySelection } from './MultipleActions_primary_selection.stories'; export { MultipleActionsDifferentPrimary } from './MultipleActions_different_primary.stories'; export { ListSelectionControlledBasic } from './ListSelectionControlledBasic.stories'; From e65222518b6a8e1880f8b8fbc71123ab88cdd375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 14 Feb 2024 15:39:34 +0100 Subject: [PATCH 078/136] remove unused stories --- .../stories/ListItem/ListItemBestPractices.md | 5 ----- .../ListItem/ListItemDefault.stories.tsx | 4 ---- .../stories/ListItem/ListItemDescription.md | 0 .../stories/ListItem/index.stories.tsx | 18 ------------------ .../ListItemButtonBestPractices.md | 5 ----- .../ListItemButtonDefault.stories.tsx | 5 ----- .../ListItemButtonDescription.md | 0 .../stories/ListItemButton/index.stories.tsx | 18 ------------------ 8 files changed, 55 deletions(-) delete mode 100644 packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md delete mode 100644 packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx delete mode 100644 packages/react-components/react-list-preview/stories/ListItem/ListItemDescription.md delete mode 100644 packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx delete mode 100644 packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonBestPractices.md delete mode 100644 packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx delete mode 100644 packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDescription.md delete mode 100644 packages/react-components/react-list-preview/stories/ListItemButton/index.stories.tsx diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md b/packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md deleted file mode 100644 index 08ff8ddeeb5f86..00000000000000 --- a/packages/react-components/react-list-preview/stories/ListItem/ListItemBestPractices.md +++ /dev/null @@ -1,5 +0,0 @@ -## Best practices - -### Do - -### Don't diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx b/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx deleted file mode 100644 index 2845b17e2e625a..00000000000000 --- a/packages/react-components/react-list-preview/stories/ListItem/ListItemDefault.stories.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import * as React from 'react'; -import { ListItem, ListItemProps } from '@fluentui/react-list-preview'; - -export const Default = (props: ListItemProps) => ; diff --git a/packages/react-components/react-list-preview/stories/ListItem/ListItemDescription.md b/packages/react-components/react-list-preview/stories/ListItem/ListItemDescription.md deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx b/packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx deleted file mode 100644 index fdd4db71031fff..00000000000000 --- a/packages/react-components/react-list-preview/stories/ListItem/index.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ListItem } from '@fluentui/react-list-preview'; - -import descriptionMd from './ListItemDescription.md'; -import bestPracticesMd from './ListItemBestPractices.md'; - -export { Default } from './ListItemDefault.stories'; - -export default { - title: 'Preview Components/ListItem', - component: ListItem, - parameters: { - docs: { - description: { - component: [descriptionMd, bestPracticesMd].join('\n'), - }, - }, - }, -}; diff --git a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonBestPractices.md b/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonBestPractices.md deleted file mode 100644 index 08ff8ddeeb5f86..00000000000000 --- a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonBestPractices.md +++ /dev/null @@ -1,5 +0,0 @@ -## Best practices - -### Do - -### Don't diff --git a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx b/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx deleted file mode 100644 index a566a6b6b2b56d..00000000000000 --- a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDefault.stories.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as React from 'react'; -import { ListItemButton } from '@fluentui/react-list-preview'; -import { ButtonProps } from '@fluentui/react-components'; - -export const Default = (props: ButtonProps) => ; diff --git a/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDescription.md b/packages/react-components/react-list-preview/stories/ListItemButton/ListItemButtonDescription.md deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/packages/react-components/react-list-preview/stories/ListItemButton/index.stories.tsx b/packages/react-components/react-list-preview/stories/ListItemButton/index.stories.tsx deleted file mode 100644 index 21996d5ecd6c91..00000000000000 --- a/packages/react-components/react-list-preview/stories/ListItemButton/index.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ListItemButton } from '@fluentui/react-list-preview'; - -import descriptionMd from './ListItemButtonDescription.md'; -import bestPracticesMd from './ListItemButtonBestPractices.md'; - -export { Default } from './ListItemButtonDefault.stories'; - -export default { - title: 'Preview Components/ListItemButton', - component: ListItemButton, - parameters: { - docs: { - description: { - component: [descriptionMd, bestPracticesMd].join('\n'), - }, - }, - }, -}; From d579247d1e5d3160122a073651c9c97ce136768c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 15 Feb 2024 11:00:16 +0100 Subject: [PATCH 079/136] spacebar toggles, but enter has different action, cleanup commented code --- .../src/components/ListItem/useListItem.tsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 2d23c325e01972..3013f3a6dd4017 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -28,13 +28,13 @@ function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', rend } } -function validateNavigableWhenOnClickPresent(navigable: boolean, onClick?: React.MouseEventHandler) { +function validateNavigableWhenOnClickPresent(focusableItems: boolean, onClick?: React.MouseEventHandler) { if (process.env.NODE_ENV === 'production') { return; } - if (onClick && !navigable) { - throw new Error('ListItem must be navigable when onClick is present. Set focusableItems={true} on the List.'); + if (onClick && !focusableItems) { + throw new Error('ListItem must be focusableItems when onClick is present. Set focusableItems={true} on the List.'); } } @@ -137,8 +137,15 @@ export const useListItem_unstable = ( return; } - // Handle selection for enter and space - if ((isSelectionEnabled || typeof props.onClick === 'function') && [Space, Enter].includes(e.key)) { + // Space always toggles selection (if enabled) + if (isSelectionEnabled && e.key === Space) { + e.preventDefault(); + toggleItem?.(e, value); + } + + // Handle clicking the list item when user presses the Enter key + // This internally triggers selection in the onClick handler if it hasn't been prevented + if (e.key === Enter) { e.preventDefault(); e.currentTarget.click(); } @@ -166,19 +173,8 @@ export const useListItem_unstable = ( // toggleItem?.(ev, value); }); - // const handleKeyUp: React.KeyboardEventHandler = useEventCallback(e => { - // onKeyUp?.(e); - - // if (!isSelectionEnabled || e.defaultPrevented || e.target !== e.currentTarget) { - // // In this case prevent default prevents the useARIAButtonProps from toggling onClick - // e.preventDefault(); - // return; - // } - // }); const arrowNavigationAttributes = useArrowNavigationGroup({ axis: 'horizontal', - // tabbable: false, - // memorizeCurrent: true, }); const tabsterAttributes = useMergedTabsterAttributes_unstable( @@ -209,6 +205,7 @@ export const useListItem_unstable = ( checked: isSelected, onChange: onCheckboxChange, onClick: onCheckboxClick, + tabIndex: -1, indicator: { className: baseIndicatorStyles.root }, }, renderByDefault: isSelectionEnabled, From fb07223153aa66650889cbcb9e72499f3f399b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 15 Feb 2024 13:28:16 +0100 Subject: [PATCH 080/136] dont toggle selection by pressing enter --- .../src/components/ListItem/useListItem.tsx | 15 ++++++++++++--- .../stories/List/SingleAction.stories.tsx | 14 ++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 3013f3a6dd4017..e039af7224cae6 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -81,7 +81,11 @@ export const useListItem_unstable = ( return; } - toggleItem?.(e, value); + // Only toggle if this was actually a pointer click, not an event triggered by Enter + // key down (handled below). + if (e.isTrusted) { + toggleItem?.(e, value); + } }); const pressEscape = useEventCallback((target: HTMLElement) => { @@ -138,9 +142,13 @@ export const useListItem_unstable = ( } // Space always toggles selection (if enabled) - if (isSelectionEnabled && e.key === Space) { + if (e.key === Space) { e.preventDefault(); - toggleItem?.(e, value); + if (isSelectionEnabled) { + toggleItem?.(e, value); + } else { + e.currentTarget.click(); + } } // Handle clicking the list item when user presses the Enter key @@ -168,6 +176,7 @@ export const useListItem_unstable = ( toggleItem?.(ev, value); }); + const onCheckboxClick = useEventCallback((ev: React.MouseEvent) => { ev.stopPropagation(); // toggleItem?.(ev, value); diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index 1f1ae13c70079c..ed2480bd01db91 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -41,10 +41,9 @@ export const SingleAction = () => { key={name} role="listitem" aria-label={name} - aria-roledescription="button" //needs to be translated string! + aria-roledescription="button" //needs to be a translated string! onClick={() => alert(name)} > - {/* */} { }, }} /> - {/* */} ))} @@ -66,9 +64,13 @@ SingleAction.parameters = { docs: { description: { story: [ - 'When the list item contains actionable element, it is recommended to use the built-in `button` slot on the `ListItem` component to handle these interactions. This will ensure the proper keyboard navigation and accessibility announcements.', - ``, - 'In this case, the `ListItem` itself should __not be focusable, neither it should have any aria roles assigned__. It should be transparent to the screen reader and keyboard navigation.', + 'When the list item has a primary action on it, you can pass the `onClick` prop to the `ListItem` component.', + 'This callback will also be automatically called when the user presses the Enter key on the list item.', + '', + 'Since we are directly attaching the `onClick` handler on the `ListItem` itself, it is important to', + 'explicitely mark the `ListItem` as a button by setting the `aria-roledescription` attribute to a localized string `button`.', + '', + 'This way the screen reader users will be informed that the list item is actionable and can be activated by pressing the Enter key.', ].join('\n'), }, }, From 71962164a458b9923fd330adc17eb42c01e8f006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 16 Feb 2024 14:09:02 +0100 Subject: [PATCH 081/136] remove focusableItems prop, remove horizontal/grid layout --- .../react-list-preview/docs/MIGRATION.md | 8 +-- .../react-list-preview/docs/Spec.md | 2 +- .../etc/react-list-preview.api.md | 4 +- .../src/components/List/List.types.ts | 8 +-- .../List/__snapshots__/List.test.tsx.snap | 2 +- .../src/components/List/listContext.ts | 1 - .../src/components/List/useList.ts | 9 +-- .../components/List/useListContextValues.ts | 3 +- .../components/List/useListStyles.styles.ts | 29 +------- .../src/components/ListItem/useListItem.tsx | 20 ++---- .../List/ListActiveElement.stories.tsx | 1 - .../stories/List/ListBestPractices.md | 5 +- .../stories/List/ListDescription.md | 4 +- .../stories/List/ListGrid.stories.tsx | 2 +- .../stories/List/ListHorizontal.stories.tsx | 2 +- .../List/ListWithMultipleActions.stories.tsx | 67 ------------------- ...tipleActions_different_primary.stories.tsx | 15 ++--- ...ctions_no_primary_no_selection.stories.tsx | 9 ++- ...ions_no_selection_with_primary.stories.tsx | 2 +- ...tipleActions_primary_selection.stories.tsx | 7 +- .../stories/List/SingleAction.stories.tsx | 2 +- .../stories/List/index.stories.tsx | 4 +- 22 files changed, 44 insertions(+), 162 deletions(-) delete mode 100644 packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx diff --git a/packages/react-components/react-list-preview/docs/MIGRATION.md b/packages/react-components/react-list-preview/docs/MIGRATION.md index 31475d17a7c85e..9a3c710374fc4e 100644 --- a/packages/react-components/react-list-preview/docs/MIGRATION.md +++ b/packages/react-components/react-list-preview/docs/MIGRATION.md @@ -70,8 +70,6 @@ Most of the v8 props are for it's virtualization functionality. Since the v9 `Li | `usePageCache` | N/A | | `version` | N/A | | - | `defaultSelectedItems` | -| - | `focusableItems` | -| - | `layout` | | - | `onSelectionChange` | | - | `selectable` | | - | `selectionMode` | @@ -162,9 +160,9 @@ We recommend using a component like `Persona` where possible, or creating a cust | `debug` | N/A | | `defaultSelectedIndex` | `defaultSelectedItems` | | `design` | N/A | -| `horizontal` | use `layout` with a value `horizontal` | +| `horizontal` | N/A - will be added in the future | | `items` | N/A - use `ListItem` components as Children | -| `navigable` | `focusableItems` if the `ListItem` contains more than 1 actionable element, otherwise use `ListItemButton` component as a child. | +| `navigable` | `tabIndex={0}` if the `ListItem`, or automatic when `onClick` or `onKeyDown` is present | | `onSelectedIndexChange` | `onSelectionChange` | | `ref` | `ref` | | `selectable` | `selectable` | @@ -192,7 +190,7 @@ We recommend using a component like `Persona` where possible, or creating a cust | `important` | N/A | | `index` | N/A | | `media` | N/A - use children | -| `navigable` | N/A - use `List` property `focusableItems` or render `ListItemButton` if the `ListItem` only has one action | +| `navigable` | N/A - use `tabIndex={0}` | | `onClick` | `onClick` | | `ref` | ref | | `selectable` | N/A - use `List` property `selectable` for uncontrolled selection, or use `useListSelection` hook to control the state | diff --git a/packages/react-components/react-list-preview/docs/Spec.md b/packages/react-components/react-list-preview/docs/Spec.md index e7f815c204cbc6..cbf05e0b2ea7a6 100644 --- a/packages/react-components/react-list-preview/docs/Spec.md +++ b/packages/react-components/react-list-preview/docs/Spec.md @@ -2,7 +2,7 @@ ## Background -A List is a component that displays a group of sequential components in one dimension. The layout can be vertical (default), horizontal or grid with completely custom template. +A List is a component that displays a set of vertically stacked components. If you are displaying more than one dimension of the data, the List probably isn't the proper component to use, instead, consider using Table or DataGrid. diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 82661ab98986f1..35480bf51dd0b1 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -53,8 +53,6 @@ export type ListItemState = ComponentState & { // @public export type ListProps = ComponentProps & { - layout?: ListLayout; - focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode_2; selectedItems?: SelectionItemId[]; @@ -70,7 +68,7 @@ export type ListSlots = { }; // @public -export type ListState = ComponentState & Required> & ListContextValue; +export type ListState = ComponentState & ListContextValue; // @public export const renderList_unstable: (state: ListState, contextValues: ListContextValues) => JSX.Element; diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index a647e217010942..b8f07395e3c719 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -2,8 +2,6 @@ import * as React from 'react'; import type { ComponentProps, ComponentState, Slot, SelectionMode, SelectionItemId } from '@fluentui/react-utilities'; import type { ListSelectionState } from '../../hooks/types'; -type ListLayout = 'horizontal' | 'vertical' | 'grid'; - export type ListSlots = { root: NonNullable>; }; @@ -12,17 +10,15 @@ export type ListSlots = { * List Props */ export type ListProps = ComponentProps & { - layout?: ListLayout; - focusableItems?: boolean; selectable?: boolean; selectionMode?: SelectionMode; selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; + // eslint-disable-next-line @nx/workspace-consistent-callback-type -- can't change type of existing callback onSelectionChange?: (event: React.SyntheticEvent, data: { selectedItems: SelectionItemId[] }) => void; }; export type ListContextValue = { - focusableItems: boolean; selection?: ListSelectionState; as?: 'div' | 'ol' | 'ul'; }; @@ -34,4 +30,4 @@ export type ListContextValues = { /** * State used in rendering List */ -export type ListState = ComponentState & Required> & ListContextValue; +export type ListState = ComponentState & ListContextValue; diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap index f85fc4cca0f365..b8821d60256775 100644 --- a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -4,7 +4,7 @@ exports[`List renders a default state 1`] = `
      Default List
    diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index 98a46318b403e6..a3fa7eef6d68a3 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -3,7 +3,6 @@ import type { ContextSelector } from '@fluentui/react-context-selector'; import { ListContextValue } from './List.types'; export const listContextDefaultValue: ListContextValue = { - focusableItems: false, selection: undefined, as: undefined, }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 4cd9ec38b8eb0d..130de6fa20b7fc 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -23,8 +23,6 @@ const DEFAULT_ROOT_EL_TYPE = 'ul'; */ export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { const { - layout = 'vertical', - focusableItems = false, selectable = false, selectionMode = 'multiselect', selectedItems, @@ -34,10 +32,9 @@ export const useList_unstable = (props: ListProps, ref: React.Ref = { @@ -13,38 +13,13 @@ const useRootBaseStyles = makeResetStyles({ listStyleType: 'none', }); -/** - * Styles for the root slot - */ -const useStyles = makeStyles({ - rootHorizontal: { - display: 'flex', - }, - - rootGrid: { - display: 'grid', - }, -}); - /** * Apply styling to the List slots based on the state */ export const useListStyles_unstable = (state: ListState): ListState => { const rootStyles = useRootBaseStyles(); - const styles = useStyles(); - - const layoutToStyles = { - ['horizontal']: styles.rootHorizontal, - ['grid']: styles.rootGrid, - ['vertical']: undefined, // no extra styles needed, keep it in for completeness and type safety - }; - state.root.className = mergeClasses( - listClassNames.root, - rootStyles, - layoutToStyles[state.layout], - state.root.className, - ); + state.root.className = mergeClasses(listClassNames.root, rootStyles, state.root.className); return state; }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index e039af7224cae6..f5d5961318ea67 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -28,16 +28,6 @@ function validateProperElementTypes(parentRenderedAs?: 'div' | 'ul' | 'ol', rend } } -function validateNavigableWhenOnClickPresent(focusableItems: boolean, onClick?: React.MouseEventHandler) { - if (process.env.NODE_ENV === 'production') { - return; - } - - if (onClick && !focusableItems) { - throw new Error('ListItem must be focusableItems when onClick is present. Set focusableItems={true} on the List.'); - } -} - /** * Create the state required to render ListItem. * @@ -52,13 +42,14 @@ export const useListItem_unstable = ( ref: React.Ref, ): ListItemState => { const id = useId('listItem'); - const { value = id, onKeyDown, onClick } = props; + const { value = id, onKeyDown, onClick, tabIndex } = props; - const focusableItems = useListContext_unstable(ctx => ctx.focusableItems); const toggleItem = useListContext_unstable(ctx => ctx.selection?.toggleItem); const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); + const focusableItems = isSelectionEnabled || tabIndex === 0 || onClick || onKeyDown; + const parentRenderedAs = useListContext_unstable(ctx => ctx.as); const renderedAs = props.as || DEFAULT_ROOT_EL_TYPE; @@ -66,8 +57,6 @@ export const useListItem_unstable = ( validateProperElementTypes(parentRenderedAs, renderedAs); - validateNavigableWhenOnClickPresent(focusableItems, onClick); - const { findFirstFocusable, findPrevFocusable, findNextFocusable } = useFocusFinders(); const baseIndicatorStyles = useIndicatorStyle(); @@ -179,7 +168,6 @@ export const useListItem_unstable = ( const onCheckboxClick = useEventCallback((ev: React.MouseEvent) => { ev.stopPropagation(); - // toggleItem?.(ev, value); }); const arrowNavigationAttributes = useArrowNavigationGroup({ @@ -194,7 +182,7 @@ export const useListItem_unstable = ( const root = slot.always( getIntrinsicElementProps(DEFAULT_ROOT_EL_TYPE, { ref: useMergedRefs(rootRef, ref) as React.Ref, - tabIndex: focusableItems || isSelectionEnabled ? 0 : undefined, + tabIndex: focusableItems ? 0 : undefined, role: 'listitem', id: String(value), ...(isSelectionEnabled && { diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index 01443fe0181b63..00607cd7263927 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -98,7 +98,6 @@ export const ListActiveElement = () => {
    { console.log(e); diff --git a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md index 0b41f760ad45fb..d30b4b74ba6e40 100644 --- a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md +++ b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md @@ -3,11 +3,10 @@ ### Do - Use `tabIndex={0}` property on the `List` where the items have no actionable elements inside. -- Use `focusableItems` property when the list items have more than one actionable element. +- Use `tabIndex={0}` property on the `ListItem` when the list items should be focusable. - If the ListItem has one action, make sure the component inside of the ListItem is actionable, or use the `ListItemButton` component as your child, which if just preconfigured `Button`. ### Don't -- Don't use `focusableItems` prop if the list items have zero actionable elements, use `tabIndex={0}` on the `List` instead. +- Don't use `tabIndex={0}` prop on the `ListItem` if the list items have zero actionable elements, use `tabIndex={0}` on the `List` instead. This way the list itself is focusable and users can scroll by using up and down arrows after focusing it. -- Don't use `focusableItems` prop if the list items have 1 actionable element, make that element focusable instead. diff --git a/packages/react-components/react-list-preview/stories/List/ListDescription.md b/packages/react-components/react-list-preview/stories/List/ListDescription.md index 2ef04701dd5ca8..b77fd0e337aa6f 100644 --- a/packages/react-components/react-list-preview/stories/List/ListDescription.md +++ b/packages/react-components/react-list-preview/stories/List/ListDescription.md @@ -1,4 +1,4 @@ -The List is a component for rendering sets of items. It supports multiple layouts, namely `vertical` (default), `horizontal` and `grid`. +The List is a component for rendering set of vertically stacked items. There are 2 basic use cases for List, based on the elements it contains: @@ -24,6 +24,6 @@ Making the List Item focusable or accessible by screen readers adds one unnecess If the list needs to support more than single click action, your best bet is to define the buttons inside of the ListItem manually. -You also want to make the list item focusable by adding `focusableItems` property on the List itself. +You also want to make the list item focusable by adding `tabIndex={0}` property on the List itself. This makes sure the list is navigable with up and down arrows and user can enter the group (ListItem) to select the action they want to take with the list. diff --git a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx index ea300eeb3b03f7..cf278f96be1264 100644 --- a/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListGrid.stories.tsx @@ -11,7 +11,7 @@ const useStyles = makeStyles({ export const ListGrid = (props: Partial) => { const classes = useStyles(); return ( - + alert('Asia')}>Asia diff --git a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx index fa81e92e7a3628..89ebf15949c496 100644 --- a/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListHorizontal.stories.tsx @@ -11,7 +11,7 @@ const useStyles = makeStyles({ export const ListHorizontal = () => { const classes = useStyles(); return ( - + Asia Africa Europe diff --git a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx deleted file mode 100644 index d1bd02b6fffca0..00000000000000 --- a/packages/react-components/react-list-preview/stories/List/ListWithMultipleActions.stories.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Button, makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { List, ListItem } from '@fluentui/react-list-preview'; -import * as React from 'react'; -const names = [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', - 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', -]; - -const useStyles = makeStyles({ - wrapper: { - maxHeight: '300px', - overflowY: 'auto', - ...shorthands.padding('2px'), - }, - listItem: { - display: 'flex', - alignItems: 'center', - ...shorthands.gap('4px'), - }, - primaryButton: { - marginRight: 'auto', - }, -}); - -export const ListWithMultipleActions = () => { - const classes = useStyles(); - return ( -
    - - {names.map(name => ( - - - - - - ))} - -
    - ); -}; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx index 804dd3e7919106..bf0133abcb5235 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActions_different_primary.stories.tsx @@ -80,7 +80,7 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = value={props.value} className={mergeClasses(listItemStyles, styles.listItem)} checkmark={{ className: styles.checkmark }} - role="listitem" + role="row" aria-label={value} onClick={e => { // Prevent the default behavior - toggling the selection @@ -88,16 +88,16 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = alert('Main action triggered!'); }} > -
    +
    Presentation Preview
    -
    +
    {props.value} You created 53m ago
    -
    +
    -
    +
    setSelectedItems(data.selectedItems)} > @@ -103,7 +87,7 @@ ListSelectionControlledBasic.parameters = { docs: { description: { story: [ - 'This example is similar to the previous one, but shows how to use the `selectedItems` and `onSelectionChange`', + 'This example shows how to use the `selectedItems` and `onSelectionChange`', 'props to control the selection state.', '', 'This is a basic example how selection can be controlled with a simple array of selected values in a state.', diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx index 06fd6a3d932ca6..f4a316e75aff0a 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -9,13 +9,6 @@ const names = [ 'Israel Rabin', 'Bart Merrill', 'Sonya Farner', - 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', ]; type Item = { @@ -24,7 +17,7 @@ type Item = { avatar: string; }; -const origItems: Item[] = names.map(name => ({ +const items: Item[] = names.map(name => ({ name, id: name, avatar: @@ -67,23 +60,15 @@ const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string export const ListSelectionControlledWithState = () => { const classes = useStyles(); - const [currentIndex, setCurrentIndex] = React.useState(4); - - const items = React.useMemo(() => { - return origItems.slice(0, currentIndex); - }, [currentIndex]); - - const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; const selection = useListSelection({ selectionMode: 'multiselect', - defaultSelectedItems, + defaultSelectedItems: ['Demetra Manwaring', 'Bart Merrill'], }); return (
    - -
    -
    - - - -
    - - ); -}; - -export const MultipleActionsNoPrimaryNoSelection = (props: Partial) => { - const classes = useStyles(); - - return ( - - - - - - - - - - - - ); -}; - -MultipleActionsNoPrimaryNoSelection.parameters = { - docs: { - description: { - story: [ - "Base item with multiple actions. Doesn't have a primary action on the list item, doesn't have selection.", - '', - 'The roles in this example are `list` and `listitem`, inherited from the ul/li tags used. List items themselves are focusable.', - ].join('\n'), - }, - }, -}; diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index e535cc633ab86d..0cf6bd4862c127 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -1,5 +1,5 @@ import { makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { List, ListItem, ListItemButton } from '@fluentui/react-list-preview'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; const names = [ 'Melda Bevel', @@ -8,13 +8,6 @@ const names = [ 'Israel Rabin', 'Bart Merrill', 'Sonya Farner', - 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', ]; const useStyles = makeStyles({ @@ -64,7 +57,7 @@ SingleAction.parameters = { docs: { description: { story: [ - 'When the list item has a primary action on it, you can pass the `onClick` prop to the `ListItem` component.', + 'When the list item should have a primary action on it, you can pass the `onClick` prop to the `ListItem` component.', 'This callback will also be automatically called when the user presses the Enter key on the list item.', '', 'Since we are directly attaching the `onClick` handler on the `ListItem` itself, it is important to', diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction_selection.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx similarity index 70% rename from packages/react-components/react-list-preview/stories/List/SingleAction_selection.stories.tsx rename to packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx index 11180f89a33c6d..44fb33f0bdf53a 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction_selection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx @@ -1,4 +1,4 @@ -import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; @@ -9,13 +9,6 @@ const names = [ 'Israel Rabin', 'Bart Merrill', 'Sonya Farner', - 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', ]; type Item = { @@ -24,7 +17,7 @@ type Item = { avatar: string; }; -const origItems: Item[] = names.map(name => ({ +const items: Item[] = names.map(name => ({ name, id: name, avatar: @@ -37,14 +30,6 @@ const useStyles = makeStyles({ overflowY: 'auto', ...shorthands.padding('2px'), }, - buttonControls: { - display: 'flex', - columnGap: '8px', - marginBottom: '16px', - }, - button: { - ...shorthands.padding(0), - }, }); // Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. @@ -67,11 +52,6 @@ const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string export const SingleActionSelection = () => { const classes = useStyles(); - const [currentIndex, setCurrentIndex] = React.useState(4); - - const items = React.useMemo(() => { - return origItems.slice(0, currentIndex); - }, [currentIndex]); const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; @@ -79,10 +59,6 @@ export const SingleActionSelection = () => { return (
    -
    - -
    - { }; SingleActionSelection.parameters = { + title: 'aaa', docs: { + title: 'DD', description: { + title: 'ddd', story: [ 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', '', 'You can pass `selectable` prop inside of the List component to get support for selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state. While we are saving the results of `onSelectionChange`, it is purely for keeping track of the selection state and is not used to control the selection.', '', - "You can see that the default selection contains an object, which is not yet rendered in the list. Try adding a new item and see that it's selected by default. This is to demonstrate that you can decouple your selection state from ", - 'your list items and even store and retrieve them separately.', + 'The items can be toggled by clicking on them, clicking on the checkbox or pressing a Spacebar when the item is focused.', '', - 'Also this example only has one action in the list item, and its for toggling the selection. The roles for this one are listbox and option, and the options themselves are focusable.', + 'Also this example only has one action in the list item, and its for toggling the selection. The roles for this one are listbox and option.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx new file mode 100644 index 00000000000000..c929b7e04c6fb8 --- /dev/null +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx @@ -0,0 +1,100 @@ +import { makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; + +import * as React from 'react'; +const names = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', +]; + +type Item = { + name: string; + id: string; + avatar: string; +}; + +const items: Item[] = names.map(name => ({ + name, + id: name, + avatar: + 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', +})); + +const useStyles = makeStyles({ + wrapper: { + maxHeight: '300px', + overflowY: 'auto', + ...shorthands.padding('2px'), + }, + button: { + ...shorthands.padding(0), + }, +}); + +// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. +const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string }) => { + const onClick = React.useCallback( + event => { + // This prevents the change in selection on click/Enter + event.preventDefault(); + alert('Hi, ' + name); + }, + [name], + ); + return ( + + + + ); +}); + +export const SingleActionSelectionDifferentPrimary = () => { + const classes = useStyles(); + + const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; + + const [selectedItems, setSelectedItems] = React.useState(defaultSelectedItems); + + return ( +
    + setSelectedItems(data.selectedItems)} + > + {items.map(({ name, avatar }) => ( + + ))} + +
    Selected people: {selectedItems.join(', ')}
    +
    + ); +}; + +SingleActionSelectionDifferentPrimary.parameters = { + docs: { + description: { + story: [ + 'This example is similar to the previous one, but it implements a custom `onClick` prop on the `ListItem`,', + 'allowing us to trigger a different action than the selection when the user clicks on the list item or ', + 'presses Enter. This is useful when you want to have a primary action on the list item, but still want ', + 'to allow the user to select it.', + '', + 'The selection can still be toggled by clicking on the checkbox or pressing `Space` when the item is focused.', + ].join('\n'), + }, + }, +}; diff --git a/packages/react-components/react-list-preview/stories/List/Untitled-2 b/packages/react-components/react-list-preview/stories/List/Untitled-2 deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx index 4dc0903dff1ab1..c777a0fe81f05e 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx @@ -216,8 +216,14 @@ export const VirtualizedListWithActionableItems = () => { outerElementType={CountriesList} > {({ index, style, data }) => ( - - alert(data[index])}>{data[index]} + alert(data[index])} + > + {data[index]} )} @@ -227,11 +233,7 @@ export const VirtualizedListWithActionableItems = () => { VirtualizedListWithActionableItems.parameters = { docs: { description: { - story: [ - 'Virtualized list can also be used with interactive elements. Note that the list itself is not focusable', - 'anymore, since each list item is focusable. To make sure list items are focusable, the `ListItemButton` component', - 'is used.', - ].join('\n'), + story: ['Virtualized list can also be used with interactive elements.'].join('\n'), }, }, }; diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index 865981e8485d88..e99fda11c56cb5 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -4,14 +4,12 @@ import descriptionMd from './ListDescription.md'; import bestPracticesMd from './ListBestPractices.md'; export { Default } from './ListDefault.stories'; -// export { ListHorizontal } from './ListHorizontal.stories'; -// export { ListGrid } from './ListGrid.stories'; export { SingleAction } from './SingleAction.stories'; -export { SingleActionSelection } from './SingleAction_selection.stories'; -export { MultipleActionsNoPrimaryNoSelection } from './MultipleActions_no_primary_no_selection.stories'; -export { MultipleActionsNoSelectionWithPrimary } from './MultipleActions_no_selection_with_primary.stories'; -export { MultipleActionsPrimarySelection } from './MultipleActions_primary_selection.stories'; -export { MultipleActionsDifferentPrimary } from './MultipleActions_different_primary.stories'; +export { SingleActionSelection } from './SingleActionSelection.stories'; +export { SingleActionSelectionDifferentPrimary } from './SingleActionSelectionDifferentPrimary.stories'; +export { MultipleActionsWithPrimary } from './MultipleActionsWithPrimary.stories'; +export { MultipleActionsSelection } from './MultipleActionsSelection.stories'; +export { MultipleActionsDifferentPrimary } from './MultipleActionsDifferentPrimary.stories'; export { ListSelectionControlledBasic } from './ListSelectionControlledBasic.stories'; export { ListSelectionControlledWithState } from './ListSelectionControlledWithState.stories'; export { VirtualizedList } from './VirtualizedList.stories'; From c7576ea1a747074a63a8d3db2309bbbfb26883a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 19 Feb 2024 16:46:11 +0100 Subject: [PATCH 084/136] Remove ListItemButton completely --- .../etc/react-components.api.md | 6 ---- .../react-components/src/index.ts | 2 -- .../etc/react-list-preview.api.md | 8 ----- .../react-list-preview/src/ListItemButton.ts | 1 - .../__snapshots__/ListItem.test.tsx.snap | 2 +- .../ListItemButton/ListItemButton.test.tsx | 20 ------------- .../ListItemButton/ListItemButton.tsx | 15 ---------- .../ListItemButton.test.tsx.snap | 12 -------- .../src/components/ListItemButton/index.ts | 2 -- .../useListItemButtonStyles.styles.ts | 30 ------------------- .../react-list-preview/src/index.ts | 1 - .../stories/List/ListBestPractices.md | 1 - ...ualizedListWithActionableItems.stories.tsx | 2 +- .../react-provider/etc/react-provider.api.md | 1 - .../etc/react-shared-contexts.api.md | 3 -- .../CustomStyleHooksContext.ts | 1 - 16 files changed, 2 insertions(+), 105 deletions(-) delete mode 100644 packages/react-components/react-list-preview/src/ListItemButton.ts delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/index.ts delete mode 100644 packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts diff --git a/packages/react-components/react-components/etc/react-components.api.md b/packages/react-components/react-components/etc/react-components.api.md index c3ad467eb80387..c147d794ccb50a 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -427,7 +427,6 @@ import { ListboxSlots } from '@fluentui/react-combobox'; import { ListboxState } from '@fluentui/react-combobox'; import { listClassNames } from '@fluentui/react-list-preview'; import { ListItem } from '@fluentui/react-list-preview'; -import { ListItemButton } from '@fluentui/react-list-preview'; import { listItemClassNames } from '@fluentui/react-list-preview'; import { ListItemProps } from '@fluentui/react-list-preview'; import { ListItemSlots } from '@fluentui/react-list-preview'; @@ -1256,7 +1255,6 @@ import { useListbox_unstable } from '@fluentui/react-combobox'; import { useListboxContextValues } from '@fluentui/react-combobox'; import { useListboxStyles_unstable } from '@fluentui/react-combobox'; import { useListItem_unstable } from '@fluentui/react-list-preview'; -import { useListItemButtonStyles_unstable } from '@fluentui/react-list-preview'; import { useListItemStyles_unstable } from '@fluentui/react-list-preview'; import { useListSelection } from '@fluentui/react-list-preview'; import { useListStyles_unstable } from '@fluentui/react-list-preview'; @@ -2296,8 +2294,6 @@ export { listClassNames } export { ListItem } -export { ListItemButton } - export { listItemClassNames } export { ListItemProps } @@ -3954,8 +3950,6 @@ export { useListboxStyles_unstable } export { useListItem_unstable } -export { useListItemButtonStyles_unstable } - export { useListItemStyles_unstable } export { useListSelection } diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index 1e1830e2e6c312..170812f935e28f 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -1584,8 +1584,6 @@ export { renderListItem_unstable, useListItemStyles_unstable, useListItem_unstable, - ListItemButton, - useListItemButtonStyles_unstable, useListSelection, } from '@fluentui/react-list-preview'; diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 35480bf51dd0b1..9b64c4829a5ac0 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -6,8 +6,6 @@ /// -import { ButtonProps } from '@fluentui/react-button'; -import { ButtonState } from '@fluentui/react-button'; import { Checkbox } from '@fluentui/react-checkbox'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; @@ -28,9 +26,6 @@ export const listClassNames: SlotClassNames; // @public (undocumented) export const ListItem: ForwardRefComponent; -// @public (undocumented) -export const ListItemButton: ForwardRefComponent; - // @public (undocumented) export const listItemClassNames: SlotClassNames; @@ -82,9 +77,6 @@ export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListItemState; -// @public -export const useListItemButtonStyles_unstable: (state: ButtonState) => ButtonState; - // @public export const useListItemStyles_unstable: (state: ListItemState) => ListItemState; diff --git a/packages/react-components/react-list-preview/src/ListItemButton.ts b/packages/react-components/react-list-preview/src/ListItemButton.ts deleted file mode 100644 index 3737c764d3713b..00000000000000 --- a/packages/react-components/react-list-preview/src/ListItemButton.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components/ListItemButton/index'; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap index a9644caf36164c..d94ce328b00f41 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/ListItem/__snapshots__/ListItem.test.tsx.snap @@ -4,7 +4,7 @@ exports[`ListItem renders a default state 1`] = `
  • diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx deleted file mode 100644 index 41d75873d76eef..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { isConformant } from '../../testing/isConformant'; -import { ListItemButton } from './ListItemButton'; -import { ButtonProps } from '../../../../react-button/src/Button'; - -describe('ListItemButton', () => { - isConformant({ - Component: ListItemButton as React.FunctionComponent, - displayName: 'ListItemButton', - disabledTests: ['component-has-static-classnames-object'], - }); - - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - - it('renders a default state', () => { - const result = render(Default ListItemButton); - expect(result.container).toMatchSnapshot(); - }); -}); diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx b/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx deleted file mode 100644 index 67ec32607ef57d..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/ListItemButton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; -import { useListItemButtonStyles_unstable } from './useListItemButtonStyles.styles'; -import { ButtonProps, renderButton_unstable, useButton_unstable } from '@fluentui/react-button'; - -export const ListItemButton: ForwardRefComponent = React.forwardRef((props, ref) => { - const state = useButton_unstable({ appearance: 'transparent', ...props }, ref); - - useListItemButtonStyles_unstable(state); - useCustomStyleHook_unstable('useListItemButtonStyles_unstable')(state); - return renderButton_unstable(state); -}); - -ListItemButton.displayName = 'ListItemButton'; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap b/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap deleted file mode 100644 index d039935d43acd5..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/__snapshots__/ListItemButton.test.tsx.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ListItemButton renders a default state 1`] = ` -
    - -
    -`; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts deleted file mode 100644 index f2b857ac4363d5..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ListItemButton'; -export * from './useListItemButtonStyles.styles'; diff --git a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts deleted file mode 100644 index 06e9ad87d3fa7f..00000000000000 --- a/packages/react-components/react-list-preview/src/components/ListItemButton/useListItemButtonStyles.styles.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ButtonState, useButtonStyles_unstable } from '@fluentui/react-button'; -import { makeResetStyles, mergeClasses } from '@griffel/react'; - -/** - * Styles for the root slot - */ -const useRootBaseStyles = makeResetStyles({ - display: 'block', - width: '100%', - textAlign: 'left', - padding: 0, - '&:hover': { - color: 'unset', - }, - '&:hover:active': { - color: 'unset', - }, -}); - -/** - * Apply styling to the ListItemButton slots based on the state - */ -export const useListItemButtonStyles_unstable = (state: ButtonState): ButtonState => { - useButtonStyles_unstable(state); - - const baseStyles = useRootBaseStyles(); - state.root.className = mergeClasses(state.root.className, baseStyles); - - return state; -}; diff --git a/packages/react-components/react-list-preview/src/index.ts b/packages/react-components/react-list-preview/src/index.ts index ad430893364c47..827896be95a61b 100644 --- a/packages/react-components/react-list-preview/src/index.ts +++ b/packages/react-components/react-list-preview/src/index.ts @@ -9,6 +9,5 @@ export { useListItem_unstable, } from './ListItem'; export type { ListItemProps, ListItemSlots, ListItemState } from './ListItem'; -export { ListItemButton, useListItemButtonStyles_unstable } from './ListItemButton'; export { useListSelection } from './hooks'; diff --git a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md index d30b4b74ba6e40..ddd814d7f12cc1 100644 --- a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md +++ b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md @@ -4,7 +4,6 @@ - Use `tabIndex={0}` property on the `List` where the items have no actionable elements inside. - Use `tabIndex={0}` property on the `ListItem` when the list items should be focusable. -- If the ListItem has one action, make sure the component inside of the ListItem is actionable, or use the `ListItemButton` component as your child, which if just preconfigured `Button`. ### Don't diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx index c777a0fe81f05e..b0930800dc2f90 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { FixedSizeList } from 'react-window'; -import { List, ListItem, ListItemButton } from '@fluentui/react-list-preview'; +import { List, ListItem } from '@fluentui/react-list-preview'; const countries = [ 'Afghanistan', diff --git a/packages/react-components/react-provider/etc/react-provider.api.md b/packages/react-components/react-provider/etc/react-provider.api.md index 801ce9f7169177..584391a39a1cbe 100644 --- a/packages/react-components/react-provider/etc/react-provider.api.md +++ b/packages/react-components/react-provider/etc/react-provider.api.md @@ -54,7 +54,6 @@ export const FluentProvider: React_2.ForwardRefExoticComponent void; useListStyles_unstable: (state: unknown) => void; useListItemStyles_unstable: (state: unknown) => void; - useListItemButtonStyles_unstable: (state: unknown) => void; useOptionStyles_unstable: (state: unknown) => void; useOptionGroupStyles_unstable: (state: unknown) => void; useDividerStyles_unstable: (state: unknown) => void; diff --git a/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md b/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md index 16886126374958..70b27e565f6e21 100644 --- a/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md +++ b/packages/react-components/react-shared-contexts/etc/react-shared-contexts.api.md @@ -61,7 +61,6 @@ export const CustomStyleHooksContext_unstable: React_2.Context Date: Tue, 20 Feb 2024 15:57:56 +0100 Subject: [PATCH 085/136] fix label for single action list --- .../react-list-preview/stories/List/SingleAction.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index 0cf6bd4862c127..2ee92d7db08308 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -33,7 +33,7 @@ export const SingleAction = () => { id={`id_${name}`} key={name} role="listitem" - aria-label={name} + aria-label={`${name}, available`} aria-roledescription="button" //needs to be a translated string! onClick={() => alert(name)} > From 2e75a91df1291e74bf06a3700cdcfd05646876f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 21 Feb 2024 16:19:05 +0100 Subject: [PATCH 086/136] cleaned up examples, used more descriptive alerts --- .../src/components/ListItem/useListItem.tsx | 23 +++++--- .../stories/List/ListBestPractices.md | 1 + ...ultipleActionsDifferentPrimary.stories.tsx | 47 ++++++---------- .../List/MultipleActionsSelection.stories.tsx | 50 ++++++----------- .../MultipleActionsWithPrimary.stories.tsx | 53 ++++++------------- .../stories/List/SingleAction.stories.tsx | 4 +- ...ctionSelectionDifferentPrimary.stories.tsx | 2 +- 7 files changed, 68 insertions(+), 112 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index d23ea3870258c0..f52e71a62b5493 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -57,11 +57,14 @@ export const useListItem_unstable = ( validateProperElementTypes(parentRenderedAs, renderedAs); - const { findFirstFocusable, findPrevFocusable, findNextFocusable } = useFocusFinders(); + const { findFirstFocusable, findAllFocusable } = useFocusFinders(); const baseIndicatorStyles = useIndicatorStyle(); - const focusableGroupAttrs = useFocusableGroup({ ignoreDefaultKeydown: { Enter: true }, tabBehavior: 'limited' }); + const focusableGroupAttrs = useFocusableGroup({ + ignoreDefaultKeydown: { Enter: true }, + tabBehavior: 'limited-trap-focus', + }); const handleClick: React.MouseEventHandler = useEventCallback(e => { onClick?.(e); @@ -105,18 +108,26 @@ export const useListItem_unstable = ( } if (['ArrowUp', 'ArrowDown'].includes(e.key)) { - // Prevents scrolling for ArrowUp/ArowDown + if (!e.currentTarget.parentElement) { + return; + } + // Prevents scrolling for ArrowUp/ArrowDown e.preventDefault(); - const prevItem = findPrevFocusable(e.currentTarget as HTMLElement); - const nextItem = findNextFocusable(e.currentTarget as HTMLElement); + const focusableEls = findAllFocusable( + e.currentTarget.parentElement, + el => el === e.currentTarget || !e.currentTarget.contains(el), + ); + const currentIndex = focusableEls.indexOf(e.currentTarget as HTMLElement); if (e.key === 'ArrowUp') { + const prevItem = focusableEls[currentIndex - 1]; prevItem && (e.currentTarget as HTMLElement).previousSibling === prevItem ? prevItem.focus() : pressEscape(e.target as HTMLElement); } - if (e.key === 'ArrowDown' && nextItem) { + if (e.key === 'ArrowDown') { + const nextItem = focusableEls[currentIndex + 1]; nextItem && (e.currentTarget as HTMLElement).nextSibling === nextItem ? nextItem.focus() : pressEscape(e.target as HTMLElement); diff --git a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md index ddd814d7f12cc1..4852d4e8364191 100644 --- a/packages/react-components/react-list-preview/stories/List/ListBestPractices.md +++ b/packages/react-components/react-list-preview/stories/List/ListBestPractices.md @@ -4,6 +4,7 @@ - Use `tabIndex={0}` property on the `List` where the items have no actionable elements inside. - Use `tabIndex={0}` property on the `ListItem` when the list items should be focusable. +- Use `aria-label` property on the `ListItem` for custom screen reader label. ### Don't diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index 2ea14e2432b241..aaa8aa29415d23 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -1,7 +1,6 @@ import { Button, Caption1, - CardProps, makeResetStyles, makeStyles, Menu, @@ -37,31 +36,17 @@ const useStyles = makeStyles({ ...shorthands.gap('16px'), maxWidth: '300px', }, - listItem: { display: 'grid', ...shorthands.padding('8px') }, - + listItem: { + display: 'grid', + ...shorthands.padding('8px'), + }, caption: { color: tokens.colorNeutralForeground3, }, - image: { maxWidth: '100%', ...shorthands.borderRadius('5px'), }, - - grayBackground: { - backgroundColor: tokens.colorNeutralBackground3, - }, - - logoBadge: { - ...shorthands.padding('5px'), - ...shorthands.borderRadius(tokens.borderRadiusSmall), - backgroundColor: '#FFF', - boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', - }, - actionsWrapper: { - display: 'flex', - ...shorthands.gap('8px'), - }, checkmark: { position: 'absolute', left: '10px', @@ -70,7 +55,7 @@ const useStyles = makeStyles({ }, }); -const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { +const CardExample = (props: { title: string; value: string; selected?: boolean }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; @@ -85,7 +70,7 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = onClick={e => { // Prevent the default behavior - toggling the selection e.preventDefault(); - alert('Main action triggered!'); + alert('Primary action triggered!'); }} >
    @@ -93,7 +78,7 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) =
    - {props.value} + {props.title} You created 53m ago
    @@ -174,15 +159,15 @@ export const MultipleActionsDifferentPrimary = (props: Partial) => { onSelectionChange={(e, data) => setSelectedItems(data.selectedItems)} selectionMode="multiselect" > - - - - - - - - - + + + + + + + + + ); }; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx index 26ff2da79599aa..b0412771b99713 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx @@ -1,7 +1,6 @@ import { Button, Caption1, - CardProps, makeResetStyles, makeStyles, Menu, @@ -37,31 +36,17 @@ const useStyles = makeStyles({ ...shorthands.gap('16px'), maxWidth: '300px', }, - listItem: { display: 'grid', ...shorthands.padding('8px') }, - + listItem: { + display: 'grid', + ...shorthands.padding('8px'), + }, caption: { color: tokens.colorNeutralForeground3, }, - image: { maxWidth: '100%', ...shorthands.borderRadius('5px'), }, - - grayBackground: { - backgroundColor: tokens.colorNeutralBackground3, - }, - - logoBadge: { - ...shorthands.padding('5px'), - ...shorthands.borderRadius(tokens.borderRadiusSmall), - backgroundColor: '#FFF', - boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', - }, - actionsWrapper: { - display: 'flex', - ...shorthands.gap('8px'), - }, checkmark: { position: 'absolute', left: '10px', @@ -70,7 +55,7 @@ const useStyles = makeStyles({ }, }); -const CardExample = (props: CardProps & { value: string; selected?: boolean }) => { +const CardExample = (props: { title: string; value: string; selected?: boolean }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; @@ -82,18 +67,13 @@ const CardExample = (props: CardProps & { value: string; selected?: boolean }) = checkmark={{ className: styles.checkmark }} role="row" aria-label={value} - onClick={e => { - // Prevent the default behavior - toggling the selection - // e.preventDefault(); - // alert('Main action triggered!'); - }} >
    Presentation Preview
    - {props.value} + {props.title} You created 53m ago
    @@ -174,15 +154,15 @@ export const MultipleActionsSelection = (props: Partial) => { onSelectionChange={(e, data) => setSelectedItems(data.selectedItems)} selectionMode="multiselect" > - - - - - - - - - + + + + + + + + + ); }; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx index c25398e3fda71a..5827981e76cd21 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx @@ -1,7 +1,6 @@ import { Button, Caption1, - CardProps, makeResetStyles, makeStyles, Menu, @@ -37,40 +36,20 @@ const useStyles = makeStyles({ ...shorthands.gap('16px'), maxWidth: '300px', }, - listItem: { display: 'grid', ...shorthands.padding('8px') }, - + listItem: { + display: 'grid', + ...shorthands.padding('8px'), + }, caption: { color: tokens.colorNeutralForeground3, }, - image: { maxWidth: '100%', ...shorthands.borderRadius('5px'), }, - - grayBackground: { - backgroundColor: tokens.colorNeutralBackground3, - }, - - logoBadge: { - ...shorthands.padding('5px'), - ...shorthands.borderRadius(tokens.borderRadiusSmall), - backgroundColor: '#FFF', - boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12)', - }, - actionsWrapper: { - display: 'flex', - ...shorthands.gap('8px'), - }, - checkmark: { - position: 'absolute', - left: '10px', - top: '10px', - zIndex: 1, - }, }); -const CardExample = (props: CardProps & { value: string }) => { +const CardExample = (props: { title: string; value: string }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; @@ -80,14 +59,14 @@ const CardExample = (props: CardProps & { value: string }) => { value={props.value} className={mergeClasses(listItemStyles, styles.listItem)} aria-label={value} - onClick={() => alert('Hello!')} + onClick={() => alert('Primary action triggered!')} >
    Presentation Preview
    - {props.value} + {props.title} You created 53m ago
    @@ -160,15 +139,15 @@ export const MultipleActionsWithPrimary = (props: Partial) => { return ( - - - - - - - - - + + + + + + + + + ); }; diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index 2ee92d7db08308..3dca9cadf6942e 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -35,7 +35,7 @@ export const SingleAction = () => { role="listitem" aria-label={`${name}, available`} aria-roledescription="button" //needs to be a translated string! - onClick={() => alert(name)} + onClick={() => alert(`Triggered action on ${name}`)} > { // This prevents the change in selection on click/Enter event.preventDefault(); - alert('Hi, ' + name); + alert(`Triggered action on ${name}`); }, [name], ); From 41cba52c7dfa7a9fa74d9bdf2ea8b316ab848bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 21 Feb 2024 17:26:25 +0100 Subject: [PATCH 087/136] toggle item on Enter if default hasn't been prevented --- .../src/components/ListItem/useListItem.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index f52e71a62b5493..aa0f1d75098d3f 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -73,11 +73,7 @@ export const useListItem_unstable = ( return; } - // Only toggle if this was actually a pointer click, not an event triggered by Enter - // key down (handled below). - if (e.isTrusted) { - toggleItem?.(e, value); - } + toggleItem?.(e, value); }); const pressEscape = useEventCallback((target: HTMLElement) => { From e0c875dd7eeeb75c7ec4c116fca15a605e6a7e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 21 Feb 2024 17:58:13 +0100 Subject: [PATCH 088/136] update the activeElement story to change selection n focus --- .../src/components/ListItem/useListItem.tsx | 1 + .../List/ListActiveElement.stories.tsx | 89 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index aa0f1d75098d3f..d376590fe55ff2 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -198,6 +198,7 @@ export const useListItem_unstable = ( }), ...tabsterAttributes, ...props, + 'data-value': String(value), onKeyDown: handleKeyDown, onClick: handleClick, }), diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index 00607cd7263927..4530456f92ea2c 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -20,12 +20,6 @@ const names = [ 'Bart Merrill', 'Sonya Farner', 'Kristan Cable', - 'Cythia Ignacio', - 'Gia Laura', - 'Dewayne Oda', - 'Lang Yeldell', - 'Kathlyn Brewer', - 'Nia Woodworth', ]; type Item = { @@ -61,58 +55,63 @@ const useStyles = makeStyles({ }); // Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. -const MyListItem = React.memo(({ name, avatar, ...rest }: { name: string; avatar: string; className?: string }) => { - const styles = useStyles(); - return ( - - - + + + + List Item 2 + + + + List Item 3 + + + , + ); +}; + +describe('List', () => { + describe('keyboard navigation', () => { + describe('Simple list with a single action', () => { + it('Up/Down arrow keys work', () => { + mountSimpleList(); + testSequence([ + 'focused:list-item-1', + 'DownArrow', + 'focused:list-item-2', + 'DownArrow', + 'focused:list-item-3', + 'DownArrow', + 'focused:list-item-3', + 'UpArrow', + 'focused:list-item-2', + 'UpArrow', + 'focused:list-item-1', + 'UpArrow', + 'focused:list-item-1', + ]); + }); + + it('Home/End arrow keys work', () => { + mountSimpleList(); + testSequence(['focused:list-item-1', 'End', 'focused:list-item-3', 'Home', 'focused:list-item-1']); + }); + + it('PgUp/PgDown arrow keys work', () => { + mountSimpleList(); + testSequence(['focused:list-item-1', 'PageDown', 'focused:list-item-3', 'PageUp', 'focused:list-item-1']); + }); + }); + describe('List with multiple actions', () => { + it('Up/Down arrows work', () => { + mountListWithSecondaryActions(); + testSequence([ + 'focused:list-item-1', + 'DownArrow', + 'focused:list-item-2', + 'DownArrow', + 'focused:list-item-3', + 'DownArrow', + 'focused:list-item-3', + 'UpArrow', + 'focused:list-item-2', + 'UpArrow', + 'focused:list-item-1', + 'UpArrow', + 'focused:list-item-1', + ]); + }); + + it('Home/End arrow keys work', () => { + mountListWithSecondaryActions(); + testSequence(['focused:list-item-1', 'End', 'focused:list-item-3', 'Home', 'focused:list-item-1']); + }); + + it('PgUp/PgDown arrow keys work', () => { + mountListWithSecondaryActions(); + testSequence(['focused:list-item-1', 'PageDown', 'focused:list-item-3', 'PageUp', 'focused:list-item-1']); + }); + + it('Left/Right arrow key moves focus on the first secondary action', () => { + mountListWithSecondaryActions(); + testSequence([ + 'focused:list-item-1', + 'RightArrow', + 'focused:list-item-1-button-1', + 'RightArrow', + 'focused:list-item-1-button-2', + 'RightArrow', + 'focused:list-item-1-button-2', + 'LeftArrow', + 'focused:list-item-1-button-1', + 'LeftArrow', + 'focused:list-item-1', + ]); + }); + + it('Escape moves out of the secondary and focuses on the same row', () => { + mountListWithSecondaryActions(); + testSequence([ + 'focused:list-item-1', + 'RightArrow', + 'focused:list-item-1-button-1', + 'RightArrow', + 'focused:list-item-1-button-2', + 'Esc', + 'focused:list-item-1', + ]); + }); + + it('Arrow up/down on the secondary action focuses the item above/below', () => { + mountListWithSecondaryActions(); + testSequence([ + 'focused:list-item-1', + 'DownArrow', + 'RightArrow', + 'focused:list-item-2-button-1', + 'UpArrow', + 'focused:list-item-1', + 'DownArrow', + 'RightArrow', + 'focused:list-item-2-button-1', + 'DownArrow', + 'focused:list-item-3', + ]); + }); + + it('Ignored keys are ignored', () => { + mountListWithSecondaryActions(); + testSequence(['RightArrow', 'focused:list-item-1-button-1', 'Enter', ' ', 'focused:list-item-1-button-1']); + }); + }); + }); +}); diff --git a/packages/react-components/react-list-preview/tsconfig.cy.json b/packages/react-components/react-list-preview/tsconfig.cy.json new file mode 100644 index 00000000000000..93a140885851da --- /dev/null +++ b/packages/react-components/react-list-preview/tsconfig.cy.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "isolatedModules": false, + "types": ["node", "cypress", "cypress-storybook/cypress", "cypress-real-events"], + "lib": ["ES2019", "dom"] + }, + "include": ["**/*.cy.ts", "**/*.cy.tsx"] +} diff --git a/packages/react-components/react-list-preview/tsconfig.json b/packages/react-components/react-list-preview/tsconfig.json index 1941a041d46c19..1317f81620ca5e 100644 --- a/packages/react-components/react-list-preview/tsconfig.json +++ b/packages/react-components/react-list-preview/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "./.storybook/tsconfig.json" + }, + { + "path": "./tsconfig.cy.json" } ] } diff --git a/packages/react-components/react-list-preview/tsconfig.lib.json b/packages/react-components/react-list-preview/tsconfig.lib.json index 6f90cf95c005bd..e17f808c039339 100644 --- a/packages/react-components/react-list-preview/tsconfig.lib.json +++ b/packages/react-components/react-list-preview/tsconfig.lib.json @@ -16,7 +16,9 @@ "**/*.test.ts", "**/*.test.tsx", "**/*.stories.ts", - "**/*.stories.tsx" + "**/*.stories.tsx", + "**/*.cy.ts", + "**/*.cy.tsx" ], "include": ["./src/**/*.ts", "./src/**/*.tsx"] } From e2381d7e1e2c30b5f58d5470988727a48985bb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 28 Feb 2024 10:49:11 +0100 Subject: [PATCH 095/136] remove selectable prop, as it is redundant --- .../react-list-preview/docs/MIGRATION.md | 49 +++++++++---------- .../etc/react-list-preview.api.md | 1 - .../src/components/List/List.test.tsx | 14 +++--- .../src/components/List/List.types.ts | 1 - .../src/components/List/useList.ts | 15 ++---- .../List/ListActiveElement.stories.tsx | 2 +- .../ListSelectionControlledBasic.stories.tsx | 6 +-- ...stSelectionControlledWithState.stories.tsx | 2 +- ...ultipleActionsDifferentPrimary.stories.tsx | 2 +- .../List/MultipleActionsSelection.stories.tsx | 3 +- .../List/SingleActionSelection.stories.tsx | 4 +- ...ctionSelectionDifferentPrimary.stories.tsx | 2 +- 12 files changed, 43 insertions(+), 58 deletions(-) diff --git a/packages/react-components/react-list-preview/docs/MIGRATION.md b/packages/react-components/react-list-preview/docs/MIGRATION.md index 9a3c710374fc4e..a8798fbbd5a6eb 100644 --- a/packages/react-components/react-list-preview/docs/MIGRATION.md +++ b/packages/react-components/react-list-preview/docs/MIGRATION.md @@ -71,7 +71,6 @@ Most of the v8 props are for it's virtualization functionality. Since the v9 `Li | `version` | N/A | | - | `defaultSelectedItems` | | - | `onSelectionChange` | -| - | `selectable` | | - | `selectionMode` | ## Migration from v0 @@ -165,7 +164,7 @@ We recommend using a component like `Persona` where possible, or creating a cust | `navigable` | `tabIndex={0}` if the `ListItem`, or automatic when `onClick` or `onKeyDown` is present | | `onSelectedIndexChange` | `onSelectionChange` | | `ref` | `ref` | -| `selectable` | `selectable` | +| `selectable` | use `selectionMode` of value `single` or `multiselect` | | `selectedIndex` | only in controlled mode, use `selection` state; see [example](https://react.fluentui.dev/?path=/story/preview-components-list--list-selection-controlled). | | `styles` | use slots in combination with `className` | | `truncateContent` | N/A - the `List` is not concerned about it's content | @@ -175,29 +174,29 @@ We recommend using a component like `Persona` where possible, or creating a cust #### ListItem -| v0 ListItem | v9 ListItem | -| ----------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `accessibility` | N/A | -| `as` | `as` | -| `className` | `className` | -| `content` | N/A - use children | -| `contentMedia` | N/A - use children | -| `debug` | N/A | -| `design` | N/A | -| `endMedia` | N/A - use children | -| `header` | N/A - use children | -| `headerMedia` | N/A - use children | -| `important` | N/A | -| `index` | N/A | -| `media` | N/A - use children | -| `navigable` | N/A - use `tabIndex={0}` | -| `onClick` | `onClick` | -| `ref` | ref | -| `selectable` | N/A - use `List` property `selectable` for uncontrolled selection, or use `useListSelection` hook to control the state | -| `selected` | N/A - the selection state takes care of this automatically | -| `styles` | N/A - use `className` for any slot | -| `truncateContent` | N/A - the `List` is not concerned about it's content | -| `truncateHeader` | N/A - the `List` is not concerned about it's content | +| v0 ListItem | v9 ListItem | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `accessibility` | N/A | +| `as` | `as` | +| `className` | `className` | +| `content` | N/A - use children | +| `contentMedia` | N/A - use children | +| `debug` | N/A | +| `design` | N/A | +| `endMedia` | N/A - use children | +| `header` | N/A - use children | +| `headerMedia` | N/A - use children | +| `important` | N/A | +| `index` | N/A | +| `media` | N/A - use children | +| `navigable` | N/A - use `tabIndex={0}` | +| `onClick` | `onClick` | +| `ref` | ref | +| `selectable` | N/A - use `List` property `selectionMode` for uncontrolled selection, or use `useListSelection` hook to control the state | +| `selected` | N/A - the selection state takes care of this automatically | +| `styles` | N/A - use `className` for any slot | +| `truncateContent` | N/A - the `List` is not concerned about it's content | +| `truncateHeader` | N/A - the `List` is not concerned about it's content | #### Other diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 9b64c4829a5ac0..0f68dc3478e1d1 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -48,7 +48,6 @@ export type ListItemState = ComponentState & { // @public export type ListProps = ComponentProps & { - selectable?: boolean; selectionMode?: SelectionMode_2; selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 3c93949d23483b..517e8f61df8c27 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -52,7 +52,7 @@ describe('List', () => { it('selectable - should have listbox/option roles', () => { const result = render( - + First ListItem Second ListItem , @@ -68,7 +68,7 @@ describe('List', () => { describe('selection', () => { it('Single mode should unselect previous', () => { const result = render( - + First ListItem Second ListItem , @@ -91,7 +91,7 @@ describe('List', () => { it('Multi mode should select an item when clicked', () => { const result = render( - + First ListItem Second ListItem , @@ -152,7 +152,7 @@ describe('List', () => { const onClick = jest.fn(customOnclick); const result = render( - + First ListItem Second ListItem , @@ -228,7 +228,7 @@ describe('List', () => { }); it('should be focusable when list is selectable', () => { const result = render( - + First ListItem Second ListItem , @@ -240,7 +240,7 @@ describe('List', () => { }); it('should be focusable when tabIndex=0 is passed', () => { const result = render( - + First ListItem Second ListItem , @@ -285,7 +285,7 @@ describe('List', () => { const onClick = jest.fn(customOnclick); const result = render( - + First ListItem Second ListItem , diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index b8f07395e3c719..3101d5e84a19fb 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -10,7 +10,6 @@ export type ListSlots = { * List Props */ export type ListProps = ComponentProps & { - selectable?: boolean; selectionMode?: SelectionMode; selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 130de6fa20b7fc..5811843712d71e 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -22,14 +22,7 @@ const DEFAULT_ROOT_EL_TYPE = 'ul'; * @param ref - reference to root HTMLElement of List */ export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { - const { - selectable = false, - selectionMode = 'multiselect', - selectedItems, - defaultSelectedItems, - as, - onSelectionChange, - } = props; + const { selectionMode, selectedItems, defaultSelectedItems, as, onSelectionChange } = props; const arrowNavigationAttributes = useArrowNavigationGroup({ axis: 'vertical', @@ -50,7 +43,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref { return (
    - + {items.map(({ name, avatar }) => ( {
    - setSelectedItems(data.selectedItems)} - > + setSelectedItems(data.selectedItems)}> {items.map(({ name, avatar }) => ( ))} diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx index f4a316e75aff0a..771af783e9078d 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledWithState.stories.tsx @@ -83,7 +83,7 @@ export const ListSelectionControlledWithState = () => {
  • selection.setSelectedItems(data.selectedItems)} > diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index aaa8aa29415d23..a69ef12090f149 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -154,7 +154,7 @@ export const MultipleActionsDifferentPrimary = (props: Partial) => { return ( setSelectedItems(data.selectedItems)} selectionMode="multiselect" diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx index b0412771b99713..72232967dbf82d 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx @@ -148,11 +148,10 @@ export const MultipleActionsSelection = (props: Partial) => { return ( setSelectedItems(data.selectedItems)} - selectionMode="multiselect" > diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx index fa820e57730abc..51780d6a75c3b3 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx @@ -59,7 +59,7 @@ export const SingleActionSelection = () => { return (
    setSelectedItems(data.selectedItems)} > @@ -78,7 +78,7 @@ SingleActionSelection.parameters = { story: [ 'Any List can be selectable. You have an option to control the selection state yourself or let the List manage it for you.', '', - 'You can pass `selectable` prop inside of the List component to get support for selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state. While we are saving the results of `onSelectionChange`, it is purely for keeping track of the selection state and is not used to control the selection.', + 'You can pass `selectionMode` prop with value "single" or "multiselect" to the List component to get support for selection. The List notifies the parent about changes in selection via the `onSelectionChange` props. In the example we are also using the `defaultSelectedItems` prop to set the initial selection state. While we are saving the results of `onSelectionChange`, it is purely for keeping track of the selection state and is not used to control the selection.', '', 'The items can be toggled by clicking on them, clicking on the checkbox or pressing a Spacebar when the item is focused.', '', diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx index 8e52d36ffa8fe3..fecd2b7b62bfa9 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx @@ -71,7 +71,7 @@ export const SingleActionSelectionDifferentPrimary = () => { return (
    setSelectedItems(data.selectedItems)} > From d0d46111ff711a05669ae3e245bb1686479ea77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 28 Feb 2024 11:26:20 +0100 Subject: [PATCH 096/136] replace custom focus logic with tabster events --- .../src/components/List/List.cy.tsx | 4 +-- .../src/components/ListItem/useListItem.tsx | 25 ++++--------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx index 2b886144d7035a..10bc596be892b5 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx @@ -130,7 +130,7 @@ describe('List', () => { testSequence(['focused:list-item-1', 'PageDown', 'focused:list-item-3', 'PageUp', 'focused:list-item-1']); }); - it('Left/Right arrow key moves focus on the first secondary action', () => { + it('Left/Right arrow key moves focus horizontally in the list item', () => { mountListWithSecondaryActions(); testSequence([ 'focused:list-item-1', @@ -177,7 +177,7 @@ describe('List', () => { ]); }); - it('Ignored keys are ignored', () => { + it('Keys like Enter and Space are ignored', () => { mountListWithSecondaryActions(); testSequence(['RightArrow', 'focused:list-item-1-button-1', 'Enter', ' ', 'focused:list-item-1-button-1']); }); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index f0bdf9a035e656..c679bb017e8db5 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -6,13 +6,12 @@ import { TabsterTypes, useArrowNavigationGroup, useFocusableGroup, - useFocusFinders, useMergedTabsterAttributes_unstable, } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; -import { Enter, Escape, ArrowUp, ArrowDown, keyCodes, Space } from '@fluentui/keyboard-keys'; +import { Enter, Space, ArrowUp, ArrowDown, ArrowRight, ArrowLeft } from '@fluentui/keyboard-keys'; import { Checkbox, CheckboxOnChangeData } from '@fluentui/react-checkbox'; import { useIndicatorStyle } from './useListItemStyles.styles'; @@ -60,8 +59,6 @@ export const useListItem_unstable = ( validateProperElementTypes(parentRenderedAs, renderedAs); - const { findFirstFocusable } = useFocusFinders(); - const baseIndicatorStyles = useIndicatorStyle(); const focusableGroupAttrs = useFocusableGroup({ @@ -79,15 +76,6 @@ export const useListItem_unstable = ( toggleItem?.(e, value); }); - const pressEscape = useEventCallback((target: HTMLElement) => { - target.dispatchEvent( - new KeyboardEvent('keydown', { - key: Escape, - keyCode: keyCodes.Escape, - }), - ); - }); - const handleKeyDown: React.KeyboardEventHandler = useEventCallback(e => { onKeyDown?.(e); @@ -102,8 +90,8 @@ export const useListItem_unstable = ( // The ArrowLeft will only trigger if the target element is the leftmost, otherwise the // arrowNavigationAttributes handles it and prevents it from bubbling here. - if (e.key === 'ArrowLeft') { - pressEscape(e.target as HTMLElement); + if (e.key === ArrowLeft) { + dispatchGroupperMoveFocusEvent(e.target as HTMLElement, TabsterTypes.GroupperMoveFocusActions.Escape); } if (e.key === ArrowDown || e.key === ArrowUp) { @@ -140,11 +128,8 @@ export const useListItem_unstable = ( } // Handle entering the list item when user presses the ArrowRight - if (e.key === 'ArrowRight') { - const el = findFirstFocusable(e.currentTarget); - if (el) { - el.focus(); - } + if (e.key === ArrowRight) { + dispatchGroupperMoveFocusEvent(e.target as HTMLElement, TabsterTypes.GroupperMoveFocusActions.Enter); } }); From 91b697b524e8d85fb5a04960131fdf152da68c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 28 Feb 2024 15:23:28 +0100 Subject: [PATCH 097/136] add more tests --- .../src/components/List/List.cy.tsx | 159 ++++++++++++++++++ .../src/components/List/List.test.tsx | 99 +++++++++-- .../List/__snapshots__/List.test.tsx.snap | 30 +++- 3 files changed, 277 insertions(+), 11 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx index 10bc596be892b5..9c1c192cb6cca3 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx @@ -5,6 +5,7 @@ import { teamsLightTheme } from '@fluentui/react-theme'; import { List } from './List'; import { ListItem } from '../ListItem'; +import { SelectionItemId } from '@fluentui/react-utilities'; const mount = (element: JSX.Element) => { mountBase({element}); @@ -68,6 +69,102 @@ const mountListWithSecondaryActions = () => { ); }; +type SelectionTestListProps = { + selectionMode: React.ComponentProps['selectionMode']; + defaultSelectedItems?: React.ComponentProps['defaultSelectedItems']; + controlled?: boolean; +}; + +const SelectionTestList = ({ selectionMode, defaultSelectedItems, controlled }: SelectionTestListProps) => { + const [selectedItems, setSelectedItems] = React.useState(defaultSelectedItems || []); + + const onChange = React.useCallback((_, { selectedItems: selected }) => { + setSelectedItems(selected); + }, []); + + const onSelectLastClick = React.useCallback(_ => { + setSelectedItems(['list-item-3']); + }, []); + + return ( + <> + + + List Item 1 + + + List Item 2 + + + List Item 3 + + + +
    {JSON.stringify(selectedItems)}
    + + ); +}; + +const mountListForSelection = ( + selectionMode: 'single' | 'multiselect', + defaultSelectedItems?: Array, + controlled?: boolean, +) => { + mount( + , + ); +}; + +/** + * Validates the state of the list items based on the expected states. + * It checks: + * - aria-selected attribute on each item + * - checkbox state on each item + * - presence of the item in the parent state + * + * @param expectedStates - Array of boolean values representing the expected state of the list items + */ +const validateSetOfListItems = (expectedStates: Array) => + expectedStates.forEach((checked, index) => { + const listItem = cy.get(`[data-test="list-item-${index + 1}"]`); + cy.log('Validate aria-selected attr on item', index + 1); + listItem.should('have.attr', 'aria-selected', checked.toString()); + + cy.log('Validate checkbox state on item', index + 1); + cy.get(`[data-test="list-item-${index + 1}"] .fui-Checkbox__indicator > svg`).should( + checked ? 'exist' : 'not.exist', + ); + + cy.log('Validate that the item is present/not present in the parent state (or stringified state)'); + cy.get(`[data-test="selected-items"]`).should(checked ? 'contain' : 'not.contain', `list-item-${index + 1}`); + }); + +/** Toggles list item based on "shortId" - just a number + * @param shortId - Short id of the list item + * @example + * toggleListItem('1'); + */ +const toggleListItem = (shortId: String) => cy.get(`[data-test="list-item-${shortId}"]`).click(); + +/** + * Toggles the last item in the list + * Useful for testing that the controlled selection works as expected, as we change the state from the parent + * component and expect the list to reflect the change. + * @returns + */ +const selectOnlyLastItem = () => cy.get(`[data-test="select-last-item"]`).click(); + describe('List', () => { describe('keyboard navigation', () => { describe('Simple list with a single action', () => { @@ -183,4 +280,66 @@ describe('List', () => { }); }); }); + + describe('selection', () => { + describe('single select', () => { + it('selects the item when clicked', () => { + mountListForSelection('single'); + validateSetOfListItems([false, false, false]); + toggleListItem('1'); + validateSetOfListItems([true, false, false]); + toggleListItem('2'); + validateSetOfListItems([false, true, false]); + }); + + it('uncontrolled selection with defaultSelectedItems works', () => { + mountListForSelection('single', ['list-item-2']); + validateSetOfListItems([false, true, false]); + toggleListItem('3'); + validateSetOfListItems([false, false, true]); + }); + + it('controlled selection works', () => { + mountListForSelection('single', ['list-item-2'], true); + validateSetOfListItems([false, true, false]); + toggleListItem('1'); + validateSetOfListItems([true, false, false]); + selectOnlyLastItem(); + validateSetOfListItems([false, false, true]); + }); + }); + + describe('multi select', () => { + it('selects the item when clicked', () => { + mountListForSelection('multiselect'); + validateSetOfListItems([false, false, false]); + toggleListItem('1'); + validateSetOfListItems([true, false, false]); + toggleListItem('2'); + validateSetOfListItems([true, true, false]); + }); + + it('uncontrolled selection with defaultSelectedItems works', () => { + mountListForSelection('multiselect', ['list-item-2']); + validateSetOfListItems([false, true, false]); + toggleListItem('3'); + validateSetOfListItems([false, true, true]); + toggleListItem('2'); + validateSetOfListItems([false, false, true]); + toggleListItem('1'); + validateSetOfListItems([true, false, true]); + }); + + it('controlled selection works', () => { + mountListForSelection('multiselect', ['list-item-2'], true); + validateSetOfListItems([false, true, false]); + toggleListItem('1'); + validateSetOfListItems([true, true, false]); + selectOnlyLastItem(); + validateSetOfListItems([false, false, true]); + toggleListItem('2'); + validateSetOfListItems([false, true, true]); + }); + }); + }); }); diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 517e8f61df8c27..260faf955bafc6 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -7,7 +7,6 @@ import { List } from './List'; import { ListProps } from './List.types'; import { ListItem } from '../ListItem/ListItem'; import { EventHandler } from 'react'; -import userEvent from '@testing-library/user-event'; function expectListboxItemSelected(item: HTMLElement, selected: boolean) { expect(item.getAttribute('aria-selected')).toBe(selected.toString()); @@ -25,16 +24,96 @@ describe('List', () => { }, }); - // TODO add more tests here, and create visual regression tests in /apps/vr-tests + describe('rendering', () => { + it('renders a default state', () => { + const result = render( + + First ListItem + Second ListItem + , + ); + expect(result.container).toMatchSnapshot(); + }); + + describe('checkbox indicator', () => { + it("doesn't render checkbox when selectionMode is not set", () => { + const result = render( + + First ListItem + Second ListItem + , + ); + expect(result.queryAllByRole('checkbox')).toHaveLength(0); + }); + it("renders checkbox when selectionMode is 'single'", () => { + const result = render( + + First ListItem + Second ListItem + , + ); + console.log(prettyDOM(result.container)); + expect(result.queryAllByRole('checkbox')).toHaveLength(2); + }); + it("renders checkbox when selectionMode is 'multiselect'", () => { + const result = render( + + First ListItem + Second ListItem + , + ); + expect(result.queryAllByRole('checkbox')).toHaveLength(2); + }); + }); - it('renders a default state', () => { - const result = render( - - First ListItem - Second ListItem - , - ); - expect(result.container).toMatchSnapshot(); + describe('render as', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => jest.fn()); + }); + + afterEach(() => { + (console.error as jest.Mock).mockRestore(); + }); + it('div and li throws', () => { + expect(() => + render( + + First ListItem + Second ListItem + , + ), + ).toThrowError('ListItem cannot be rendered as a li when its parent is a div.'); + }); + + it('ul and div throws', () => { + expect(() => + render( + + + First ListItem + + + Second ListItem + + , + ), + ).toThrowError('ListItem cannot be rendered as a div when its parent is not a div.'); + }); + + it("div and div doesn't throw", () => { + const result = render( + + + First ListItem + + + Second ListItem + + , + ); + expect(result.container).toMatchSnapshot(); + }); + }); }); describe('roles', () => { diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap index 74807c5d24a177..73c1b5cffcc386 100644 --- a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -1,6 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`List renders a default state 1`] = ` +exports[`List rendering render as div and div doesn't throw 1`] = ` +
    +
    +
    + First ListItem +
    +
    + Second ListItem +
    +
    +
    +`; + +exports[`List rendering renders a default state 1`] = `
      Date: Wed, 28 Feb 2024 15:34:13 +0100 Subject: [PATCH 098/136] add tests for custom aria roles --- .../src/components/List/List.test.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 260faf955bafc6..09c4e8907f1a0a 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -1,7 +1,7 @@ import '@testing-library/jest-dom'; import * as React from 'react'; -import { fireEvent, prettyDOM, render, waitFor, within } from '@testing-library/react'; +import { fireEvent, prettyDOM, render, within } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { List } from './List'; import { ListProps } from './List.types'; @@ -16,12 +16,6 @@ describe('List', () => { isConformant({ Component: List as React.FunctionComponent, displayName: 'List', - testOptions: { - 'consistent-callback-args': { - // onSelectionChange has an eventArgument which is React.SyntheticEvent. This throws an error during testing - ignoreProps: ['onSelectionChange'], - }, - }, }); describe('rendering', () => { @@ -141,6 +135,18 @@ describe('List', () => { expect(result.getAllByRole('option')).toHaveLength(2); }); + it('custom - should have passed roles', () => { + const result = render( + + First ListItem + Second ListItem + , + ); + + expect(result.getAllByRole('grid')).toHaveLength(1); + expect(result.getAllByRole('row')).toHaveLength(2); + }); + // TODO: Add more tests for multiple action once those components are created }); From 4250a331b58b53c8eedf7259f2b7d26eace4ccf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 29 Feb 2024 16:04:41 +0100 Subject: [PATCH 099/136] test snapshots of selectable/multiselectable --- .../src/components/List/List.test.tsx | 21 +++ .../List/__snapshots__/List.test.tsx.snap | 127 ++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 09c4e8907f1a0a..f58b31a5123fcb 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -151,6 +151,27 @@ describe('List', () => { }); describe('selection', () => { + it('single select items are properly rendered with all attributes', () => { + const result = render( + + First ListItem + Second ListItem + , + ); + + expect(result.container).toMatchSnapshot(); + }); + it('multiselect items are properly rendered with all attributes', () => { + const result = render( + + First ListItem + Second ListItem + , + ); + + expect(result.container).toMatchSnapshot(); + }); + it('Single mode should unselect previous', () => { const result = render( diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap index 73c1b5cffcc386..73b01b58b4f3d3 100644 --- a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -57,3 +57,130 @@ exports[`List rendering renders a default state 1`] = `
    `; + +exports[`List selection multiselect items are properly rendered with all attributes 1`] = ` +
    +
      +
    • + + +
    • +
    • + + +
    • +
    +
    +`; + +exports[`List selection single select items are properly rendered with all attributes 1`] = ` +
    +
      +
    • + + +
    • +
    • + + +
    • +
    +
    +`; From 78bf29ba4147acd0d963d8b158977b7d65f77a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 4 Mar 2024 11:59:03 +0100 Subject: [PATCH 100/136] fix one story --- .../List/ListSelectionControlledBasic.stories.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx index 5a6e58a75044c7..f3e3e42f367fc9 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx @@ -1,5 +1,4 @@ -import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; -import { List, ListItem } from '@fluentui/react-list-preview'; +import { Button, makeStyles, Persona, shorthands, SelectionItemId, List, ListItem } from '@fluentui/react-components'; import * as React from 'react'; const names = [ @@ -69,7 +68,11 @@ export const ListSelectionControlledBasic = () => {
    - setSelectedItems(data.selectedItems)}> + setSelectedItems(data.selectedItems)} + > {items.map(({ name, avatar }) => ( ))} From b0a81cafb651679ea05dcafbfba9bc7c1fb29e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 4 Mar 2024 15:04:15 +0100 Subject: [PATCH 101/136] automatically defer proper aria roles --- .../src/components/List/List.cy.tsx | 122 ++++++++++++++++++ .../src/components/List/List.test.tsx | 3 +- .../src/components/List/List.types.ts | 2 + .../List/__snapshots__/List.test.tsx.snap | 2 + .../src/components/List/listContext.ts | 7 + .../src/components/List/useList.ts | 6 +- .../components/List/useListContextValues.ts | 3 +- .../src/components/ListItem/ListItem.test.tsx | 2 - .../src/components/ListItem/useListItem.tsx | 15 ++- .../src/hooks/useListAccessibilityRoles.tsx | 60 +++++++++ ...ultipleActionsDifferentPrimary.stories.tsx | 8 +- .../List/MultipleActionsSelection.stories.tsx | 7 +- .../stories/List/SingleAction.stories.tsx | 5 +- 13 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx diff --git a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx index 9c1c192cb6cca3..b7f3a3f3b3430d 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx @@ -342,4 +342,126 @@ describe('List', () => { }); }); }); + + describe('Accessibility roles', () => { + describe('without focusable children', () => { + it('default list is list/listitem', () => { + mountSimpleList(); + cy.get('ul').should('have.attr', 'role', 'list'); + cy.get('li').should('have.attr', 'role', 'listitem'); + }); + + it("single select list is listbox/option and doesn't have multiselectable aria prop", () => { + mount( + + + List Item 1 + + + List Item 2 + + + List Item 3 + + , + ); + cy.get('ul').should('have.attr', 'role', 'listbox'); + cy.get('li').should('have.attr', 'role', 'option'); + }); + + it('multiple select list is listbox/option and has multiselectable aria prop', () => { + mount( + + + List Item 1 + + + List Item 2 + + + List Item 3 + + , + ); + cy.get('ul').should('have.attr', 'aria-multiselectable', 'true'); + cy.get('ul').should('have.attr', 'role', 'listbox'); + cy.get('li').should('have.attr', 'role', 'option'); + }); + + it('custom roles work', () => { + mount( + + + List Item 1 + + + List Item 2 + + + List Item 3 + + , + ); + cy.get('ul').should('have.attr', 'role', 'customListRole'); + cy.get('li').should('have.attr', 'role', 'customListItemRole'); + }); + }); + + describe('with focusable children', () => { + it('default list is grid/row', () => { + mount( + + + List Item 1 + + + List Item 2 + + + List Item 3 + + , + ); + cy.get('ul').should('have.attr', 'role', 'grid'); + cy.get('li').should('have.attr', 'role', 'row'); + }); + + it("single select list is grid/row and doesn't have multiselectable aria prop", () => { + mount( + + + List Item 1 + + + List Item 2 + + + List Item 3 + + , + ); + cy.get('ul').should('have.attr', 'role', 'grid'); + cy.get('li').should('have.attr', 'role', 'row'); + }); + + it('multiple select list is grid/row and has multiselectable aria prop', () => { + mount( + + + List Item 1 + + + List Item 2 + + + List Item 3 + + , + ); + cy.get('ul').should('have.attr', 'aria-multiselectable', 'true'); + cy.get('ul').should('have.attr', 'role', 'grid'); + cy.get('li').should('have.attr', 'role', 'row'); + }); + }); + }); }); diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index f58b31a5123fcb..655ca143fe2dc6 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -1,7 +1,7 @@ import '@testing-library/jest-dom'; import * as React from 'react'; -import { fireEvent, prettyDOM, render, within } from '@testing-library/react'; +import { fireEvent, render, within } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; import { List } from './List'; import { ListProps } from './List.types'; @@ -46,7 +46,6 @@ describe('List', () => { Second ListItem , ); - console.log(prettyDOM(result.container)); expect(result.queryAllByRole('checkbox')).toHaveLength(2); }); it("renders checkbox when selectionMode is 'multiselect'", () => { diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 3101d5e84a19fb..c07f39927dfd08 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import type { ComponentProps, ComponentState, Slot, SelectionMode, SelectionItemId } from '@fluentui/react-utilities'; import type { ListSelectionState } from '../../hooks/types'; +import { ListAccessibilityRoles } from '../../hooks/useListAccessibilityRoles'; export type ListSlots = { root: NonNullable>; @@ -20,6 +21,7 @@ export type ListProps = ComponentProps & { export type ListContextValue = { selection?: ListSelectionState; as?: 'div' | 'ol' | 'ul'; + accessibilityRoles: ListAccessibilityRoles; }; export type ListContextValues = { diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap index 73b01b58b4f3d3..43884034f2de2a 100644 --- a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -5,6 +5,7 @@ exports[`List rendering render as div and div doesn't throw 1`] = `
  • { + /* noop */ + }, + }, }; const listContext = createContext(undefined); diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 5811843712d71e..9ddfe9323e56bb 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -9,6 +9,7 @@ import { import { useArrowNavigationGroup } from '@fluentui/react-tabster'; import { ListProps, ListState } from './List.types'; import { useListSelection } from '../../hooks/useListSelection'; +import { useListAccessibilityRoles } from '../../hooks/useListAccessibilityRoles'; const DEFAULT_ROOT_EL_TYPE = 'ul'; @@ -48,6 +49,8 @@ export const useList_unstable = (props: ListProps, ref: React.Ref { }, }); - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - it('renders a default state', () => { const result = render(Default ListItem); expect(result.container).toMatchSnapshot(); diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index c679bb017e8db5..301707014036e1 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -6,6 +6,7 @@ import { TabsterTypes, useArrowNavigationGroup, useFocusableGroup, + useFocusFinders, useMergedTabsterAttributes_unstable, } from '@fluentui/react-tabster'; import { getIntrinsicElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; @@ -49,6 +50,10 @@ export const useListItem_unstable = ( const toggleItem = useListContext_unstable(ctx => ctx.selection?.toggleItem); const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); + const listItemRole = useListContext_unstable(ctx => ctx.accessibilityRoles.listItemRole); + const setFocusableChildren = useListContext_unstable(ctx => ctx.accessibilityRoles.setFocusableChildren); + + const { findAllFocusable } = useFocusFinders(); const focusableItems = isSelectionEnabled || tabIndex === 0 || onClick || onKeyDown; @@ -61,6 +66,13 @@ export const useListItem_unstable = ( const baseIndicatorStyles = useIndicatorStyle(); + React.useEffect(() => { + if (rootRef.current) { + const focusable = findAllFocusable(rootRef.current); + setFocusableChildren(focusable.length > 0); + } + }, [findAllFocusable, setFocusableChildren]); + const focusableGroupAttrs = useFocusableGroup({ ignoreDefaultKeydown: { Enter: true }, tabBehavior: 'limited-trap-focus', @@ -160,10 +172,9 @@ export const useListItem_unstable = ( getIntrinsicElementProps(DEFAULT_ROOT_EL_TYPE, { ref: useMergedRefs(rootRef, ref) as React.Ref, tabIndex: focusableItems ? 0 : undefined, - role: 'listitem', + role: listItemRole, id: String(value), ...(isSelectionEnabled && { - role: 'option', 'aria-selected': isSelected, }), ...tabsterAttributes, diff --git a/packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx b/packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx new file mode 100644 index 00000000000000..42f7ddd313d86f --- /dev/null +++ b/packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +export type ListAccessibilityRoles = { + listRole: string; + listItemRole: string; + setFocusableChildren: (focusableItems: boolean) => void; +}; + +export function useListAccessibilityRoles(hasSelection: boolean): ListAccessibilityRoles { + const [hasFocusableChildren, setHasFocusableChildren] = React.useState(); + + const setFocusableChildren = React.useCallback( + (focusableItems: boolean) => { + if (hasFocusableChildren === undefined) { + setHasFocusableChildren(focusableItems); + } + }, + [hasFocusableChildren], + ); + + const listRole = React.useMemo(() => { + //not initialized, return "list" + if (hasFocusableChildren === undefined) { + return 'list'; + } + + if (hasFocusableChildren) { + return 'grid'; + } else { + if (hasSelection) { + return 'listbox'; + } else { + return 'list'; + } + } + }, [hasFocusableChildren, hasSelection]); + + const listItemRole = React.useMemo(() => { + //not initialized, return "listitem" + if (hasFocusableChildren === undefined) { + return 'listitem'; + } + + if (hasFocusableChildren) { + return 'row'; + } else { + if (hasSelection) { + return 'option'; + } else { + return 'listitem'; + } + } + }, [hasFocusableChildren, hasSelection]); + + return { + listRole, + listItemRole, + setFocusableChildren, + }; +} diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index a69ef12090f149..aace8bea0eabb8 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -12,9 +12,12 @@ import { shorthands, Text, tokens, + List, + ListItem, + ListProps, } from '@fluentui/react-components'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; -import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; + import * as React from 'react'; const useListItemRootStyles = makeResetStyles({ @@ -65,7 +68,6 @@ const CardExample = (props: { title: string; value: string; selected?: boolean } value={props.value} className={mergeClasses(listItemStyles, styles.listItem)} checkmark={{ className: styles.checkmark }} - role="row" aria-label={value} onClick={e => { // Prevent the default behavior - toggling the selection @@ -155,9 +157,7 @@ export const MultipleActionsDifferentPrimary = (props: Partial) => { setSelectedItems(data.selectedItems)} - selectionMode="multiselect" > diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx index 72232967dbf82d..b78ac8fed471f6 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx @@ -12,9 +12,12 @@ import { shorthands, Text, tokens, + List, + ListItem, + ListProps, } from '@fluentui/react-components'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; -import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; + import * as React from 'react'; const useListItemRootStyles = makeResetStyles({ @@ -65,7 +68,6 @@ const CardExample = (props: { title: string; value: string; selected?: boolean } value={props.value} className={mergeClasses(listItemStyles, styles.listItem)} checkmark={{ className: styles.checkmark }} - role="row" aria-label={value} >
    @@ -150,7 +152,6 @@ export const MultipleActionsSelection = (props: Partial) => { setSelectedItems(data.selectedItems)} > diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index 3dca9cadf6942e..52c0a3f0ca74bc 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -1,5 +1,5 @@ -import { makeStyles, Persona, shorthands } from '@fluentui/react-components'; -import { List, ListItem } from '@fluentui/react-list-preview'; +import { makeStyles, Persona, shorthands, List, ListItem } from '@fluentui/react-components'; + import * as React from 'react'; const names = [ 'Melda Bevel', @@ -32,7 +32,6 @@ export const SingleAction = () => { alert(`Triggered action on ${name}`)} From a4adc8dca881458c0d98a12d21a27b84bc9063cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 5 Mar 2024 10:40:18 +0100 Subject: [PATCH 102/136] only pass onClick if it actually does something --- .../react-list-preview/src/components/ListItem/useListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 301707014036e1..7f21e5639782fd 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -181,7 +181,7 @@ export const useListItem_unstable = ( ...props, 'data-value': String(value), onKeyDown: handleKeyDown, - onClick: handleClick, + onClick: isSelectionEnabled || onClick ? handleClick : undefined, }), { elementType: DEFAULT_ROOT_EL_TYPE }, ); From 2b6623c35766b85460154f019fc63f10372a2fad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 5 Mar 2024 11:57:37 +0100 Subject: [PATCH 103/136] bump list deps to latest --- packages/react-components/react-list-preview/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index a78364db77e78f..572035a2fc22cd 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -38,13 +38,13 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-button": "^9.3.68", - "@fluentui/react-checkbox": "^9.2.12", - "@fluentui/react-context-selector": "^9.1.51", + "@fluentui/react-button": "^9.3.70", + "@fluentui/react-checkbox": "^9.2.14", + "@fluentui/react-context-selector": "^9.1.53", "@fluentui/react-icons": "^2.0.224", "@fluentui/react-jsx-runtime": "^9.0.31", "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-tabster": "^9.19.0", + "@fluentui/react-tabster": "^9.19.2", "@fluentui/react-theme": "^9.1.16", "@fluentui/react-utilities": "^9.18.2", "@fluentui/react-shared-contexts": "^9.14.1", From df210bd2c6e9808c13f1d2e013ffcec1029f195a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 5 Mar 2024 12:04:33 +0100 Subject: [PATCH 104/136] update changelog for removed ListItemButton --- ...eact-provider-c53fd31c-6213-4dc3-a622-f036b04f8d5a.json | 7 +++++++ ...ared-contexts-c8fe7f5c-9503-41d6-a0d6-dec1071446ac.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@fluentui-react-provider-c53fd31c-6213-4dc3-a622-f036b04f8d5a.json create mode 100644 change/@fluentui-react-shared-contexts-c8fe7f5c-9503-41d6-a0d6-dec1071446ac.json diff --git a/change/@fluentui-react-provider-c53fd31c-6213-4dc3-a622-f036b04f8d5a.json b/change/@fluentui-react-provider-c53fd31c-6213-4dc3-a622-f036b04f8d5a.json new file mode 100644 index 00000000000000..946bbc8a100c09 --- /dev/null +++ b/change/@fluentui-react-provider-c53fd31c-6213-4dc3-a622-f036b04f8d5a.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "remove unused useListItemButtonStyles_unstable", + "packageName": "@fluentui/react-provider", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-shared-contexts-c8fe7f5c-9503-41d6-a0d6-dec1071446ac.json b/change/@fluentui-react-shared-contexts-c8fe7f5c-9503-41d6-a0d6-dec1071446ac.json new file mode 100644 index 00000000000000..0e1172e029f85e --- /dev/null +++ b/change/@fluentui-react-shared-contexts-c8fe7f5c-9503-41d6-a0d6-dec1071446ac.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "remove unused ListItemButton exports", + "packageName": "@fluentui/react-shared-contexts", + "email": "jirivyhnalek@microsoft.com", + "dependentChangeType": "patch" +} From 92f0c28cec403a8ae3e2f4a9b1b79833c85db0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 6 Mar 2024 13:12:07 +0100 Subject: [PATCH 105/136] Remove export from react-components --- apps/public-docsite-v9/package.json | 1 + .../etc/react-components.api.md | 51 ------------------- .../react-components/package.json | 3 +- .../react-components/src/index.ts | 24 --------- .../ListSelectionControlledBasic.stories.tsx | 3 +- ...ultipleActionsDifferentPrimary.stories.tsx | 4 +- .../List/MultipleActionsSelection.stories.tsx | 4 +- .../stories/List/SingleAction.stories.tsx | 3 +- .../List/SingleActionSelection.stories.tsx | 3 +- 9 files changed, 10 insertions(+), 86 deletions(-) diff --git a/apps/public-docsite-v9/package.json b/apps/public-docsite-v9/package.json index 5a3b05b83a6a29..e1868cc0fa44e6 100644 --- a/apps/public-docsite-v9/package.json +++ b/apps/public-docsite-v9/package.json @@ -34,6 +34,7 @@ "@fluentui/react-storybook-addon": "*", "@fluentui/react-storybook-addon-export-to-sandbox": "*", "@fluentui/theme-designer": "*", + "@fluentui/react-list-preview": "*", "@fluentui/react-rating-preview": "*", "@fluentui/react-search-preview": "*", "@fluentui/react-motion-preview": "*", diff --git a/packages/react-components/react-components/etc/react-components.api.md b/packages/react-components/react-components/etc/react-components.api.md index c147d794ccb50a..39899cf84cea2c 100644 --- a/packages/react-components/react-components/etc/react-components.api.md +++ b/packages/react-components/react-components/etc/react-components.api.md @@ -416,7 +416,6 @@ import { linkClassNames } from '@fluentui/react-link'; import { LinkProps } from '@fluentui/react-link'; import { LinkSlots } from '@fluentui/react-link'; import { LinkState } from '@fluentui/react-link'; -import { List } from '@fluentui/react-list-preview'; import { Listbox } from '@fluentui/react-combobox'; import { listboxClassNames } from '@fluentui/react-combobox'; import { ListboxContextValue } from '@fluentui/react-combobox'; @@ -425,15 +424,6 @@ import { ListboxProps } from '@fluentui/react-combobox'; import { ListboxProvider } from '@fluentui/react-combobox'; import { ListboxSlots } from '@fluentui/react-combobox'; import { ListboxState } from '@fluentui/react-combobox'; -import { listClassNames } from '@fluentui/react-list-preview'; -import { ListItem } from '@fluentui/react-list-preview'; -import { listItemClassNames } from '@fluentui/react-list-preview'; -import { ListItemProps } from '@fluentui/react-list-preview'; -import { ListItemSlots } from '@fluentui/react-list-preview'; -import { ListItemState } from '@fluentui/react-list-preview'; -import { ListProps } from '@fluentui/react-list-preview'; -import { ListSlots } from '@fluentui/react-list-preview'; -import { ListState } from '@fluentui/react-list-preview'; import { makeResetStyles } from '@griffel/react'; import { makeStaticStyles } from '@griffel/react'; import { makeStyles } from '@griffel/react'; @@ -701,9 +691,7 @@ import { renderInteractionTagPrimary_unstable } from '@fluentui/react-tags'; import { renderInteractionTagSecondary_unstable } from '@fluentui/react-tags'; import { renderLabel_unstable } from '@fluentui/react-label'; import { renderLink_unstable } from '@fluentui/react-link'; -import { renderList_unstable } from '@fluentui/react-list-preview'; import { renderListbox_unstable } from '@fluentui/react-combobox'; -import { renderListItem_unstable } from '@fluentui/react-list-preview'; import { renderMenu_unstable } from '@fluentui/react-menu'; import { renderMenuButton_unstable } from '@fluentui/react-button'; import { renderMenuDivider_unstable } from '@fluentui/react-menu'; @@ -1250,14 +1238,9 @@ import { useLabelStyles_unstable } from '@fluentui/react-label'; import { useLink_unstable } from '@fluentui/react-link'; import { useLinkState_unstable } from '@fluentui/react-link'; import { useLinkStyles_unstable } from '@fluentui/react-link'; -import { useList_unstable } from '@fluentui/react-list-preview'; import { useListbox_unstable } from '@fluentui/react-combobox'; import { useListboxContextValues } from '@fluentui/react-combobox'; import { useListboxStyles_unstable } from '@fluentui/react-combobox'; -import { useListItem_unstable } from '@fluentui/react-list-preview'; -import { useListItemStyles_unstable } from '@fluentui/react-list-preview'; -import { useListSelection } from '@fluentui/react-list-preview'; -import { useListStyles_unstable } from '@fluentui/react-list-preview'; import { useMenu_unstable } from '@fluentui/react-menu'; import { useMenuButton_unstable } from '@fluentui/react-button'; import { useMenuButtonStyles_unstable } from '@fluentui/react-button'; @@ -2272,8 +2255,6 @@ export { LinkSlots } export { LinkState } -export { List } - export { Listbox } export { listboxClassNames } @@ -2290,24 +2271,6 @@ export { ListboxSlots } export { ListboxState } -export { listClassNames } - -export { ListItem } - -export { listItemClassNames } - -export { ListItemProps } - -export { ListItemSlots } - -export { ListItemState } - -export { ListProps } - -export { ListSlots } - -export { ListState } - export { makeResetStyles } export { makeStaticStyles } @@ -2842,12 +2805,8 @@ export { renderLabel_unstable } export { renderLink_unstable } -export { renderList_unstable } - export { renderListbox_unstable } -export { renderListItem_unstable } - export { renderMenu_unstable } export { renderMenuButton_unstable } @@ -3940,22 +3899,12 @@ export { useLinkState_unstable } export { useLinkStyles_unstable } -export { useList_unstable } - export { useListbox_unstable } export { useListboxContextValues } export { useListboxStyles_unstable } -export { useListItem_unstable } - -export { useListItemStyles_unstable } - -export { useListSelection } - -export { useListStyles_unstable } - export { useMenu_unstable } export { useMenuButton_unstable } diff --git a/packages/react-components/react-components/package.json b/packages/react-components/react-components/package.json index 4bf859d1a7e0cc..e6bb3b4e89e3b1 100644 --- a/packages/react-components/react-components/package.json +++ b/packages/react-components/react-components/package.json @@ -83,8 +83,7 @@ "@swc/helpers": "^0.5.1", "@fluentui/react-message-bar": "^9.0.21", "@fluentui/react-breadcrumb": "^9.0.16", - "@fluentui/react-aria": "^9.9.1", - "@fluentui/react-list-preview": "0.0.1" + "@fluentui/react-aria": "^9.9.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <19.0.0", diff --git a/packages/react-components/react-components/src/index.ts b/packages/react-components/react-components/src/index.ts index a0097b591102ee..10cc1c0a2df8da 100644 --- a/packages/react-components/react-components/src/index.ts +++ b/packages/react-components/react-components/src/index.ts @@ -1572,27 +1572,3 @@ export { useAriaLiveAnnouncerContextValues_unstable, } from '@fluentui/react-aria'; export type { AriaLiveAnnouncerProps, AriaLiveAnnouncerState } from '@fluentui/react-aria'; - -// List preview just for building PR docsite -export { - List, - listClassNames, - renderList_unstable, - useListStyles_unstable, - useList_unstable, - ListItem, - listItemClassNames, - renderListItem_unstable, - useListItemStyles_unstable, - useListItem_unstable, - useListSelection, -} from '@fluentui/react-list-preview'; - -export type { - ListProps, - ListSlots, - ListState, - ListItemProps, - ListItemSlots, - ListItemState, -} from '@fluentui/react-list-preview'; diff --git a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx index f3e3e42f367fc9..531a2681dccb7a 100644 --- a/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListSelectionControlledBasic.stories.tsx @@ -1,4 +1,5 @@ -import { Button, makeStyles, Persona, shorthands, SelectionItemId, List, ListItem } from '@fluentui/react-components'; +import { Button, makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; const names = [ diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index aace8bea0eabb8..7c1a4190b52932 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -12,10 +12,8 @@ import { shorthands, Text, tokens, - List, - ListItem, - ListProps, } from '@fluentui/react-components'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; import * as React from 'react'; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx index b78ac8fed471f6..cca3b91770ab98 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx @@ -12,10 +12,8 @@ import { shorthands, Text, tokens, - List, - ListItem, - ListProps, } from '@fluentui/react-components'; +import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; import * as React from 'react'; diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index 52c0a3f0ca74bc..0eadede8c4a497 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -1,4 +1,5 @@ -import { makeStyles, Persona, shorthands, List, ListItem } from '@fluentui/react-components'; +import { makeStyles, Persona, shorthands } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; const names = [ diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx index 51780d6a75c3b3..97799ef6fe70b0 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx @@ -1,4 +1,5 @@ -import { makeStyles, Persona, shorthands, SelectionItemId, List, ListItem } from '@fluentui/react-components'; +import { makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; const names = [ From 79c1ba59bad0390af3e572202a9b0f413683f59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 6 Mar 2024 13:16:39 +0100 Subject: [PATCH 106/136] remove obsolete change record --- ...ct-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json diff --git a/change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json b/change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json deleted file mode 100644 index db496dd07d2072..00000000000000 --- a/change/@fluentui-react-components-3c8e3f9c-60b3-4530-bf79-10e7d31830ff.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "Feat: Export List Preview component", - "packageName": "@fluentui/react-components", - "email": "jirivyhnalek@microsoft.com", - "dependentChangeType": "patch" -} From 73930df92bb5d624f80ec3f840fac61c1fba22af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 6 Mar 2024 15:35:22 +0100 Subject: [PATCH 107/136] remove suggestion to user aria-roledescription --- .../react-list-preview/stories/List/ListDescription.md | 2 +- .../stories/List/SingleAction.stories.tsx | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListDescription.md b/packages/react-components/react-list-preview/stories/List/ListDescription.md index b16ee18a020c1a..eceeb4cdc15eae 100644 --- a/packages/react-components/react-list-preview/stories/List/ListDescription.md +++ b/packages/react-components/react-list-preview/stories/List/ListDescription.md @@ -29,7 +29,7 @@ The interactive lists can then be further divided into 2 different categories. T A list item with single action is a list item which doesn't contain any focusable child elements. It can be selectable. -When list item has a custom action on it, it's recommended to pass `aria-roledescription={translated("button")}` to explicitly communicate screen reader users that they are focused on an actionable element. The value needs to be a localized "button" string. +When list item has a custom action on it and it isn't clear from the context, it's possible enhance the list item with other accessibility props like `aria-roledescription={translated("button")}` to explicitly communicate screen reader users that they are focused on an actionable element. ### List items with multiple actions diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index 0eadede8c4a497..ced5846628968d 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -34,7 +34,6 @@ export const SingleAction = () => { id={`id_${name}`} key={name} aria-label={`${name}, available`} - aria-roledescription="button" //needs to be a translated string! onClick={() => alert(`Triggered action on ${name}`)} > Date: Mon, 18 Mar 2024 11:42:34 +0100 Subject: [PATCH 108/136] bump deps --- packages/react-components/react-list-preview/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 925cbd3bcfb406..75f69f3eb81099 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -38,13 +38,13 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-button": "^9.3.70", - "@fluentui/react-checkbox": "^9.2.14", - "@fluentui/react-context-selector": "^9.1.53", + "@fluentui/react-button": "^9.3.72", + "@fluentui/react-checkbox": "^9.2.16", + "@fluentui/react-context-selector": "^9.1.55", "@fluentui/react-icons": "^2.0.224", "@fluentui/react-jsx-runtime": "^9.0.33", "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-tabster": "^9.19.2", + "@fluentui/react-tabster": "^9.19.4", "@fluentui/react-theme": "^9.1.18", "@fluentui/react-utilities": "^9.18.4", "@fluentui/react-shared-contexts": "^9.15.1", From 5e801120c962d068b3996355fa5451108d0fc37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 18 Mar 2024 11:45:32 +0100 Subject: [PATCH 109/136] yarn.lock --- yarn.lock | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4dd59cf8fd80b0..b8fceb559829ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,6 +1972,19 @@ "@griffel/react" "^1.0.0" tslib "^2.1.0" +"@fluentui/react-rating-preview@*": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@fluentui/react-rating-preview/-/react-rating-preview-0.4.0.tgz#d880388653d96e2e0bc07b83a594e10a0f1f0462" + integrity sha512-eKC3GU8VbxeVoypUh+G8futOTogvfmY31vCFoZs37XM/nGt9q4LucK1dduGQXUHV0zy6YNloKvozu3rVUt1VoA== + dependencies: + "@fluentui/react-icons" "^2.0.224" + "@fluentui/react-jsx-runtime" "^9.0.32" + "@fluentui/react-tabster" "^9.19.3" + "@fluentui/react-theme" "^9.1.17" + "@fluentui/react-utilities" "^9.18.3" + "@griffel/react" "^1.5.14" + "@swc/helpers" "^0.5.1" + "@griffel/babel-preset@1.4.20", "@griffel/babel-preset@^1.4.20": version "1.4.20" resolved "https://registry.yarnpkg.com/@griffel/babel-preset/-/babel-preset-1.4.20.tgz#047eb62f104e7bea781820816263e7407fafe543" From a5e67e908fe752a07af30f287861cfb6a4393e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Mon, 18 Mar 2024 12:36:05 +0100 Subject: [PATCH 110/136] fix bad merge --- apps/public-docsite-v9/package.json | 1 - yarn.lock | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/apps/public-docsite-v9/package.json b/apps/public-docsite-v9/package.json index e1868cc0fa44e6..53bb8f29fe7997 100644 --- a/apps/public-docsite-v9/package.json +++ b/apps/public-docsite-v9/package.json @@ -35,7 +35,6 @@ "@fluentui/react-storybook-addon-export-to-sandbox": "*", "@fluentui/theme-designer": "*", "@fluentui/react-list-preview": "*", - "@fluentui/react-rating-preview": "*", "@fluentui/react-search-preview": "*", "@fluentui/react-motion-preview": "*", "@fluentui/react-motions-preview": "*", diff --git a/yarn.lock b/yarn.lock index b8fceb559829ea..4dd59cf8fd80b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1972,19 +1972,6 @@ "@griffel/react" "^1.0.0" tslib "^2.1.0" -"@fluentui/react-rating-preview@*": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@fluentui/react-rating-preview/-/react-rating-preview-0.4.0.tgz#d880388653d96e2e0bc07b83a594e10a0f1f0462" - integrity sha512-eKC3GU8VbxeVoypUh+G8futOTogvfmY31vCFoZs37XM/nGt9q4LucK1dduGQXUHV0zy6YNloKvozu3rVUt1VoA== - dependencies: - "@fluentui/react-icons" "^2.0.224" - "@fluentui/react-jsx-runtime" "^9.0.32" - "@fluentui/react-tabster" "^9.19.3" - "@fluentui/react-theme" "^9.1.17" - "@fluentui/react-utilities" "^9.18.3" - "@griffel/react" "^1.5.14" - "@swc/helpers" "^0.5.1" - "@griffel/babel-preset@1.4.20", "@griffel/babel-preset@^1.4.20": version "1.4.20" resolved "https://registry.yarnpkg.com/@griffel/babel-preset/-/babel-preset-1.4.20.tgz#047eb62f104e7bea781820816263e7407fafe543" From 6c4364df62a8b6b79c7ed3866b64b294cd64eb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 19 Mar 2024 14:09:16 +0100 Subject: [PATCH 111/136] PR feedback round 1 --- .../react-list-preview/config/tests.js | 2 ++ .../react-list-preview/package.json | 8 +++---- .../src/components/List/List.test.tsx | 2 -- .../src/components/ListItem/useListItem.tsx | 21 ++++++++++++------- .../ListItem/useListItemStyles.styles.ts | 14 ++++++------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/react-components/react-list-preview/config/tests.js b/packages/react-components/react-list-preview/config/tests.js index 2e211ae9e21420..c6c67de97059e8 100644 --- a/packages/react-components/react-list-preview/config/tests.js +++ b/packages/react-components/react-list-preview/config/tests.js @@ -1 +1,3 @@ /** Jest test setup file. */ + +require('@testing-library/jest-dom'); diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index a1b59eb7dc83ba..d4897c04d93a84 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -38,13 +38,11 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { - "@fluentui/react-button": "^9.3.72", - "@fluentui/react-checkbox": "^9.2.16", - "@fluentui/react-context-selector": "^9.1.55", - "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-checkbox": "^9.2.17", + "@fluentui/react-context-selector": "^9.1.56", "@fluentui/react-jsx-runtime": "^9.0.34", "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-tabster": "^9.19.4", + "@fluentui/react-tabster": "^9.19.5", "@fluentui/react-theme": "^9.1.19", "@fluentui/react-utilities": "^9.18.5", "@fluentui/react-shared-contexts": "^9.15.2", diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 655ca143fe2dc6..bc6bd4ffeca66f 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -1,5 +1,3 @@ -import '@testing-library/jest-dom'; - import * as React from 'react'; import { fireEvent, render, within } from '@testing-library/react'; import { isConformant } from '../../testing/isConformant'; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 17eece9f958ae9..fd118906c345ee 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -9,12 +9,18 @@ import { useFocusFinders, useMergedTabsterAttributes_unstable, } from '@fluentui/react-tabster'; -import { getIntrinsicElementProps, slot, useEventCallback, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { + getIntrinsicElementProps, + mergeCallbacks, + slot, + useEventCallback, + useId, + useMergedRefs, +} from '@fluentui/react-utilities'; import type { ListItemProps, ListItemState } from './ListItem.types'; import { useListContext_unstable } from '../List/listContext'; import { Enter, Space, ArrowUp, ArrowDown, ArrowRight, ArrowLeft } from '@fluentui/keyboard-keys'; import { Checkbox, CheckboxOnChangeData } from '@fluentui/react-checkbox'; -import { useIndicatorStyle } from './useListItemStyles.styles'; const DEFAULT_ROOT_EL_TYPE = 'li'; @@ -64,8 +70,6 @@ export const useListItem_unstable = ( validateProperElementTypes(parentRenderedAs, renderedAs); - const baseIndicatorStyles = useIndicatorStyle(); - React.useEffect(() => { if (rootRef.current) { const focusable = findAllFocusable(rootRef.current); @@ -146,8 +150,6 @@ export const useListItem_unstable = ( }); const onCheckboxChange = useEventCallback((e: React.ChangeEvent, data: CheckboxOnChangeData) => { - props.checkmark?.onChange?.(e, data); - if (!isSelectionEnabled || e.defaultPrevented) { return; } @@ -189,15 +191,18 @@ export const useListItem_unstable = ( const checkmark = slot.optional(props.checkmark, { defaultProps: { checked: isSelected, - onChange: onCheckboxChange, onClick: onCheckboxClick, tabIndex: -1, - indicator: { className: baseIndicatorStyles.root }, }, renderByDefault: isSelectionEnabled, elementType: Checkbox, }); + if (checkmark) { + checkmark.onChange = mergeCallbacks(checkmark.onChange, onCheckboxChange); + checkmark.onClick = mergeCallbacks(checkmark.onClick, onCheckboxClick); + } + const state: ListItemState = { components: { root: DEFAULT_ROOT_EL_TYPE, diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 56527ff3a4eec8..38e07781d59963 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -10,8 +10,8 @@ export const listItemClassNames: SlotClassNames = { }; const useRootBaseStyles = makeResetStyles({ - ...shorthands.padding(0), - ...shorthands.margin(0), + padding: 0, + margin: 0, textIndent: 0, listStyleType: 'none', ...createCustomFocusIndicatorStyle( @@ -26,6 +26,10 @@ const useRootBaseStyles = makeResetStyles({ const useCheckmarkBaseStyles = makeStyles({ root: { alignSelf: 'center', + //eslint-disable-next-line + '& .fui-Checkbox__indicator': { + ...shorthands.margin('4px'), + }, }, }); /** @@ -65,9 +69,3 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState return state; }; - -export const useIndicatorStyle = makeStyles({ - root: { - ...shorthands.margin('4px'), - }, -}); From 5040ace1d2e6dccb5cb751309e6fd7c4fe609726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Tue, 19 Mar 2024 14:24:35 +0100 Subject: [PATCH 112/136] update stories - do not promote use of style prop --- ...ultipleActionsDifferentPrimary.stories.tsx | 23 +++++++++++++------ .../List/MultipleActionsSelection.stories.tsx | 23 +++++++++++++------ .../MultipleActionsWithPrimary.stories.tsx | 23 +++++++++++++------ 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index 7c1a4190b52932..4913bfa671c905 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -48,12 +48,23 @@ const useStyles = makeStyles({ maxWidth: '100%', ...shorthands.borderRadius('5px'), }, + title: { + fontWeight: 600, + display: 'block', + }, checkmark: { position: 'absolute', left: '10px', top: '10px', zIndex: 1, }, + preview: { + ...shorthands.gridArea('preview'), + ...shorthands.overflow('hidden'), + }, + header: { ...shorthands.gridArea('header') }, + action: { ...shorthands.gridArea('action') }, + secondaryAction: { ...shorthands.gridArea('secondary_action') }, }); const CardExample = (props: { title: string; value: string; selected?: boolean }) => { @@ -73,16 +84,14 @@ const CardExample = (props: { title: string; value: string; selected?: boolean } alert('Primary action triggered!'); }} > -
    +
    Presentation Preview
    -
    - - {props.title} - +
    + {props.title} You created 53m ago
    -
    +
    -
    +
    -
    +
    -
    +
    - + List Item 2 - + List Item 3 @@ -89,18 +83,19 @@ const SelectionTestList = ({ selectionMode, defaultSelectedItems, controlled }: return ( <> - + List Item 1 - + List Item 2 - + List Item 3 @@ -354,15 +349,9 @@ describe('List', () => { it("single select list is listbox/option and doesn't have multiselectable aria prop", () => { mount( - - List Item 1 - - - List Item 2 - - - List Item 3 - + List Item 1 + List Item 2 + List Item 3 , ); cy.get('ul').should('have.attr', 'role', 'listbox'); @@ -372,15 +361,9 @@ describe('List', () => { it('multiple select list is listbox/option and has multiselectable aria prop', () => { mount( - - List Item 1 - - - List Item 2 - - - List Item 3 - + List Item 1 + List Item 2 + List Item 3 , ); cy.get('ul').should('have.attr', 'aria-multiselectable', 'true'); @@ -391,13 +374,13 @@ describe('List', () => { it('custom roles work', () => { mount( - + List Item 1 - + List Item 2 - + List Item 3 , @@ -410,14 +393,14 @@ describe('List', () => { describe('with focusable children', () => { it('default list is grid/row', () => { mount( - - + + List Item 1 - + List Item 2 - + List Item 3 , @@ -429,13 +412,13 @@ describe('List', () => { it("single select list is grid/row and doesn't have multiselectable aria prop", () => { mount( - + List Item 1 - + List Item 2 - + List Item 3 , @@ -447,13 +430,13 @@ describe('List', () => { it('multiple select list is grid/row and has multiselectable aria prop', () => { mount( - + List Item 1 - + List Item 2 - + List Item 3 , diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index bc6bd4ffeca66f..4dc9a75bc22ac6 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -305,11 +305,11 @@ describe('List', () => { firstItem.focus(); expect(document.activeElement).not.toBe(firstItem); }); - it('should be focusable when onClick is passed', () => { + it('should be focusable when "navigable" is passed', () => { const result = render( - - null}>First ListItem - null}>Second ListItem + + First ListItem + Second ListItem , ); @@ -317,18 +317,7 @@ describe('List', () => { firstItem.focus(); expect(document.activeElement).toBe(firstItem); }); - it('should be focusable when onKeyDown is passed', () => { - const result = render( - - null}>First ListItem - null}>Second ListItem - , - ); - const firstItem = result.getByText('First ListItem'); - firstItem.focus(); - expect(document.activeElement).toBe(firstItem); - }); it('should be focusable when list is selectable', () => { const result = render( diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index abd5e5e03397a0..73de5106f5e628 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -24,6 +24,7 @@ export type OnListSelectionChangeData = EventData<'change', React.SyntheticEvent * List Props */ export type ListProps = ComponentProps & { + navigable?: boolean; selectionMode?: SelectionMode; selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; @@ -31,6 +32,7 @@ export type ListProps = ComponentProps & { }; export type ListContextValue = { + navigable: boolean; selection?: ListSelectionState; as?: 'div' | 'ol' | 'ul'; accessibilityRoles: ListAccessibilityRoles; diff --git a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap index 43884034f2de2a..ac42284645f3a1 100644 --- a/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap +++ b/packages/react-components/react-list-preview/src/components/List/__snapshots__/List.test.tsx.snap @@ -10,7 +10,6 @@ exports[`List rendering render as div and div doesn't throw 1`] = `
    @@ -19,7 +18,6 @@ exports[`List rendering render as div and div doesn't throw 1`] = `
    @@ -39,7 +37,6 @@ exports[`List rendering renders a default state 1`] = `
  • ): ListState => { - const { selectionMode, selectedItems, defaultSelectedItems, as, onSelectionChange } = props; + const { navigable, selectionMode, selectedItems, defaultSelectedItems, as, onSelectionChange } = props; const arrowNavigationAttributes = useArrowNavigationGroup({ axis: 'vertical', @@ -68,6 +68,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 730f7fdf5c2fb1..640bc461b7ed3d 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -54,6 +54,7 @@ export const useListItem_unstable = ( const { value = id, onKeyDown, onClick, tabIndex } = props; const toggleItem = useListContext_unstable(ctx => ctx.selection?.toggleItem); + const navigable = useListContext_unstable(ctx => ctx.navigable); const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); const listItemRole = useListContext_unstable(ctx => ctx.accessibilityRoles.listItemRole); @@ -61,7 +62,7 @@ export const useListItem_unstable = ( const { findAllFocusable } = useFocusFinders(); - const focusableItems = isSelectionEnabled || tabIndex === 0 || onClick || onKeyDown; + const focusableItems = isSelectionEnabled || navigable || tabIndex === 0; const parentRenderedAs = useListContext_unstable(ctx => ctx.as); const renderedAs = props.as || DEFAULT_ROOT_EL_TYPE; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx index f9654d3b4eac11..4961ff94d1a178 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx @@ -147,7 +147,7 @@ export const MultipleActionsWithPrimary = (props: Partial) => { const classes = useStyles(); return ( - + diff --git a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx index ced5846628968d..53e0c843930a93 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleAction.stories.tsx @@ -28,7 +28,7 @@ export const SingleAction = () => { const classes = useStyles(); return (
    - + {names.map(name => ( ((props: React.ComponentProps, ref) => ( - + )); export const VirtualizedListWithActionableItems = () => { From 4be0c79e96df9c9f6b3bb16db49e4f216e556a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 20 Mar 2024 11:49:59 +0100 Subject: [PATCH 116/136] rely on navigable instead of hasCustomClick --- .../react-list-preview/etc/react-list-preview.api.md | 4 ++-- .../src/components/ListItem/ListItem.types.ts | 2 +- .../src/components/ListItem/useListItem.tsx | 2 +- .../src/components/ListItem/useListItemStyles.styles.ts | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 4b317a4d76e6ff..65c09b6e696aee 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -44,8 +44,8 @@ export type ListItemSlots = { // @public export type ListItemState = ComponentState & { - selectable?: boolean; - hasCustomOnClick?: boolean; + selectable: boolean; + navigable: boolean; }; // @public diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts index 1677f5836a74f5..1b167eaf33b992 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.types.ts @@ -16,4 +16,4 @@ export type ListItemProps = ComponentProps & { /** * State used in rendering ListItem */ -export type ListItemState = ComponentState & { selectable?: boolean; hasCustomOnClick?: boolean }; +export type ListItemState = ComponentState & { selectable: boolean; navigable: boolean }; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx index 640bc461b7ed3d..b2c8f44ddffbfb 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItem.tsx @@ -211,7 +211,7 @@ export const useListItem_unstable = ( root, checkmark, selectable: isSelectionEnabled, - hasCustomOnClick: typeof props.onClick === 'function', + navigable: focusableItems, }; return state; diff --git a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts index 38e07781d59963..453f034c3eb50e 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts +++ b/packages/react-components/react-list-preview/src/components/ListItem/useListItemStyles.styles.ts @@ -36,7 +36,7 @@ const useCheckmarkBaseStyles = makeStyles({ * Styles for the root slot */ const useStyles = makeStyles({ - rootClickable: { + rootClickableOrSelectable: { display: 'flex', cursor: 'pointer', }, @@ -53,9 +53,7 @@ export const useListItemStyles_unstable = (state: ListItemState): ListItemState state.root.className = mergeClasses( listItemClassNames.root, rootBaseStyles, - // add the clickable root only if we know the items are selectable and there is no custom onClick - // because the custom onClick could be overriding the selection behavior. - (state.selectable || state.hasCustomOnClick) && styles.rootClickable, + (state.selectable || state.navigable) && styles.rootClickableOrSelectable, state.root.className, ); From f78f8986c20fb1c7dd51dada5c5f12ca21d34f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Wed, 20 Mar 2024 15:09:20 +0100 Subject: [PATCH 117/136] rework active element example to less resemble tab interface --- .../stories/List/ListActiveElement.stories.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index d4d56a52c00555..f775b040204b7b 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -36,10 +36,8 @@ const items: Item[] = names.map(name => ({ })); const useStyles = makeStyles({ - wrapper: { - display: 'grid', - gridTemplateColumns: '240px 1fr', - columnGap: '16px', + selectedInfo: { + marginTop: '16px', }, button: { alignSelf: 'center', @@ -88,7 +86,7 @@ const MyListItem = React.memo( export const ListActiveElement = () => { const classes = useStyles(); - const [selectedItems, setSelectedItems] = React.useState([]); + const [selectedItems, setSelectedItems] = React.useState(['Melda Bevel']); const onSelectionChange = React.useCallback((_, data) => { setSelectedItems(data.selectedItems); @@ -103,7 +101,7 @@ export const ListActiveElement = () => { }, []); return ( -
    +
    {items.map(({ name, avatar }) => ( { /> ))} -
    +
    + Currently selected:{' '} {selectedItems[0]} - {selectedItems.length ? {selectedItems[0]} is a great person! : null}
    ); From 03647f925649166484f0e1a51b6bcbd19a48b263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Fri, 22 Mar 2024 13:59:24 +0100 Subject: [PATCH 118/136] add navigationMode prop and infer roles from it --- .../etc/react-list-preview.api.md | 2 +- .../src/components/List/List.cy.tsx | 14 ++--- .../src/components/List/List.test.tsx | 17 +++++- .../src/components/List/List.types.ts | 10 ++-- .../src/components/List/listContext.ts | 11 ++-- .../src/components/List/useList.ts | 34 ++++++++--- .../components/List/useListContextValues.ts | 7 ++- .../src/components/ListItem/useListItem.tsx | 24 ++++---- .../src/hooks/useListAccessibilityRoles.tsx | 60 ------------------- .../utils/calculateListItemRoleForListRole.ts | 17 ++++++ .../src/utils/calculateListRole.ts | 18 ++++++ .../react-list-preview/src/utils/index.ts | 4 ++ .../src/utils/validateGridCellsArePresent.ts | 21 +++++++ .../src/utils/validateProperRolesAreUsed.ts | 49 +++++++++++++++ .../List/ListActiveElement.stories.tsx | 31 ++++++---- ...ultipleActionsDifferentPrimary.stories.tsx | 1 + .../List/MultipleActionsSelection.stories.tsx | 4 ++ .../MultipleActionsWithPrimary.stories.tsx | 14 +++-- .../stories/List/SingleAction.stories.tsx | 4 +- .../List/SingleActionSelection.stories.tsx | 2 +- ...ualizedListWithActionableItems.stories.tsx | 2 +- 21 files changed, 221 insertions(+), 125 deletions(-) delete mode 100644 packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx create mode 100644 packages/react-components/react-list-preview/src/utils/calculateListItemRoleForListRole.ts create mode 100644 packages/react-components/react-list-preview/src/utils/calculateListRole.ts create mode 100644 packages/react-components/react-list-preview/src/utils/index.ts create mode 100644 packages/react-components/react-list-preview/src/utils/validateGridCellsArePresent.ts create mode 100644 packages/react-components/react-list-preview/src/utils/validateProperRolesAreUsed.ts diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 65c09b6e696aee..685c4843483c74 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -50,7 +50,7 @@ export type ListItemState = ComponentState & { // @public export type ListProps = ComponentProps & { - navigable?: boolean; + navigationMode?: ListNavigationMode; selectionMode?: SelectionMode_2; selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; diff --git a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx index 8f3860de9e844c..f850e8173e5a58 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.cy.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.cy.tsx @@ -36,7 +36,7 @@ const testSequence = (sequence: Array) => { const mountSimpleList = () => { mount( - + List Item 1 List Item 2 List Item 3 @@ -46,7 +46,7 @@ const mountSimpleList = () => { const mountListWithSecondaryActions = () => { mount( - + List Item 1 @@ -83,7 +83,7 @@ const SelectionTestList = ({ selectionMode, defaultSelectedItems, controlled }: return ( <> { }); describe('with focusable children', () => { - it('default list is grid/row', () => { + it('default list is grid/row for composite', () => { mount( - + List Item 1 @@ -411,7 +411,7 @@ describe('List', () => { it("single select list is grid/row and doesn't have multiselectable aria prop", () => { mount( - + List Item 1 @@ -429,7 +429,7 @@ describe('List', () => { it('multiple select list is grid/row and has multiselectable aria prop', () => { mount( - + List Item 1 diff --git a/packages/react-components/react-list-preview/src/components/List/List.test.tsx b/packages/react-components/react-list-preview/src/components/List/List.test.tsx index 4dc9a75bc22ac6..d4d1b4962a5f38 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.test.tsx +++ b/packages/react-components/react-list-preview/src/components/List/List.test.tsx @@ -305,9 +305,22 @@ describe('List', () => { firstItem.focus(); expect(document.activeElement).not.toBe(firstItem); }); - it('should be focusable when "navigable" is passed', () => { + it('should be focusable when "navigationMode" is "items"', () => { const result = render( - + + First ListItem + Second ListItem + , + ); + + const firstItem = result.getByText('First ListItem'); + firstItem.focus(); + expect(document.activeElement).toBe(firstItem); + }); + + it('should be focusable when "navigationMode" is "composite"', () => { + const result = render( + First ListItem Second ListItem , diff --git a/packages/react-components/react-list-preview/src/components/List/List.types.ts b/packages/react-components/react-list-preview/src/components/List/List.types.ts index 73de5106f5e628..8936e657cdc58d 100644 --- a/packages/react-components/react-list-preview/src/components/List/List.types.ts +++ b/packages/react-components/react-list-preview/src/components/List/List.types.ts @@ -10,7 +10,6 @@ import type { EventData, } from '@fluentui/react-utilities'; import type { ListSelectionState } from '../../hooks/types'; -import { ListAccessibilityRoles } from '../../hooks/useListAccessibilityRoles'; export type ListSlots = { root: NonNullable>; @@ -20,11 +19,13 @@ export type OnListSelectionChangeData = EventData<'change', React.SyntheticEvent selectedItems: SelectionItemId[]; }; +export type ListNavigationMode = 'items' | 'composite'; + /** * List Props */ export type ListProps = ComponentProps & { - navigable?: boolean; + navigationMode?: ListNavigationMode; selectionMode?: SelectionMode; selectedItems?: SelectionItemId[]; defaultSelectedItems?: SelectionItemId[]; @@ -32,10 +33,11 @@ export type ListProps = ComponentProps & { }; export type ListContextValue = { - navigable: boolean; + navigationMode: ListNavigationMode | undefined; selection?: ListSelectionState; as?: 'div' | 'ol' | 'ul'; - accessibilityRoles: ListAccessibilityRoles; + listItemRole: string; + validateListItems: (listItemElement: HTMLElement) => void; }; export type ListContextValues = { diff --git a/packages/react-components/react-list-preview/src/components/List/listContext.ts b/packages/react-components/react-list-preview/src/components/List/listContext.ts index 6f37229a0110bb..518b69d9c5d248 100644 --- a/packages/react-components/react-list-preview/src/components/List/listContext.ts +++ b/packages/react-components/react-list-preview/src/components/List/listContext.ts @@ -3,15 +3,12 @@ import type { ContextSelector } from '@fluentui/react-context-selector'; import { ListContextValue } from './List.types'; export const listContextDefaultValue: ListContextValue = { - navigable: false, + navigationMode: undefined, selection: undefined, as: undefined, - accessibilityRoles: { - listRole: 'list', - listItemRole: 'listitem', - setFocusableChildren: () => { - /* noop */ - }, + listItemRole: 'listitem', + validateListItems: () => { + /* noop */ }, }; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index a58be12cd260e9..c1c77e8537df7b 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -6,10 +6,15 @@ import { useControllableState, useEventCallback, } from '@fluentui/react-utilities'; -import { useArrowNavigationGroup } from '@fluentui/react-tabster'; +import { useArrowNavigationGroup, useFocusFinders } from '@fluentui/react-tabster'; import { ListProps, ListState } from './List.types'; import { useListSelection } from '../../hooks/useListSelection'; -import { useListAccessibilityRoles } from '../../hooks/useListAccessibilityRoles'; +import { + calculateListItemRoleForListRole, + calculateListRole, + validateGridCellsArePresent, + validateProperRolesAreUsed, +} from '../../utils'; const DEFAULT_ROOT_EL_TYPE = 'ul'; @@ -23,7 +28,8 @@ const DEFAULT_ROOT_EL_TYPE = 'ul'; * @param ref - reference to root HTMLElement of List */ export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { - const { navigable, selectionMode, selectedItems, defaultSelectedItems, as, onSelectionChange } = props; + const { navigationMode, selectionMode, selectedItems, defaultSelectedItems, as, onSelectionChange } = props; + const haveListItemsBeenValidated = React.useRef(false); const arrowNavigationAttributes = useArrowNavigationGroup({ axis: 'vertical', @@ -49,7 +55,20 @@ export const useList_unstable = (props: ListProps, ref: React.Ref { + if (!haveListItemsBeenValidated.current) { + const itemRole = listItemEl.getAttribute('role') || ''; + const focusable = findAllFocusable(listItemEl); + validateProperRolesAreUsed(listRole, itemRole, !!selectionMode, focusable.length > 0); + validateGridCellsArePresent(listRole, listItemEl); + haveListItemsBeenValidated.current = true; + } + }); return { components: { @@ -58,7 +77,7 @@ export const useList_unstable = (props: ListProps, ref: React.Ref, ): ListItemState => { const id = useId('listItem'); - const { value = id, onKeyDown, onClick, tabIndex } = props; + const { value = id, onKeyDown, onClick, tabIndex, role } = props; const toggleItem = useListContext_unstable(ctx => ctx.selection?.toggleItem); - const navigable = useListContext_unstable(ctx => ctx.navigable); + const navigationMode = useListContext_unstable(ctx => ctx.navigationMode); const isSelectionEnabled = useListContext_unstable(ctx => !!ctx.selection); const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)); - const listItemRole = useListContext_unstable(ctx => ctx.accessibilityRoles.listItemRole); - const setFocusableChildren = useListContext_unstable(ctx => ctx.accessibilityRoles.setFocusableChildren); + const listItemRole = useListContext_unstable(ctx => ctx.listItemRole); + const validateListItems = useListContext_unstable(ctx => ctx.validateListItems); - const { findAllFocusable } = useFocusFinders(); + const finalListItemRole = role || listItemRole; - const focusableItems = isSelectionEnabled || navigable || tabIndex === 0; + const focusableItems = Boolean(isSelectionEnabled || navigationMode || tabIndex === 0); const parentRenderedAs = useListContext_unstable(ctx => ctx.as); const renderedAs = props.as || DEFAULT_ROOT_EL_TYPE; @@ -73,10 +72,9 @@ export const useListItem_unstable = ( React.useEffect(() => { if (rootRef.current) { - const focusable = findAllFocusable(rootRef.current); - setFocusableChildren(focusable.length > 0); + validateListItems(rootRef.current); } - }, [findAllFocusable, setFocusableChildren]); + }, [validateListItems]); const focusableGroupAttrs = useFocusableGroup({ ignoreDefaultKeydown: { Enter: true }, @@ -144,8 +142,8 @@ export const useListItem_unstable = ( e.currentTarget.click(); } - // Handle entering the list item when user presses the ArrowRight - if (e.key === ArrowRight) { + // Handle entering the list item when user presses the ArrowRight, when composite navigation is enabled + if (e.key === ArrowRight && navigationMode === 'composite') { dispatchGroupperMoveFocusEvent(e.target as HTMLElement, TabsterTypes.GroupperMoveFocusActions.Enter); } }); @@ -175,7 +173,7 @@ export const useListItem_unstable = ( getIntrinsicElementProps(DEFAULT_ROOT_EL_TYPE, { ref: useMergedRefs(rootRef, ref) as React.Ref, tabIndex: focusableItems ? 0 : undefined, - role: listItemRole, + role: finalListItemRole, id: String(value), ...(isSelectionEnabled && { 'aria-selected': isSelected, diff --git a/packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx b/packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx deleted file mode 100644 index 42f7ddd313d86f..00000000000000 --- a/packages/react-components/react-list-preview/src/hooks/useListAccessibilityRoles.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; - -export type ListAccessibilityRoles = { - listRole: string; - listItemRole: string; - setFocusableChildren: (focusableItems: boolean) => void; -}; - -export function useListAccessibilityRoles(hasSelection: boolean): ListAccessibilityRoles { - const [hasFocusableChildren, setHasFocusableChildren] = React.useState(); - - const setFocusableChildren = React.useCallback( - (focusableItems: boolean) => { - if (hasFocusableChildren === undefined) { - setHasFocusableChildren(focusableItems); - } - }, - [hasFocusableChildren], - ); - - const listRole = React.useMemo(() => { - //not initialized, return "list" - if (hasFocusableChildren === undefined) { - return 'list'; - } - - if (hasFocusableChildren) { - return 'grid'; - } else { - if (hasSelection) { - return 'listbox'; - } else { - return 'list'; - } - } - }, [hasFocusableChildren, hasSelection]); - - const listItemRole = React.useMemo(() => { - //not initialized, return "listitem" - if (hasFocusableChildren === undefined) { - return 'listitem'; - } - - if (hasFocusableChildren) { - return 'row'; - } else { - if (hasSelection) { - return 'option'; - } else { - return 'listitem'; - } - } - }, [hasFocusableChildren, hasSelection]); - - return { - listRole, - listItemRole, - setFocusableChildren, - }; -} diff --git a/packages/react-components/react-list-preview/src/utils/calculateListItemRoleForListRole.ts b/packages/react-components/react-list-preview/src/utils/calculateListItemRoleForListRole.ts new file mode 100644 index 00000000000000..de4bb6055eb9b5 --- /dev/null +++ b/packages/react-components/react-list-preview/src/utils/calculateListItemRoleForListRole.ts @@ -0,0 +1,17 @@ +/** + * Calculate the role for the list item based on the list role. + * @param listRole - the role of the list + * @returns proper role for the list item + */ +export const calculateListItemRoleForListRole = (listRole: string): string => { + switch (listRole) { + case 'list': + return 'listitem'; + case 'listbox': + return 'option'; + case 'grid': + return 'row'; + default: + return 'listitem'; + } +}; diff --git a/packages/react-components/react-list-preview/src/utils/calculateListRole.ts b/packages/react-components/react-list-preview/src/utils/calculateListRole.ts new file mode 100644 index 00000000000000..aac2247a1fea4d --- /dev/null +++ b/packages/react-components/react-list-preview/src/utils/calculateListRole.ts @@ -0,0 +1,18 @@ +import { ListNavigationMode } from '../List'; + +/** + * Calculate the role for the list based on the navigation mode and selectable state + * @param navigationMode - the navigation mode of the list + * @param selectable - whether the list is selectable + * @returns 'grid' if navigationMode is 'composite', otherwise 'listbox' if selectable or 'list' if not + */ + +export const calculateListRole = (navigationMode: ListNavigationMode | undefined, selectable: boolean) => { + if (navigationMode === 'composite') { + return 'grid'; + } else if (selectable) { + return 'listbox'; + } else { + return 'list'; + } +}; diff --git a/packages/react-components/react-list-preview/src/utils/index.ts b/packages/react-components/react-list-preview/src/utils/index.ts new file mode 100644 index 00000000000000..8f5edd9fa7a797 --- /dev/null +++ b/packages/react-components/react-list-preview/src/utils/index.ts @@ -0,0 +1,4 @@ +export { calculateListRole } from './calculateListRole'; +export { validateProperRolesAreUsed } from './validateProperRolesAreUsed'; +export { calculateListItemRoleForListRole } from './calculateListItemRoleForListRole'; +export { validateGridCellsArePresent } from './validateGridCellsArePresent'; diff --git a/packages/react-components/react-list-preview/src/utils/validateGridCellsArePresent.ts b/packages/react-components/react-list-preview/src/utils/validateGridCellsArePresent.ts new file mode 100644 index 00000000000000..dc1c5d5f52269d --- /dev/null +++ b/packages/react-components/react-list-preview/src/utils/validateGridCellsArePresent.ts @@ -0,0 +1,21 @@ +/** + * Validates that grid cells are present in a grid list item. This is necessary for proper screen reader support. + * If grid cells are not present and we're not running in production mode, a warning will be logged to the console. + * @param listRole - The role of the list + * @param listItemEl - The list item element + * @returns + */ +export const validateGridCellsArePresent = (listRole: string, listItemEl: HTMLElement) => { + if (process.env.NODE_ENV === 'production' || listRole !== 'grid') { + return; + } + + const gridCells = listItemEl.querySelectorAll(':scope > [role="gridcell"]'); + if (gridCells.length === 0) { + //eslint-disable-next-line no-console + console.warn( + `@fluentui/react-list-preview [useList]:\nList items in List with "grid" role must contain at least one "gridcell" as direct child of for proper screen reader support.`, + `Ideally, each focus target should be in it's own "gridcell", which is a direct child of .\n`, + ); + } +}; diff --git a/packages/react-components/react-list-preview/src/utils/validateProperRolesAreUsed.ts b/packages/react-components/react-list-preview/src/utils/validateProperRolesAreUsed.ts new file mode 100644 index 00000000000000..1d60a7d6150ea9 --- /dev/null +++ b/packages/react-components/react-list-preview/src/utils/validateProperRolesAreUsed.ts @@ -0,0 +1,49 @@ +/** + * Validate that the proper roles are used for the given combination of roles and states. + * If the roles are invalid and we're not running in production mode, an warning will be logged to the console. + * + * @param role - the role of the list + * @param listItemRole - the role of the list item + * @param hasSelection - whether the list has selection enabled + * @param hasFocusableChildren - whether the list has focusable children + * @returns + */ +export const validateProperRolesAreUsed = ( + role: string, + listItemRole: string, + hasSelection: boolean, + hasFocusableChildren: boolean, +) => { + if (process.env.NODE_ENV === 'production') { + return; + } + + // Explode when the pair of roles is invalid + if (role === 'list' && listItemRole !== 'listitem') { + throw new Error('When role is "list", listItemRole must be "listitem".'); + } + if (role === 'listbox' && listItemRole !== 'option') { + throw new Error('When role is "listbox", listItemRole must be "option".'); + } + if (role === 'grid' && listItemRole !== 'row') { + throw new Error('When role is "grid", listItemRole must be "row".'); + } + + const expectedRole = (() => { + if (hasFocusableChildren) { + return 'grid'; + } else { + if (hasSelection) { + return 'listbox'; + } else { + return 'list'; + } + } + })(); + + if (role !== expectedRole) { + /* eslint-disable-next-line no-console */ + console.warn(`@fluentui/react-list-preview [useList]:\nThe role "${role}" does not match the expected role "${expectedRole}".\nPlease set the List role appropriately to ensure good accessibility.\nIf you are using this role intentionally, make sure to verify screen reader support. + `); + } +}; diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index f775b040204b7b..1730f84141de53 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -39,7 +39,7 @@ const useStyles = makeStyles({ selectedInfo: { marginTop: '16px', }, - button: { + buttonWrapper: { alignSelf: 'center', }, item: { @@ -60,6 +60,7 @@ const MyListItem = React.memo( -
    ); }, @@ -102,7 +104,12 @@ export const ListActiveElement = () => { return (
    - + {items.map(({ name, avatar }) => ( ) => { setSelectedItems(data.selectedItems)} > diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx index 978d48347ce1f6..ca9d72a6e5f45e 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx @@ -158,6 +158,7 @@ export const MultipleActionsSelection = (props: Partial) => { return ( setSelectedItems(data.selectedItems)} > @@ -183,6 +184,9 @@ MultipleActionsSelection.parameters = { 'Because the selection is the action on the item, to properly narrate the state of selection', 'we are using the role grid / row / gridcell here to properly announce when the selection on the', 'item is toggled.', + '', + 'To enable the user to navigate inside of the list items by pressing the `RightArrow` key,', + 'the `navigationMode` prop should be set to `composite`.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx index 4961ff94d1a178..95089379fc5132 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx @@ -72,14 +72,14 @@ const CardExample = (props: { title: string; value: string }) => { aria-label={value} onClick={() => alert('Primary action triggered!')} > -
    +
    Presentation Preview
    -
    +
    {props.title} You created 53m ago
    -
    +
    -
    +
    - -
    - - selection.setSelectedItems(data.selectedItems)} - > - {items.map(({ name, avatar }) => ( - - ))} - -
    Selected people: {selection.selectedItems.join(', ')}
    -
    - ); -}; - -ListSelectionControlledWithState.parameters = { - docs: { - description: { - story: [ - 'This example is an extension of the previous example of controlled selection. ', - 'It shows how to use the `useListSelection` hook to control the selection state. This hook is also used ', - ' internally when the selection is used in uncontrolled mode.', - '', - 'The `useListSelection` hook is by no means required for the selection to work, but it provides a convenient ', - 'way to control the selection state by providing selection specific helper functions.', - '', - 'While only the list of selected items is required to control the selection state, using the hook to ', - 'manage the selection state can be benefitial by providing selection specific helper functions.', - ].join('\n'), - }, - }, -}; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index d31e9c0a524caf..b0bad94eb1206c 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -67,22 +67,25 @@ const useStyles = makeStyles({ secondaryAction: { ...shorthands.gridArea('secondary_action') }, }); -const CardExample = (props: { title: string; value: string; selected?: boolean }) => { +const CustomListItem = (props: { title: string; value: string; selected?: boolean }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; + // This will be triggered by user pressing Enter or clicking on the list item + const onAction = React.useCallback(event => { + // This prevents the change in selection on click/Enter + event.preventDefault(); + alert(`Triggered custom action!`); + }, []); + return ( { - // Prevent the default behavior - toggling the selection - e.preventDefault(); - alert('Primary action triggered!'); - }} + onAction={onAction} >
    Presentation Preview @@ -97,7 +100,6 @@ const CardExample = (props: { title: string; value: string; selected?: boolean } aria-label="Install" onClick={e => { e.preventDefault(); - e.stopPropagation(); alert('Installing!'); }} > @@ -110,7 +112,6 @@ const CardExample = (props: { title: string; value: string; selected?: boolean }
    @@ -75,22 +49,32 @@ export const ListSelectionControlledBasic = () => { onSelectionChange={(_, data) => setSelectedItems(data.selectedItems)} > {items.map(({ name, avatar }) => ( - + + + ))} -
    Selected people: {selectedItems.join(', ')}
    ); }; -ListSelectionControlledBasic.parameters = { +SingleActionSelectionControlled.parameters = { docs: { description: { story: [ 'This example shows how to use the `selectedItems` and `onSelectionChange`', - 'props to control the selection state.', + 'props to control the selection state of the List and keep track of it in the parent component.', '', - 'This is a basic example how selection can be controlled with a simple array of selected values in a state.', + 'This is more in line with how we expect the selection to be used in production environment.', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx index 078e46fe40584d..8a0a1ffe35112d 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx @@ -1,4 +1,4 @@ -import { makeStyles, Persona, shorthands, SelectionItemId } from '@fluentui/react-components'; +import { Persona, SelectionItemId } from '@fluentui/react-components'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; @@ -24,63 +24,37 @@ const items: Item[] = names.map(name => ({ 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/persona-male.png', })); -const useStyles = makeStyles({ - wrapper: { - maxHeight: '300px', - overflowY: 'auto', - ...shorthands.padding('2px'), - }, - button: { - ...shorthands.padding(0), - }, -}); - -// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. -const MyListItem = React.memo(({ name, avatar }: { name: string; avatar: string }) => { - const onAction = React.useCallback( - event => { - // This prevents the change in selection on click/Enter - event.preventDefault(); - alert(`Triggered action on ${name}`); - }, - [name], - ); - return ( - - - - ); -}); - export const SingleActionSelectionDifferentPrimary = () => { - const classes = useStyles(); + const [selectedItems, setSelectedItems] = React.useState(['Demetra Manwaring', 'Bart Merrill']); - const defaultSelectedItems = ['Demetra Manwaring', 'Bart Merrill']; - - const [selectedItems, setSelectedItems] = React.useState(defaultSelectedItems); + // This will be triggered by user pressing Enter or clicking on the list item + const onAction = React.useCallback(event => { + // This prevents the change in selection on click/Enter + event.preventDefault(); + alert(`Triggered custom action!`); + }, []); return ( -
    - setSelectedItems(data.selectedItems)} - > - {items.map(({ name, avatar }) => ( - - ))} - -
    Selected people: {selectedItems.join(', ')}
    -
    + setSelectedItems(data.selectedItems)} + > + {items.map(({ name, avatar }) => ( + + + + ))} + ); }; @@ -89,18 +63,18 @@ SingleActionSelectionDifferentPrimary.parameters = { description: { story: [ 'This example is similar to the previous one, but it implements a custom primary action on `ListItem`,', - 'allowing us to trigger a different action than the selection when the user clicks on the list item or ', - 'presses Enter. This is useful when you want to have a primary action on the list item, but still want ', + 'allowing us to trigger a', + '__different action than the selection when the user clicks on the list item or presses Enter__', + '. This is useful when you want to have a primary action on the list item, but still want ', 'to allow the user to select it.', '', 'To change the default action on the `ListItem` (when user clicks on it or presses Enter), you can use the', - '`onAction` prop. By callign `event.preventDefault()` in the `onAction` callback, you can prevent the default', + '`onAction` prop. By calling `event.preventDefault()` in the `onAction` callback, you can prevent the default', 'action (toggling the selection) from happening. This way, you can perform a completely custom action.', 'In this example, the custom action is an alert that triggers when the user', 'clicks on the list item or presses Enter.', '', - '', - 'The selection can still be toggled by clicking on the checkbox or pressing `Space` when the item is focused.', + '__The selection can still be toggled by clicking on the checkbox or pressing `Space` when the item is focused.__', ].join('\n'), }, }, diff --git a/packages/react-components/react-list-preview/stories/List/index.stories.tsx b/packages/react-components/react-list-preview/stories/List/index.stories.tsx index e99fda11c56cb5..a90149cbbdc23b 100644 --- a/packages/react-components/react-list-preview/stories/List/index.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/index.stories.tsx @@ -6,12 +6,11 @@ import bestPracticesMd from './ListBestPractices.md'; export { Default } from './ListDefault.stories'; export { SingleAction } from './SingleAction.stories'; export { SingleActionSelection } from './SingleActionSelection.stories'; +export { SingleActionSelectionControlled } from './SingleActionSelectionControlled.stories'; export { SingleActionSelectionDifferentPrimary } from './SingleActionSelectionDifferentPrimary.stories'; export { MultipleActionsWithPrimary } from './MultipleActionsWithPrimary.stories'; export { MultipleActionsSelection } from './MultipleActionsSelection.stories'; export { MultipleActionsDifferentPrimary } from './MultipleActionsDifferentPrimary.stories'; -export { ListSelectionControlledBasic } from './ListSelectionControlledBasic.stories'; -export { ListSelectionControlledWithState } from './ListSelectionControlledWithState.stories'; export { VirtualizedList } from './VirtualizedList.stories'; export { VirtualizedListWithActionableItems } from './VirtualizedListWithActionableItems.stories'; export { ListActiveElement } from './ListActiveElement.stories'; From 0e5465c85eb82913048fa0f1ffa43d2f62c4f1b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 28 Mar 2024 10:09:08 +0100 Subject: [PATCH 134/136] prepare for a separate release --- apps/public-docsite-v9/package.json | 1 - ...-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json | 7 ------- packages/react-components/react-list-preview/package.json | 1 + 3 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json diff --git a/apps/public-docsite-v9/package.json b/apps/public-docsite-v9/package.json index 4d9fdc2f85fd59..cbdfe313f278ad 100644 --- a/apps/public-docsite-v9/package.json +++ b/apps/public-docsite-v9/package.json @@ -34,7 +34,6 @@ "@fluentui/react-storybook-addon": "*", "@fluentui/react-storybook-addon-export-to-sandbox": "*", "@fluentui/theme-designer": "*", - "@fluentui/react-list-preview": "*", "@fluentui/react-search-preview": "*", "@fluentui/react-swatch-picker-preview": "*", "@fluentui/react-motion-preview": "*", diff --git a/change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json b/change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json deleted file mode 100644 index 41a98e175bd637..00000000000000 --- a/change/@fluentui-react-list-preview-0f946870-6a40-4785-b533-51e92b000e85.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "List: unmark private", - "packageName": "@fluentui/react-list-preview", - "email": "jirivyhnalek@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 91c07d3d6cf04c..2488c233328d2d 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -1,6 +1,7 @@ { "name": "@fluentui/react-list-preview", "version": "0.0.1", + "private": true, "description": "React List v9", "main": "lib-commonjs/index.js", "module": "lib/index.js", From 8a74d6560597ad4f8d891bfa2905e30a07c3bb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 28 Mar 2024 10:57:25 +0100 Subject: [PATCH 135/136] update stories --- .../react-list-preview/package.json | 2 +- .../List/ListActiveElement.stories.tsx | 86 ++++++++----------- ...ultipleActionsDifferentPrimary.stories.tsx | 33 ++++--- .../List/MultipleActionsSelection.stories.tsx | 33 ++++--- .../MultipleActionsWithPrimary.stories.tsx | 13 ++- .../List/SingleActionSelection.stories.tsx | 17 ++-- ...ingleActionSelectionControlled.stories.tsx | 17 ++-- ...ctionSelectionDifferentPrimary.stories.tsx | 17 ++-- .../stories/List/VirtualizedList.stories.tsx | 2 +- ...ualizedListWithActionableItems.stories.tsx | 1 - 10 files changed, 114 insertions(+), 107 deletions(-) diff --git a/packages/react-components/react-list-preview/package.json b/packages/react-components/react-list-preview/package.json index 2488c233328d2d..b78e1d5aa3568a 100644 --- a/packages/react-components/react-list-preview/package.json +++ b/packages/react-components/react-list-preview/package.json @@ -1,6 +1,6 @@ { "name": "@fluentui/react-list-preview", - "version": "0.0.1", + "version": "0.0.0", "private": true, "description": "React List v9", "main": "lib-commonjs/index.js", diff --git a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx index 1730f84141de53..1fb360ab26fb11 100644 --- a/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/ListActiveElement.stories.tsx @@ -12,15 +12,6 @@ import { Mic16Regular } from '@fluentui/react-icons'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; -const names = [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', - 'Kristan Cable', -]; type Item = { name: string; @@ -28,7 +19,15 @@ type Item = { avatar: string; }; -const items: Item[] = names.map(name => ({ +const items: Item[] = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', + 'Kristan Cable', +].map(name => ({ name, id: name, avatar: @@ -52,39 +51,6 @@ const useStyles = makeStyles({ }, }); -// Memoizing the ListItem like this allows the unaffected ListItem not to be re-rendered when the selection changes. -const MyListItem = React.memo( - ({ name, avatar, ...rest }: React.ComponentProps & { name: string; avatar: string }) => { - const styles = useStyles(); - return ( - - -
    -
    -
    - ); - }, -); - export const ListActiveElement = () => { const classes = useStyles(); @@ -111,14 +77,38 @@ export const ListActiveElement = () => { onSelectionChange={onSelectionChange} > {items.map(({ name, avatar }) => ( - + aria-label={name} + onFocus={onFocus} + checkmark={null} + > + +
    +
    + ))}
    diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx index b0bad94eb1206c..3309feb7f953f9 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsDifferentPrimary.stories.tsx @@ -1,6 +1,7 @@ import { Button, Caption1, + Image, makeResetStyles, makeStyles, Menu, @@ -13,8 +14,8 @@ import { Text, tokens, } from '@fluentui/react-components'; -import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; @@ -45,6 +46,7 @@ const useStyles = makeStyles({ color: tokens.colorNeutralForeground3, }, image: { + height: '160px', maxWidth: '100%', ...shorthands.borderRadius('5px'), }, @@ -67,7 +69,7 @@ const useStyles = makeStyles({ secondaryAction: { ...shorthands.gridArea('secondary_action') }, }); -const CustomListItem = (props: { title: string; value: string; selected?: boolean }) => { +const CustomListItem = (props: { title: string; value: string }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; @@ -88,7 +90,12 @@ const CustomListItem = (props: { title: string; value: string; selected?: boolea onAction={onAction} >
    - Presentation Preview + Presentation Preview
    {props.title} @@ -153,7 +160,7 @@ const CustomListItem = (props: { title: string; value: string; selected?: boolea ); }; -export const MultipleActionsDifferentPrimary = (props: Partial) => { +export const MultipleActionsDifferentPrimary = () => { const classes = useStyles(); const [selectedItems, setSelectedItems] = React.useState>([]); @@ -166,15 +173,15 @@ export const MultipleActionsDifferentPrimary = (props: Partial) => { selectedItems={selectedItems} onSelectionChange={(e, data) => setSelectedItems(data.selectedItems)} > - - - - - - - - - + + + + + + + + + ); }; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx index 3848788cb41533..c6c16c615b3eb5 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsSelection.stories.tsx @@ -1,6 +1,7 @@ import { Button, Caption1, + Image, makeResetStyles, makeStyles, Menu, @@ -13,8 +14,8 @@ import { Text, tokens, } from '@fluentui/react-components'; -import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; @@ -45,6 +46,7 @@ const useStyles = makeStyles({ color: tokens.colorNeutralForeground3, }, image: { + height: '160px', maxWidth: '100%', ...shorthands.borderRadius('5px'), }, @@ -67,7 +69,7 @@ const useStyles = makeStyles({ secondaryAction: { ...shorthands.gridArea('secondary_action') }, }); -const CustomListItem = (props: { title: string; value: string; selected?: boolean }) => { +const CustomListItem = (props: { title: string; value: string }) => { const listItemStyles = useListItemRootStyles(); const styles = useStyles(); const { value } = props; @@ -80,7 +82,12 @@ const CustomListItem = (props: { title: string; value: string; selected?: boolea aria-label={value} >
    - Presentation Preview + Presentation Preview
    {props.title} @@ -145,7 +152,7 @@ const CustomListItem = (props: { title: string; value: string; selected?: boolea ); }; -export const MultipleActionsSelection = (props: Partial) => { +export const MultipleActionsSelection = () => { const classes = useStyles(); const [selectedItems, setSelectedItems] = React.useState>([]); @@ -158,15 +165,15 @@ export const MultipleActionsSelection = (props: Partial) => { selectedItems={selectedItems} onSelectionChange={(e, data) => setSelectedItems(data.selectedItems)} > - - - - - - - - - + + + + + + + + + ); }; diff --git a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx index 7c3c62736c5e80..dfeeb1d40d69db 100644 --- a/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/MultipleActionsWithPrimary.stories.tsx @@ -1,6 +1,7 @@ import { Button, Caption1, + Image, makeResetStyles, makeStyles, Menu, @@ -14,7 +15,7 @@ import { tokens, } from '@fluentui/react-components'; import { MoreHorizontal20Regular } from '@fluentui/react-icons'; -import { List, ListItem, ListProps } from '@fluentui/react-list-preview'; +import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; @@ -45,6 +46,7 @@ const useStyles = makeStyles({ color: tokens.colorNeutralForeground3, }, image: { + height: '160px', maxWidth: '100%', ...shorthands.borderRadius('5px'), }, @@ -81,7 +83,12 @@ const CustomListItem = (props: { title: string; value: string }) => { onAction={onAction} >
    - Presentation Preview + Presentation Preview
    {props.title} @@ -144,7 +151,7 @@ const CustomListItem = (props: { title: string; value: string }) => { ); }; -export const MultipleActionsWithPrimary = (props: Partial) => { +export const MultipleActionsWithPrimary = () => { const classes = useStyles(); return ( diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx index eda937caaed17f..cefa248fccadeb 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelection.stories.tsx @@ -2,14 +2,6 @@ import { Persona } from '@fluentui/react-components'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; -const names = [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', -]; type Item = { name: string; @@ -17,7 +9,14 @@ type Item = { avatar: string; }; -const items: Item[] = names.map(name => ({ +const items: Item[] = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', +].map(name => ({ name, id: name, avatar: diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionControlled.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionControlled.stories.tsx index b0c7ba4765df2a..ce613707178b38 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionControlled.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionControlled.stories.tsx @@ -2,14 +2,6 @@ import { Button, makeStyles, Persona, SelectionItemId } from '@fluentui/react-co import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; -const names = [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', -]; type Item = { name: string; @@ -17,7 +9,14 @@ type Item = { avatar: string; }; -const items: Item[] = names.map(name => ({ +const items: Item[] = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', +].map(name => ({ name, id: name, avatar: diff --git a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx index 8a0a1ffe35112d..bbce803e41e5a1 100644 --- a/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/SingleActionSelectionDifferentPrimary.stories.tsx @@ -2,14 +2,6 @@ import { Persona, SelectionItemId } from '@fluentui/react-components'; import { List, ListItem } from '@fluentui/react-list-preview'; import * as React from 'react'; -const names = [ - 'Melda Bevel', - 'Demetra Manwaring', - 'Eusebia Stufflebeam', - 'Israel Rabin', - 'Bart Merrill', - 'Sonya Farner', -]; type Item = { name: string; @@ -17,7 +9,14 @@ type Item = { avatar: string; }; -const items: Item[] = names.map(name => ({ +const items: Item[] = [ + 'Melda Bevel', + 'Demetra Manwaring', + 'Eusebia Stufflebeam', + 'Israel Rabin', + 'Bart Merrill', + 'Sonya Farner', +].map(name => ({ name, id: name, avatar: diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx index 9f42625d04589f..588f4df9cb4dc5 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedList.stories.tsx @@ -216,7 +216,7 @@ export const VirtualizedList = () => { outerElementType={CountriesList} > {({ index, style, data }) => ( - + {data[index]} )} diff --git a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx index ed4cd21a185991..2e3011d41cf4aa 100644 --- a/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx +++ b/packages/react-components/react-list-preview/stories/List/VirtualizedListWithActionableItems.stories.tsx @@ -218,7 +218,6 @@ export const VirtualizedListWithActionableItems = () => { {({ index, style, data }) => ( alert(data[index])} From ccb10f1b0ead28aa0f569a8f45af95b01a925c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Vyhn=C3=A1lek?= Date: Thu, 28 Mar 2024 11:25:46 +0100 Subject: [PATCH 136/136] Bernardos feedback --- .../react-list-preview/etc/react-list-preview.api.md | 2 +- .../react-list-preview/src/components/List/useList.ts | 5 ++++- .../src/components/ListItem/ListItem.test.tsx | 2 +- .../src/components/ListItem/renderListItem.tsx | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-list-preview/etc/react-list-preview.api.md b/packages/react-components/react-list-preview/etc/react-list-preview.api.md index 17c8e1518541ab..e6d8e41b062623 100644 --- a/packages/react-components/react-list-preview/etc/react-list-preview.api.md +++ b/packages/react-components/react-list-preview/etc/react-list-preview.api.md @@ -72,7 +72,7 @@ export const renderList_unstable: (state: ListState, contextValues: ListContextV export const renderListItem_unstable: (state: ListItemState) => JSX.Element; // @public -export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; +export const useList_unstable: (props: ListProps, ref: React_2.Ref) => ListState; // @public export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref) => ListItemState; diff --git a/packages/react-components/react-list-preview/src/components/List/useList.ts b/packages/react-components/react-list-preview/src/components/List/useList.ts index 4abba40dc67edc..a8a1b8370c8fe6 100644 --- a/packages/react-components/react-list-preview/src/components/List/useList.ts +++ b/packages/react-components/react-list-preview/src/components/List/useList.ts @@ -28,7 +28,10 @@ const DEFAULT_ROOT_EL_TYPE = 'ul'; * @param props - props from this instance of List * @param ref - reference to root HTMLElement of List */ -export const useList_unstable = (props: ListProps, ref: React.Ref): ListState => { +export const useList_unstable = ( + props: ListProps, + ref: React.Ref, +): ListState => { const { navigationMode, selectionMode, diff --git a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx index 1ddb48795553f0..28096802579b47 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/ListItem.test.tsx @@ -5,7 +5,7 @@ import { ListItem } from './ListItem'; import { ListItemProps } from './ListItem.types'; describe('ListItem', () => { - isConformant({ + isConformant({ Component: ListItem as React.FunctionComponent, displayName: 'ListItem', testOptions: { diff --git a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx index 78c2f6103550c3..a9cb06033c966a 100644 --- a/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx +++ b/packages/react-components/react-list-preview/src/components/ListItem/renderListItem.tsx @@ -12,7 +12,7 @@ export const renderListItem_unstable = (state: ListItemState) => { return ( - {state.checkmark ? : null} + {state.checkmark && } {state.root.children} );