From 3cf92ffe784a44c16541a130a7ef3e11bc2689ae Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Tue, 11 Jul 2023 11:56:51 +0000 Subject: [PATCH 1/2] chore: move slots back to TreeItem and creates slot context --- ...-1c832b76-193f-45ac-a4a1-6241a3627f1c.json | 7 + .../react-tree/etc/react-tree.api.md | 26 +-- .../src/components/Tree/Tree.cy.tsx | 17 +- .../src/components/TreeItem/TreeItem.types.ts | 33 +++- .../components/TreeItem/renderTreeItem.tsx | 10 +- .../{useTreeItem.ts => useTreeItem.tsx} | 64 +++++-- .../TreeItem/useTreeItemContextValues.ts | 21 ++- .../TreeItem/useTreeItemStyles.styles.ts | 4 +- .../TreeItemLayout/TreeItemLayout.test.tsx | 11 +- .../TreeItemLayout/TreeItemLayout.types.ts | 27 +-- .../TreeItemLayout/renderTreeItemLayout.tsx | 4 +- .../TreeItemLayout/useTreeItemLayout.ts | 45 +++++ .../TreeItemLayout/useTreeItemLayout.tsx | 73 -------- .../useTreeItemLayoutStyles.styles.ts | 4 +- .../TreeItemPersonaLayout.test.tsx | 11 +- .../TreeItemPersonaLayout.types.ts | 9 +- .../renderTreeItemPersonaLayout.tsx | 4 +- ...Layout.tsx => useTreeItemPersonaLayout.ts} | 0 .../useTreeItemPersonaLayoutStyles.styles.ts | 4 +- .../react-tree/src/contexts/index.ts | 1 + .../src/contexts/treeItemContext.ts | 8 - .../src/contexts/treeItemSlotsContext.ts | 18 ++ .../B_TreeItem/TreeItemActions.stories.tsx | 44 ++--- .../B_TreeItem/TreeItemExpandIcon.stories.tsx | 30 ++-- .../C_Layouts/LayoutsDefault.stories.tsx | 159 +++++++++--------- .../C_Layouts/TreeItemLayout.stories.tsx | 20 ++- .../C_Layouts/TreeItemLayoutAside.stories.tsx | 52 +++--- .../TreeItemPersonaLayout.stories.tsx | 44 ++--- ...reeItemPersonaLayoutReadUnread.stories.tsx | 7 +- .../D_flatTree/TreeItemAddRemove.stories.tsx | 30 ++-- .../D_flatTree/TreeLazyLoading.stories.tsx | 4 +- 31 files changed, 432 insertions(+), 359 deletions(-) create mode 100644 change/@fluentui-react-tree-1c832b76-193f-45ac-a4a1-6241a3627f1c.json rename packages/react-components/react-tree/src/components/TreeItem/{useTreeItem.ts => useTreeItem.tsx} (71%) create mode 100644 packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.ts delete mode 100644 packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.tsx rename packages/react-components/react-tree/src/components/TreeItemPersonaLayout/{useTreeItemPersonaLayout.tsx => useTreeItemPersonaLayout.ts} (100%) create mode 100644 packages/react-components/react-tree/src/contexts/treeItemSlotsContext.ts diff --git a/change/@fluentui-react-tree-1c832b76-193f-45ac-a4a1-6241a3627f1c.json b/change/@fluentui-react-tree-1c832b76-193f-45ac-a4a1-6241a3627f1c.json new file mode 100644 index 00000000000000..44375d0e64aa59 --- /dev/null +++ b/change/@fluentui-react-tree-1c832b76-193f-45ac-a4a1-6241a3627f1c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "chore: move slots back to TreeItem and creates slot context", + "packageName": "@fluentui/react-tree", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-tree/etc/react-tree.api.md b/packages/react-components/react-tree/etc/react-tree.api.md index 66f4cac3c7359d..30ece2cf07a640 100644 --- a/packages/react-components/react-tree/etc/react-tree.api.md +++ b/packages/react-components/react-tree/etc/react-tree.api.md @@ -98,13 +98,13 @@ export type TreeContextValue = { export const TreeItem: ForwardRefComponent; // @public (undocumented) -export const treeItemClassNames: SlotClassNames; +export const treeItemClassNames: SlotClassNames; // @public export const TreeItemLayout: ForwardRefComponent; // @public (undocumented) -export const treeItemLayoutClassNames: SlotClassNames; +export const treeItemLayoutClassNames: SlotClassNames; // @public export type TreeItemLayoutProps = ComponentProps>; @@ -115,15 +115,10 @@ export type TreeItemLayoutSlots = { content: NonNullable>; iconBefore?: Slot<'div'>; iconAfter?: Slot<'div'>; - expandIcon?: Slot<'div'>; - aside?: Slot<'div'>; - actions?: Slot & { - visible?: boolean; - }>>; }; // @public -export type TreeItemLayoutState = ComponentState & { +export type TreeItemLayoutState = ComponentState & { buttonContextValue: ButtonContextValue; }; @@ -134,13 +129,13 @@ export const treeItemLevelToken: "--fluent-TreeItem--level"; export const TreeItemPersonaLayout: ForwardRefComponent; // @public (undocumented) -export const treeItemPersonaLayoutClassNames: SlotClassNames; +export const treeItemPersonaLayoutClassNames: SlotClassNames; // @public export type TreeItemPersonaLayoutProps = ComponentProps>; // @public (undocumented) -export type TreeItemPersonaLayoutSlots = Pick & { +export type TreeItemPersonaLayoutSlots = { root: NonNullable>; media: NonNullable>; content: NonNullable>; @@ -148,7 +143,7 @@ export type TreeItemPersonaLayoutSlots = Pick & { +export type TreeItemPersonaLayoutState = ComponentState & { avatarSize: AvatarSize; buttonContextValue: ButtonContextValue; }; @@ -167,10 +162,15 @@ export type TreeItemSlots = { root: Slot & { style?: TreeItemCSSProperties; }>>; + expandIcon?: Slot<'div'>; + aside?: Slot<'div'>; + actions?: Slot & { + visible?: boolean; + }>>; }; // @public -export type TreeItemState = ComponentState & TreeItemContextValue & { +export type TreeItemState = ComponentState & TreeItemContextValue & TreeItemSlotsContextValue & { level: number; itemType: TreeItemType; }; @@ -279,7 +279,7 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React_2.Ref(selector: ContextSelector) => T; // @public (undocumented) -export function useTreeItemContextValues_unstable(state: Pick): TreeItemContextValues; +export function useTreeItemContextValues_unstable(state: TreeItemState): TreeItemContextValues; // @public export const useTreeItemLayout_unstable: (props: TreeItemLayoutProps, ref: React_2.Ref) => TreeItemLayoutState; diff --git a/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx b/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx index 9d7a6b328d31d1..4a5ec52ddc7fb1 100644 --- a/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx +++ b/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx @@ -124,8 +124,13 @@ for (const TreeTest of [NestedTree, FlatTree]) { it('should not expand/collapse item on actions click', () => { mount( - - action!}>level 1, item 1 + action!} + itemType="branch" + value="item1" + data-testid="item1" + > + level 1, item 1 level 2, item 1 @@ -167,8 +172,8 @@ for (const TreeTest of [NestedTree, FlatTree]) { it('should focus on actions when pressing tab key', () => { mount( - - action}>level 1, item 1 + action} itemType="branch" value="item1" data-testid="item1"> + level 1, item 1 level 2, item 1 @@ -186,8 +191,8 @@ for (const TreeTest of [NestedTree, FlatTree]) { it('should not expand/collapse item on actions Enter/Space key', () => { mount( - - action}>level 1, item 1 + action} itemType="branch" value="item1" data-testid="item1"> + level 1, item 1 level 2, item 1 diff --git a/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts b/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts index 4bad27fdf576c5..d3ddf018fffc11 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts +++ b/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts @@ -2,6 +2,7 @@ import type { ComponentProps, ComponentState, ExtractSlotProps, Slot } from '@fl import type { TreeItemContextValue } from '../../contexts'; import { treeItemLevelToken } from '../../utils/tokens'; import * as React from 'react'; +import { TreeItemSlotsContextValue } from '../../contexts/treeItemSlotsContext'; export type TreeItemCSSProperties = React.CSSProperties & { [treeItemLevelToken]?: string | number }; @@ -9,11 +10,36 @@ export type TreeItemType = 'leaf' | 'branch'; export type TreeItemSlots = { root: Slot & { style?: TreeItemCSSProperties }>>; + /** + * Expand icon slot, + * by default renders a chevron icon to indicate opening and closing + */ + expandIcon?: Slot<'div'>; + aside?: Slot<'div'>; + /** + * actionable elements are normally buttons, menus, or other focusable elements. + * Those elements are only visibly available if the given tree item is currently active. + */ + actions?: Slot< + ExtractSlotProps< + Slot<'div'> & { + /** + * Forces visibility of the aside/action content + */ + visible?: boolean; + } + > + >; }; +export type TreeItemInternalSlot = Pick; + export type TreeItemValue = string | number; -export type TreeItemContextValues = { treeItem: TreeItemContextValue }; +export type TreeItemContextValues = { + treeItem: TreeItemContextValue; + treeItemSlots: TreeItemSlotsContextValue; +}; /** * TreeItem Props @@ -26,8 +52,9 @@ export type TreeItemProps = ComponentProps> & { /** * State used in rendering TreeItem */ -export type TreeItemState = ComponentState & - TreeItemContextValue & { +export type TreeItemState = ComponentState & + TreeItemContextValue & + TreeItemSlotsContextValue & { level: number; itemType: TreeItemType; }; diff --git a/packages/react-components/react-tree/src/components/TreeItem/renderTreeItem.tsx b/packages/react-components/react-tree/src/components/TreeItem/renderTreeItem.tsx index 228025bc70eb52..9d5a8ead2d8146 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/renderTreeItem.tsx +++ b/packages/react-components/react-tree/src/components/TreeItem/renderTreeItem.tsx @@ -3,18 +3,20 @@ import { createElement } from '@fluentui/react-jsx-runtime'; import { getSlotsNext } from '@fluentui/react-utilities'; -import type { TreeItemState, TreeItemSlots, TreeItemContextValues } from './TreeItem.types'; -import { TreeItemProvider } from '../../contexts'; +import type { TreeItemState, TreeItemContextValues, TreeItemInternalSlot } from './TreeItem.types'; +import { TreeItemProvider, TreeItemSlotsProvider } from '../../contexts'; /** * Render the final JSX of TreeItem */ export const renderTreeItem_unstable = (state: TreeItemState, contextValues: TreeItemContextValues) => { - const { slots, slotProps } = getSlotsNext(state); + const { slots, slotProps } = getSlotsNext(state); return ( - {slotProps.root.children} + + {slotProps.root.children} + ); }; diff --git a/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.ts b/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx similarity index 71% rename from packages/react-components/react-tree/src/components/TreeItem/useTreeItem.ts rename to packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx index c8e8595166d494..7444dc1d6afe0a 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.ts +++ b/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx @@ -1,11 +1,19 @@ import * as React from 'react'; -import { getNativeElementProps, useId, useMergedRefs } from '@fluentui/react-utilities'; +import { + getNativeElementProps, + isResolvedShorthand, + resolveShorthand, + useControllableState, + useId, + useMergedRefs, +} from '@fluentui/react-utilities'; import { useEventCallback } from '@fluentui/react-utilities'; import { elementContains } from '@fluentui/react-portal'; -import type { TreeItemProps, TreeItemState } from './TreeItem.types'; +import type { TreeItemProps, TreeItemSlots, TreeItemState } from './TreeItem.types'; import { useTreeContext_unstable } from '../../contexts/index'; import { treeDataTypes } from '../../utils/tokens'; import { dataTreeItemValueAttrName } from '../../utils/getTreeItemValueFromElement'; +import { TreeItemChevron } from '../TreeItemChevron'; /** * Create the state required to render TreeItem. @@ -21,15 +29,35 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref ctx.requestTreeResponse); - const [isActionsVisible, setActionsVisible] = React.useState(false); + const [isActionsVisibleExternal, actions]: [boolean | undefined, TreeItemSlots['actions']] = isResolvedShorthand( + props.actions, + ) + ? // .visible prop should not be propagated to the DOM + [props.actions.visible, { ...props.actions, visible: undefined }] + : [undefined, props.actions]; + + const [isActionsVisible, setActionsVisible] = useControllableState({ + state: isActionsVisibleExternal, + defaultState: false, + initialState: false, + }); const [isAsideVisible, setAsideVisible] = React.useState(true); - const handleActionsRef = (actions: HTMLDivElement | null) => { - setAsideVisible(actions === null); + const handleActionsRef = (actionsElement: HTMLDivElement | null) => { + setAsideVisible(actionsElement === null); }; const open = useTreeContext_unstable(ctx => ctx.openItems.has(value)); @@ -39,6 +67,12 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref(null); const subtreeRef = React.useRef(null); + const actionsRefs = useMergedRefs( + isResolvedShorthand(actions) ? actions.ref : undefined, + handleActionsRef, + actionsRef, + ); + const handleClick = useEventCallback((event: React.MouseEvent) => { onClick?.(event); if (event.isDefaultPrevented()) { @@ -110,16 +144,14 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref, + 'aria-hidden': true, + ref: useMergedRefs(isResolvedShorthand(expandIcon) ? expandIcon.ref : undefined, expandIconRef), + }, + }), }; } diff --git a/packages/react-components/react-tree/src/components/TreeItem/useTreeItemContextValues.ts b/packages/react-components/react-tree/src/components/TreeItem/useTreeItemContextValues.ts index 13c84cd697f35e..c8d7522abf5c15 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/useTreeItemContextValues.ts +++ b/packages/react-components/react-tree/src/components/TreeItem/useTreeItemContextValues.ts @@ -1,27 +1,26 @@ -import type { TreeItemContextValue } from '../../contexts'; +import * as React from 'react'; import type { TreeItemContextValues, TreeItemState } from './TreeItem.types'; +import type { TreeItemContextValue, TreeItemSlotsContextValue } from '../../contexts'; -export function useTreeItemContextValues_unstable( - state: Pick, -): TreeItemContextValues { - const { value, isActionsVisible, isAsideVisible, actionsRef, itemType, layoutRef, subtreeRef, expandIconRef, open } = - state; +export function useTreeItemContextValues_unstable(state: TreeItemState): TreeItemContextValues { + const { value, itemType, layoutRef, subtreeRef, open, actions, aside, expandIcon } = state; /** * This context is created with "@fluentui/react-context-selector", * there is no sense to memoize it */ const treeItem: TreeItemContextValue = { - isActionsVisible, - isAsideVisible, value, - actionsRef, itemType, layoutRef, subtreeRef, - expandIconRef, open, }; - return { treeItem }; + const treeItemSlots: TreeItemSlotsContextValue = React.useMemo( + () => ({ actions, aside, expandIcon }), + [actions, aside, expandIcon], + ); + + return { treeItem, treeItemSlots }; } diff --git a/packages/react-components/react-tree/src/components/TreeItem/useTreeItemStyles.styles.ts b/packages/react-components/react-tree/src/components/TreeItem/useTreeItemStyles.styles.ts index 42c8272f7cc398..69aef7676ad659 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/useTreeItemStyles.styles.ts +++ b/packages/react-components/react-tree/src/components/TreeItem/useTreeItemStyles.styles.ts @@ -1,11 +1,11 @@ import { GriffelStyle, makeStyles, mergeClasses } from '@griffel/react'; -import type { TreeItemCSSProperties, TreeItemSlots, TreeItemState } from './TreeItem.types'; +import type { TreeItemCSSProperties, TreeItemInternalSlot, TreeItemState } from './TreeItem.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens } from '@fluentui/react-theme'; import { createFocusOutlineStyle } from '@fluentui/react-tabster'; import { treeItemLevelToken } from '../../utils/tokens'; -export const treeItemClassNames: SlotClassNames = { +export const treeItemClassNames: SlotClassNames = { root: 'fui-TreeItem', }; diff --git a/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.test.tsx b/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.test.tsx index a0247b9b790ac7..4bb9ab530ca6a5 100644 --- a/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.test.tsx +++ b/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.test.tsx @@ -2,17 +2,22 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { TreeItemLayout } from './TreeItemLayout'; import { isConformant } from '../../testing/isConformant'; +import { TreeItemSlotsProvider } from '../../contexts/treeItemSlotsContext'; + +const Wrap: React.FC = props => ( + + {props.children} + +); describe('TreeItemLayout', () => { isConformant({ Component: TreeItemLayout, + renderOptions: { wrapper: Wrap }, displayName: 'TreeItemLayout', requiredProps: { - expandIcon: 'expandIcon', iconAfter: 'iconAfter', iconBefore: 'iconBefore', - aside: 'aside', - actions: { visible: true, children: 'actions' }, }, }); diff --git a/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.types.ts b/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.types.ts index 92c581aff09479..60f2354e3a3cad 100644 --- a/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.types.ts +++ b/packages/react-components/react-tree/src/components/TreeItemLayout/TreeItemLayout.types.ts @@ -1,5 +1,6 @@ -import type { ComponentProps, ComponentState, ExtractSlotProps, Slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { ButtonContextValue } from '@fluentui/react-button'; +import { TreeItemSlots } from '../TreeItem/TreeItem.types'; export type TreeItemLayoutSlots = { root: Slot<'div'>; @@ -15,28 +16,10 @@ export type TreeItemLayoutSlots = { * Icon slot that renders right after main content */ iconAfter?: Slot<'div'>; - /** - * Expand icon slot, - * by default renders a chevron icon to indicate opening and closing - */ - expandIcon?: Slot<'div'>; - aside?: Slot<'div'>; - /** - * actionable elements are normally buttons, menus, or other focusable elements. - * Those elements are only visibly available if the given tree item is currently active. - */ - actions?: Slot< - ExtractSlotProps< - Slot<'div'> & { - /** - * Forces visibility of the aside/action content - */ - visible?: boolean; - } - > - >; }; +export type TreeItemLayoutInternalSlots = TreeItemLayoutSlots & Pick; + /** * TreeItemLayout Props */ @@ -45,6 +28,6 @@ export type TreeItemLayoutProps = ComponentProps>; /** * State used in rendering TreeItemLayout */ -export type TreeItemLayoutState = ComponentState & { +export type TreeItemLayoutState = ComponentState & { buttonContextValue: ButtonContextValue; }; diff --git a/packages/react-components/react-tree/src/components/TreeItemLayout/renderTreeItemLayout.tsx b/packages/react-components/react-tree/src/components/TreeItemLayout/renderTreeItemLayout.tsx index 1a04c3f05aca07..9a1330fa15f518 100644 --- a/packages/react-components/react-tree/src/components/TreeItemLayout/renderTreeItemLayout.tsx +++ b/packages/react-components/react-tree/src/components/TreeItemLayout/renderTreeItemLayout.tsx @@ -3,14 +3,14 @@ import { createElement } from '@fluentui/react-jsx-runtime'; import { getSlotsNext } from '@fluentui/react-utilities'; -import type { TreeItemLayoutState, TreeItemLayoutSlots } from './TreeItemLayout.types'; +import type { TreeItemLayoutState, TreeItemLayoutInternalSlots } from './TreeItemLayout.types'; import { ButtonContextProvider } from '@fluentui/react-button'; /** * Render the final JSX of TreeItemLayout */ export const renderTreeItemLayout_unstable = (state: TreeItemLayoutState) => { - const { slots, slotProps } = getSlotsNext(state); + const { slots, slotProps } = getSlotsNext(state); return ( diff --git a/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.ts b/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.ts new file mode 100644 index 00000000000000..05b6c1fb125569 --- /dev/null +++ b/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.ts @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { getNativeElementProps, resolveShorthand, useMergedRefs } from '@fluentui/react-utilities'; +import type { TreeItemLayoutProps, TreeItemLayoutState } from './TreeItemLayout.types'; +import { useTreeItemContext_unstable } from '../../contexts/treeItemContext'; +import { useTreeItemSlotsContext_unstable } from '../../contexts/treeItemSlotsContext'; + +/** + * Create the state required to render TreeItemLayout. + * + * The returned state can be modified with hooks such as useTreeItemLayoutStyles_unstable, + * before being passed to renderTreeItemLayout_unstable. + * + * @param props - props from this instance of TreeItemLayout + * @param ref - reference to root HTMLElement of TreeItemLayout + */ +export const useTreeItemLayout_unstable = ( + props: TreeItemLayoutProps, + ref: React.Ref, +): TreeItemLayoutState => { + const { content, iconAfter, iconBefore, as = 'span' } = props; + + const { actions, aside, expandIcon } = useTreeItemSlotsContext_unstable(); + + const layoutRef = useTreeItemContext_unstable(ctx => ctx.layoutRef); + + return { + components: { + root: 'div', + expandIcon: 'div', + iconBefore: 'div', + content: 'div', + iconAfter: 'div', + actions: 'div', + aside: 'div', + }, + buttonContextValue: { size: 'small' }, + root: getNativeElementProps(as, { ...props, ref: useMergedRefs(ref, layoutRef) }), + iconBefore: resolveShorthand(iconBefore, { defaultProps: { 'aria-hidden': true } }), + content: resolveShorthand(content, { required: true }), + iconAfter: resolveShorthand(iconAfter, { defaultProps: { 'aria-hidden': true } }), + aside: resolveShorthand(aside), + actions: resolveShorthand(actions), + expandIcon: resolveShorthand(expandIcon), + }; +}; diff --git a/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.tsx b/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.tsx deleted file mode 100644 index 2b675617ba9340..00000000000000 --- a/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayout.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react'; -import { - ExtractSlotProps, - getNativeElementProps, - isResolvedShorthand, - resolveShorthand, - useMergedRefs, -} from '@fluentui/react-utilities'; -import type { TreeItemLayoutProps, TreeItemLayoutSlots, TreeItemLayoutState } from './TreeItemLayout.types'; -import { useTreeItemContext_unstable } from '../../contexts/treeItemContext'; -import { TreeItemChevron } from '../TreeItemChevron'; - -/** - * Create the state required to render TreeItemLayout. - * - * The returned state can be modified with hooks such as useTreeItemLayoutStyles_unstable, - * before being passed to renderTreeItemLayout_unstable. - * - * @param props - props from this instance of TreeItemLayout - * @param ref - reference to root HTMLElement of TreeItemLayout - */ -export const useTreeItemLayout_unstable = ( - props: TreeItemLayoutProps, - ref: React.Ref, -): TreeItemLayoutState => { - const { content, iconAfter, iconBefore, expandIcon, as = 'span', aside, actions } = props; - - const layoutRef = useTreeItemContext_unstable(ctx => ctx.layoutRef); - const expandIconRef = useTreeItemContext_unstable(ctx => ctx.expandIconRef); - const isActionsVisibleContext = useTreeItemContext_unstable(ctx => ctx.isActionsVisible); - const isAsideVisible = useTreeItemContext_unstable(ctx => ctx.isAsideVisible); - const isActionsVisible = (isResolvedShorthand(actions) ? actions.visible : undefined) ?? isActionsVisibleContext; - const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch'); - const actionsRef = useMergedRefs( - isResolvedShorthand(actions) ? actions.ref : undefined, - useTreeItemContext_unstable(ctx => ctx.actionsRef), - ); - - return { - components: { - root: 'div', - expandIcon: 'div', - iconBefore: 'div', - content: 'div', - iconAfter: 'div', - actions: 'div', - aside: 'div', - }, - buttonContextValue: { size: 'small' }, - root: getNativeElementProps(as, { ...props, ref: useMergedRefs(ref, layoutRef) }), - iconBefore: resolveShorthand(iconBefore, { defaultProps: { 'aria-hidden': true } }), - content: resolveShorthand(content, { required: true }), - iconAfter: resolveShorthand(iconAfter, { defaultProps: { 'aria-hidden': true } }), - aside: isAsideVisible ? resolveShorthand(aside) : undefined, - actions: isActionsVisible - ? resolveShorthand>( - // visible props should not be propagated to the DOM - isResolvedShorthand(actions) ? { ...actions, visible: undefined } : actions, - { - defaultProps: { ref: actionsRef }, - }, - ) - : undefined, - expandIcon: resolveShorthand(expandIcon, { - required: isBranch, - defaultProps: { - children: , - 'aria-hidden': true, - ref: useMergedRefs(isResolvedShorthand(expandIcon) ? expandIcon.ref : undefined, expandIconRef), - }, - }), - }; -}; diff --git a/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayoutStyles.styles.ts b/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayoutStyles.styles.ts index a814aebca5720c..db5c82d0319c5f 100644 --- a/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayoutStyles.styles.ts +++ b/packages/react-components/react-tree/src/components/TreeItemLayout/useTreeItemLayoutStyles.styles.ts @@ -1,12 +1,12 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; -import type { TreeItemLayoutSlots, TreeItemLayoutState } from './TreeItemLayout.types'; +import type { TreeItemLayoutInternalSlots, TreeItemLayoutState } from './TreeItemLayout.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens, typographyStyles } from '@fluentui/react-theme'; import { useTreeContext_unstable } from '../../contexts/treeContext'; import { treeItemLevelToken } from '../../utils/tokens'; import { useTreeItemContext_unstable } from '../../contexts/treeItemContext'; -export const treeItemLayoutClassNames: SlotClassNames = { +export const treeItemLayoutClassNames: SlotClassNames = { root: 'fui-TreeItemLayout', iconBefore: 'fui-TreeItemLayout__iconBefore', content: 'fui-TreeItemLayout__content', diff --git a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx index 835f7553237e3d..05070f07113478 100644 --- a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx +++ b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.test.tsx @@ -2,16 +2,21 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { TreeItemPersonaLayout } from './TreeItemPersonaLayout'; import { isConformant } from '../../testing/isConformant'; +import { TreeItemSlotsProvider } from '../../contexts/treeItemSlotsContext'; + +const Wrap: React.FC = props => ( + + {props.children} + +); describe('TreeItemPersonaLayout', () => { isConformant({ Component: TreeItemPersonaLayout, + renderOptions: { wrapper: Wrap }, displayName: 'TreeItemPersonaLayout', requiredProps: { description: 'description', - expandIcon: 'expandIcon', - aside: 'aside', - actions: { visible: true, children: 'actions' }, }, }); diff --git a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.types.ts b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.types.ts index 7ceae9c99fc29e..86e841c9368a7a 100644 --- a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.types.ts +++ b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/TreeItemPersonaLayout.types.ts @@ -1,13 +1,13 @@ import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import type { AvatarContextValue, AvatarSize } from '@fluentui/react-avatar'; import { ButtonContextValue } from '@fluentui/react-button'; -import { TreeItemLayoutSlots } from '../TreeItemLayout/TreeItemLayout.types'; +import { TreeItemSlots } from '../TreeItem/TreeItem.types'; export type TreeItemPersonaLayoutContextValues = { avatar: AvatarContextValue; }; -export type TreeItemPersonaLayoutSlots = Pick & { +export type TreeItemPersonaLayoutSlots = { root: NonNullable>; /** * Avatar to display. @@ -23,6 +23,9 @@ export type TreeItemPersonaLayoutSlots = Pick; }; +export type TreeItemPersonaLayoutInternalSlots = TreeItemPersonaLayoutSlots & + Pick; + /** * TreeItemPersonaLayout Props */ @@ -31,7 +34,7 @@ export type TreeItemPersonaLayoutProps = ComponentProps & { +export type TreeItemPersonaLayoutState = ComponentState & { avatarSize: AvatarSize; buttonContextValue: ButtonContextValue; }; diff --git a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/renderTreeItemPersonaLayout.tsx b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/renderTreeItemPersonaLayout.tsx index 86ad6770d0c892..d46e8f9d0cd5f6 100644 --- a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/renderTreeItemPersonaLayout.tsx +++ b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/renderTreeItemPersonaLayout.tsx @@ -6,7 +6,7 @@ import { getSlotsNext } from '@fluentui/react-utilities'; import type { TreeItemPersonaLayoutState, TreeItemPersonaLayoutContextValues, - TreeItemPersonaLayoutSlots, + TreeItemPersonaLayoutInternalSlots, } from './TreeItemPersonaLayout.types'; import { AvatarContextProvider } from '@fluentui/react-avatar'; import { ButtonContextProvider } from '@fluentui/react-button'; @@ -18,7 +18,7 @@ export const renderTreeItemPersonaLayout_unstable = ( state: TreeItemPersonaLayoutState, contextValues: TreeItemPersonaLayoutContextValues, ) => { - const { slots, slotProps } = getSlotsNext(state); + const { slots, slotProps } = getSlotsNext(state); return ( diff --git a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.tsx b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts similarity index 100% rename from packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.tsx rename to packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayout.ts diff --git a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayoutStyles.styles.ts b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayoutStyles.styles.ts index c79669aab55a75..236257fdb8d8e3 100644 --- a/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayoutStyles.styles.ts +++ b/packages/react-components/react-tree/src/components/TreeItemPersonaLayout/useTreeItemPersonaLayoutStyles.styles.ts @@ -1,11 +1,11 @@ -import type { TreeItemPersonaLayoutSlots, TreeItemPersonaLayoutState } from './TreeItemPersonaLayout.types'; +import type { TreeItemPersonaLayoutInternalSlots, TreeItemPersonaLayoutState } from './TreeItemPersonaLayout.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import { tokens, typographyStyles } from '@fluentui/react-theme'; import { treeItemLevelToken } from '../../utils/tokens'; import { useTreeItemContext_unstable } from '../../contexts/treeItemContext'; -export const treeItemPersonaLayoutClassNames: SlotClassNames = { +export const treeItemPersonaLayoutClassNames: SlotClassNames = { root: 'fui-TreeItemPersonaLayout', media: 'fui-TreeItemPersonaLayout__media', description: 'fui-TreeItemPersonaLayout__description', diff --git a/packages/react-components/react-tree/src/contexts/index.ts b/packages/react-components/react-tree/src/contexts/index.ts index 7f886da3787ce7..77a28ce91df42b 100644 --- a/packages/react-components/react-tree/src/contexts/index.ts +++ b/packages/react-components/react-tree/src/contexts/index.ts @@ -1,2 +1,3 @@ export * from './treeContext'; export * from './treeItemContext'; +export * from './treeItemSlotsContext'; diff --git a/packages/react-components/react-tree/src/contexts/treeItemContext.ts b/packages/react-components/react-tree/src/contexts/treeItemContext.ts index 6cff7d49c82362..f7a1219a4fd58a 100644 --- a/packages/react-components/react-tree/src/contexts/treeItemContext.ts +++ b/packages/react-components/react-tree/src/contexts/treeItemContext.ts @@ -3,10 +3,6 @@ import { Context, ContextSelector, createContext, useContextSelector } from '@fl import { TreeItemType } from '../TreeItem'; export type TreeItemContextValue = { - isActionsVisible: boolean; - isAsideVisible: boolean; - actionsRef: React.Ref; - expandIconRef: React.Ref; layoutRef: React.Ref; subtreeRef: React.Ref; itemType: TreeItemType; @@ -16,10 +12,6 @@ export type TreeItemContextValue = { const defaultContextValue: TreeItemContextValue = { value: '', - isActionsVisible: false, - isAsideVisible: true, - actionsRef: React.createRef(), - expandIconRef: React.createRef(), layoutRef: React.createRef(), subtreeRef: React.createRef(), itemType: 'leaf', diff --git a/packages/react-components/react-tree/src/contexts/treeItemSlotsContext.ts b/packages/react-components/react-tree/src/contexts/treeItemSlotsContext.ts new file mode 100644 index 00000000000000..86cb4a21d8a488 --- /dev/null +++ b/packages/react-components/react-tree/src/contexts/treeItemSlotsContext.ts @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { TreeItemSlots } from '../TreeItem'; + +export type TreeItemSlotsContextValue = Pick; + +const defaultContextValue: TreeItemSlotsContextValue = { + actions: undefined, + aside: undefined, + expandIcon: undefined, +}; + +export const TreeItemSlotsContext: React.Context = React.createContext< + TreeItemSlotsContextValue | undefined +>(undefined); + +export const { Provider: TreeItemSlotsProvider } = TreeItemSlotsContext; +export const useTreeItemSlotsContext_unstable = (): TreeItemSlotsContextValue => + React.useContext(TreeItemSlotsContext) || defaultContextValue; diff --git a/packages/react-components/react-tree/stories/B_TreeItem/TreeItemActions.stories.tsx b/packages/react-components/react-tree/stories/B_TreeItem/TreeItemActions.stories.tsx index b1125fdfe7ae76..9330443fd66bac 100644 --- a/packages/react-components/react-tree/stories/B_TreeItem/TreeItemActions.stories.tsx +++ b/packages/react-components/react-tree/stories/B_TreeItem/TreeItemActions.stories.tsx @@ -28,43 +28,43 @@ const ActionsExample = () => { export const Actions = () => ( - - }>level 1, item 1 + } itemType="branch"> + level 1, item 1 - - }>level 2, item 1 + } itemType="leaf"> + level 2, item 1 - - }>level 2, item 2 + } itemType="leaf"> + level 2, item 2 - - }>level 2, item 3 + } itemType="leaf"> + level 2, item 3 - - }>level 1, item 2 + } itemType="branch"> + level 1, item 2 - - }>level 2, item 1 + } itemType="branch"> + level 2, item 1 - - }>level 3, item 1 + } itemType="leaf"> + level 3, item 1 - - }>level 1, item 1 + } itemType="branch"> + level 1, item 1 - - }>level 2, item 1 + } itemType="leaf"> + level 2, item 1 - - }>level 2, item 2 + } itemType="leaf"> + level 2, item 2 - - }>level 2, item 3 + } itemType="leaf"> + level 2, item 3 diff --git a/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx b/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx index 36f35f5f9b6f4b..39ec8db1c1ef52 100644 --- a/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx +++ b/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx @@ -11,10 +11,12 @@ export const ExpandIcon = () => { }; return ( - - : }> - level 1, item 1 - + : } + itemType="branch" + value="tree-item-1" + > + level 1, item 1 level 2, item 1 @@ -27,15 +29,19 @@ export const ExpandIcon = () => { - - : }> - level 1, item 2 - + : } + itemType="branch" + value="tree-item-2" + > + level 1, item 2 - - : }> - level 2, item 1 - + : } + itemType="branch" + value="tree-item-3" + > + level 2, item 1 level 3, item 1 diff --git a/packages/react-components/react-tree/stories/C_Layouts/LayoutsDefault.stories.tsx b/packages/react-components/react-tree/stories/C_Layouts/LayoutsDefault.stories.tsx index 22e7446da7b058..330e063d514b42 100644 --- a/packages/react-components/react-tree/stories/C_Layouts/LayoutsDefault.stories.tsx +++ b/packages/react-components/react-tree/stories/C_Layouts/LayoutsDefault.stories.tsx @@ -24,36 +24,38 @@ import { export const Default = () => { return ( - - + + + + } + actions={{ + visible: true, + children: ( <> - - - - } - actions={{ - visible: true, - children: ( - <> -