From 5fd5d4eb186080fb4af7bb0b1771a7c2da140306 Mon Sep 17 00:00:00 2001 From: petdud Date: Mon, 7 Aug 2023 12:51:11 +0200 Subject: [PATCH 1/2] inifnite scrolling + update actions --- .../stories/Tree/TreeActions.stories.tsx | 5 +- .../stories/Tree/TreeBestPractices.md | 5 +- .../Tree/TreeInfiniteScrolling.stories.tsx | 54 +++++++++++-------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/packages/react-components/react-tree/stories/Tree/TreeActions.stories.tsx b/packages/react-components/react-tree/stories/Tree/TreeActions.stories.tsx index 3738e6928eeac2..1625946ddce1f6 100644 --- a/packages/react-components/react-tree/stories/Tree/TreeActions.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/TreeActions.stories.tsx @@ -39,6 +39,7 @@ const ActionableTreeItem = ({ children, ...rest }: TreeItemProps) => { const [layout, subtree] = React.Children.toArray(children); return ( { if (e.isDefaultPrevented()) { return; @@ -111,9 +112,9 @@ export const Actions = () => { Actions.parameters = { docs: { description: { - story: `In addition to \`aside\` content, both tree item layouts support \`actions\` prop that can be used for tasks such as edit, rename, or triggering a menu. These action buttons are initially hidden but are shown on hover or can be controlled by the \`visible\` property, ensuring they take priority over the \`aside\` content when needed. + story: `In addition to the \`aside\` content, both tree item layouts support \`actions\` prop that can be used for tasks such as edit, rename, or triggering a menu. These action buttons are initially hidden but are shown on hover or can be controlled by the \`visible\` property, ensuring they take priority over the \`aside\` content when needed. -> ⚠️ Actions prop is \`aria-hidden\` by default. Always include a context menu to ensure that actions are accessible for all users.`, +> ⚠️ The \`actions\` prop is \`aria-hidden\` by default. Always implement a context menu to ensure that these actions are accessible to all users. Additionally, include an \`aria-description\` or \`aria-describedby\` on tree items with actions or context menus to indicate interactions, such as "has context menu."`, }, }, }; diff --git a/packages/react-components/react-tree/stories/Tree/TreeBestPractices.md b/packages/react-components/react-tree/stories/Tree/TreeBestPractices.md index 2a047f3d7602b6..db0eda833f6d92 100644 --- a/packages/react-components/react-tree/stories/Tree/TreeBestPractices.md +++ b/packages/react-components/react-tree/stories/Tree/TreeBestPractices.md @@ -13,6 +13,9 @@ - **Use the `aria-label` attribute on the root of the `Tree` component to provide an accessible name for the tree:** This attribute helps screen readers to understand the purpose of the tree, making it more accessible and inclusive. -- **If you provide additional buttons or functionality within tree items, make them accessible with a context menu:** Ensure that any additional actions or features in tree items are accessible for both mouse and keyboard users by implementing a context menu. The additional functionality should not disturb keyboard navigation within the tree. +- **If you provide additional buttons or functionality within tree items, make them accessible with a context menu:** + - Ensure that any additional actions or features in tree items are accessible for both mouse and keyboard users by implementing a context menu. + - Include an `aria-description` or `aria-describedby` on tree items with actions/context menu to offer more descriptive information about the interactions, e.g., "has context menu." + - The added functionality should not disrupt keyboard navigation within the tree. diff --git a/packages/react-components/react-tree/stories/Tree/TreeInfiniteScrolling.stories.tsx b/packages/react-components/react-tree/stories/Tree/TreeInfiniteScrolling.stories.tsx index 4e312e9c70b8ac..922fdcce4fdcc9 100644 --- a/packages/react-components/react-tree/stories/Tree/TreeInfiniteScrolling.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/TreeInfiniteScrolling.stories.tsx @@ -8,13 +8,8 @@ import { } from '@fluentui/react-tree'; import { makeStyles, shorthands, Spinner } from '@fluentui/react-components'; -interface Result { - results: { name: string }[]; -} - -type Item = HeadlessFlatTreeItemProps & { name: string | React.ReactNode }; - -const MAX_PAGES = 3; +const ITEMS_PER_PAGE = 10; +const MAX_PAGES = 4; const pinnedItems = [ { value: 'pinned', name: 'Pinned', id: 'pinned' }, @@ -23,6 +18,12 @@ const pinnedItems = [ { value: 'pinned-item-3', parentValue: 'pinned', name: 'Pinned item 3' }, ]; +interface Result { + results: { name: string }[]; +} + +type Item = HeadlessFlatTreeItemProps & { name: string | React.ReactNode }; + const useStyles = makeStyles({ container: { height: '400px', @@ -36,7 +37,7 @@ export const InfiniteScrolling = () => { const [isLoading, setIsLoading] = React.useState(false); const peopleItems = useQuery([ { value: 'people', name: 'People' }, - ...Array.from({ length: 10 }, (_, index) => ({ + ...Array.from({ length: ITEMS_PER_PAGE }, (_, index) => ({ value: `person-${index + 1}`, parentValue: 'people', name: `Person ${index + 1}`, @@ -67,19 +68,17 @@ export const InfiniteScrolling = () => { const fetchMoreItems = () => { setIsLoading(true); - fetch(`https://swapi.dev/api/people?page=${page}`) - .then(res => res.json()) - .then((json: Result) => { - const fetchedItems = json.results.map(person => ({ - value: `person-${person.name}`, - parentValue: 'people', - name: person.name, - })); - - setIsLoading(false); - setPage(page + 1); - peopleItems.query(() => [...peopleItems.value, ...fetchedItems]); - }); + mockFetchPeople(page).then((json: Result) => { + const fetchedItems = json.results.map(person => ({ + value: `person-${person.name}`, + parentValue: 'people', + name: person.name, + })); + + setIsLoading(false); + setPage(prev => prev + 1); + peopleItems.query(() => [...peopleItems.value, ...fetchedItems]); + }); }; const handleScroll = (event: React.UIEvent) => { @@ -120,6 +119,19 @@ function useQuery(initialValue: Value) { return { ...queryResult, query } as const; } +function mockFetchPeople(page: number): Promise { + return new Promise(resolve => { + setTimeout(() => { + const startIndex = page * ITEMS_PER_PAGE + 1; + const results = Array.from({ length: ITEMS_PER_PAGE }, (_, index) => ({ + name: `Person ${startIndex + index}`, + })); + + resolve({ results }); + }, 1000); + }); +} + InfiniteScrolling.parameters = { docs: { description: { From 5b95fd8c0bf5ebc76bd9be909e8ec33fc6f415d9 Mon Sep 17 00:00:00 2001 From: petdud Date: Mon, 7 Aug 2023 13:44:16 +0200 Subject: [PATCH 2/2] lazy loading and actions --- .../stories/Tree/FlatTree.stories.tsx | 95 ++++++++--- .../stories/Tree/TreeActions.stories.tsx | 152 +++++++++--------- .../stories/Tree/TreeLazyLoading.stories.tsx | 29 ++-- 3 files changed, 169 insertions(+), 107 deletions(-) diff --git a/packages/react-components/react-tree/stories/Tree/FlatTree.stories.tsx b/packages/react-components/react-tree/stories/Tree/FlatTree.stories.tsx index c494ff949fe379..d721e01387b357 100644 --- a/packages/react-components/react-tree/stories/Tree/FlatTree.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/FlatTree.stories.tsx @@ -8,57 +8,67 @@ import { useHeadlessFlatTree_unstable, HeadlessFlatTreeItemProps, } from '@fluentui/react-tree'; +import { + Button, + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + useRestoreFocusTarget, +} from '@fluentui/react-components'; +import { Edit20Regular, MoreHorizontal20Regular } from '@fluentui/react-icons'; -type FlatItem = HeadlessFlatTreeItemProps & { layout: string }; +type FlatItem = HeadlessFlatTreeItemProps & { content: string }; const flatTreeItems: FlatItem[] = [ - { value: '1', layout: 'Level 1, item 1' }, - { value: '1-1', parentValue: '1', layout: 'Level 2, item 1' }, - { value: '1-2', parentValue: '1', layout: 'Level 2, item 2' }, - { value: '1-3', parentValue: '1', layout: 'Level 2, item 3' }, - { value: '2', layout: 'Level 1, item 2' }, - { value: '2-1', parentValue: '2', layout: 'Level 2, item 1' }, - { value: '2-1-1', parentValue: '2-1', layout: 'Level 3, item 1' }, - { value: '2-1-1-1', parentValue: '2-1-1', layout: 'Level 4, item 1' }, + { value: '1', content: 'Level 1, item 1' }, + { value: '1-1', parentValue: '1', content: 'Level 2, item 1' }, + { value: '1-2', parentValue: '1', content: 'Level 2, item 2' }, + { value: '1-3', parentValue: '1', content: 'Level 2, item 3' }, + { value: '2', content: 'Level 1, item 2' }, + { value: '2-1', parentValue: '2', content: 'Level 2, item 1' }, + { value: '2-1-1', parentValue: '2-1', content: 'Level 3, item 1' }, + { value: '2-1-1-1', parentValue: '2-1-1', content: 'Level 4, item 1' }, ]; // // EXAMPLE OF NESTED TREE ITEMS BEING FLATTEN BY `flattenTree`: -// type Item = TreeItemProps & { layout: React.ReactNode }; +// type Item = TreeItemProps & { content: React.ReactNode }; // const nestedTreeItems = [ // { // value: '1', -// layout: <>level 1, item 1, +// content: <>level 1, item 1, // subtree: [ // { // value: '1-1', -// layout: <>level 2, item 1, +// content: <>level 2, item 1, // }, // { // value: '1-2', -// layout: <>level 2, item 2, +// content: <>level 2, item 2, // }, // { // value: '1-3', -// layout: <>level 2, item 3, +// content: <>level 2, item 3, // }, // ], // }, // { // value: '2', -// layout: <>level 1, item 2, +// content: <>level 1, item 2, // subtree: [ // { // value: '2-1', -// layout: <>level 2, item 1, +// content: <>level 2, item 1, // subtree: [ // { // value: '2-1-1', -// layout: <>level 3, item 1, +// content: <>level 3, item 1, // subtree: [ // { // value: '2-1-1-1', -// layout: <>level 4, item 1, +// content: <>level 4, item 1, // }, // ], // }, @@ -70,16 +80,51 @@ const flatTreeItems: FlatItem[] = [ // const flatTreeItems = flattenTree_unstable(nestedTreeItems); +const ActionsExample = () => ( + <> +