Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
26 changes: 13 additions & 13 deletions packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ export type TreeContextValue = {
export const TreeItem: ForwardRefComponent<TreeItemProps>;

// @public (undocumented)
export const treeItemClassNames: SlotClassNames<TreeItemSlots>;
export const treeItemClassNames: SlotClassNames<TreeItemInternalSlot>;

// @public
export const TreeItemLayout: ForwardRefComponent<TreeItemLayoutProps>;

// @public (undocumented)
export const treeItemLayoutClassNames: SlotClassNames<TreeItemLayoutSlots>;
export const treeItemLayoutClassNames: SlotClassNames<TreeItemLayoutInternalSlots>;

// @public
export type TreeItemLayoutProps = ComponentProps<Partial<TreeItemLayoutSlots>>;
Expand All @@ -115,15 +115,10 @@ export type TreeItemLayoutSlots = {
content: NonNullable<Slot<'div'>>;
iconBefore?: Slot<'div'>;
iconAfter?: Slot<'div'>;
expandIcon?: Slot<'div'>;
aside?: Slot<'div'>;
actions?: Slot<ExtractSlotProps<Slot<'div'> & {
visible?: boolean;
}>>;
};

// @public
export type TreeItemLayoutState = ComponentState<TreeItemLayoutSlots> & {
export type TreeItemLayoutState = ComponentState<TreeItemLayoutInternalSlots> & {
buttonContextValue: ButtonContextValue;
};

Expand All @@ -134,21 +129,21 @@ export const treeItemLevelToken: "--fluent-TreeItem--level";
export const TreeItemPersonaLayout: ForwardRefComponent<TreeItemPersonaLayoutProps>;

// @public (undocumented)
export const treeItemPersonaLayoutClassNames: SlotClassNames<TreeItemPersonaLayoutSlots>;
export const treeItemPersonaLayoutClassNames: SlotClassNames<TreeItemPersonaLayoutInternalSlots>;

// @public
export type TreeItemPersonaLayoutProps = ComponentProps<Partial<TreeItemPersonaLayoutSlots>>;

// @public (undocumented)
export type TreeItemPersonaLayoutSlots = Pick<TreeItemLayoutSlots, 'actions' | 'aside' | 'expandIcon'> & {
export type TreeItemPersonaLayoutSlots = {
root: NonNullable<Slot<'div'>>;
media: NonNullable<Slot<'div'>>;
content: NonNullable<Slot<'div'>>;
description?: Slot<'div'>;
};

// @public
export type TreeItemPersonaLayoutState = ComponentState<TreeItemPersonaLayoutSlots> & {
export type TreeItemPersonaLayoutState = ComponentState<TreeItemPersonaLayoutInternalSlots> & {
avatarSize: AvatarSize;
buttonContextValue: ButtonContextValue;
};
Expand All @@ -167,10 +162,15 @@ export type TreeItemSlots = {
root: Slot<ExtractSlotProps<Slot<'div'> & {
style?: TreeItemCSSProperties;
}>>;
expandIcon?: Slot<'div'>;
aside?: Slot<'div'>;
actions?: Slot<ExtractSlotProps<Slot<'div'> & {
visible?: boolean;
}>>;
};

// @public
export type TreeItemState = ComponentState<TreeItemSlots> & TreeItemContextValue & {
export type TreeItemState = ComponentState<TreeItemInternalSlot> & TreeItemContextValue & TreeItemSlotsContextValue & {
level: number;
itemType: TreeItemType;
};
Expand Down Expand Up @@ -279,7 +279,7 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React_2.Ref<HTML
export const useTreeItemContext_unstable: <T>(selector: ContextSelector<TreeItemContextValue, T>) => T;

// @public (undocumented)
export function useTreeItemContextValues_unstable(state: Pick<TreeItemState, keyof TreeItemContextValue>): TreeItemContextValues;
export function useTreeItemContextValues_unstable(state: TreeItemState): TreeItemContextValues;

// @public
export const useTreeItemLayout_unstable: (props: TreeItemLayoutProps, ref: React_2.Ref<HTMLElement>) => TreeItemLayoutState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@ for (const TreeTest of [NestedTree, FlatTree]) {
it('should not expand/collapse item on actions click', () => {
mount(
<TreeTest id="tree" aria-label="Tree">
<TreeItem itemType="branch" value="item1" data-testid="item1">
<TreeItemLayout actions={<Button id="action">action!</Button>}>level 1, item 1</TreeItemLayout>
<TreeItem
actions={<Button id="action">action!</Button>}
itemType="branch"
value="item1"
data-testid="item1"
>
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf" value="item1__item1" data-testid="item1__item1">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
Expand Down Expand Up @@ -167,8 +172,8 @@ for (const TreeTest of [NestedTree, FlatTree]) {
it('should focus on actions when pressing tab key', () => {
mount(
<TreeTest id="tree" aria-label="Tree">
<TreeItem itemType="branch" value="item1" data-testid="item1">
<TreeItemLayout actions={<Button id="action">action</Button>}>level 1, item 1</TreeItemLayout>
<TreeItem actions={<Button id="action">action</Button>} itemType="branch" value="item1" data-testid="item1">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf" value="item1__item1" data-testid="item1__item1">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
Expand All @@ -186,8 +191,8 @@ for (const TreeTest of [NestedTree, FlatTree]) {
it('should not expand/collapse item on actions Enter/Space key', () => {
mount(
<TreeTest id="tree" aria-label="Tree">
<TreeItem itemType="branch" value="item1" data-testid="item1">
<TreeItemLayout actions={<Button id="action">action</Button>}>level 1, item 1</TreeItemLayout>
<TreeItem actions={<Button id="action">action</Button>} itemType="branch" value="item1" data-testid="item1">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf" value="item1__item1" data-testid="item1__item1">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,44 @@ 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 };

export type TreeItemType = 'leaf' | 'branch';

export type TreeItemSlots = {
root: Slot<ExtractSlotProps<Slot<'div'> & { 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<TreeItemSlots, 'root'>;

export type TreeItemValue = string | number;

export type TreeItemContextValues = { treeItem: TreeItemContextValue };
export type TreeItemContextValues = {
treeItem: TreeItemContextValue;
treeItemSlots: TreeItemSlotsContextValue;
};

/**
* TreeItem Props
Expand All @@ -26,8 +52,9 @@ export type TreeItemProps = ComponentProps<Partial<TreeItemSlots>> & {
/**
* State used in rendering TreeItem
*/
export type TreeItemState = ComponentState<TreeItemSlots> &
TreeItemContextValue & {
export type TreeItemState = ComponentState<TreeItemInternalSlot> &
TreeItemContextValue &
TreeItemSlotsContextValue & {
level: number;
itemType: TreeItemType;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<TreeItemSlots>(state);
const { slots, slotProps } = getSlotsNext<TreeItemInternalSlot>(state);

return (
<slots.root {...slotProps.root}>
<TreeItemProvider value={contextValues.treeItem}>{slotProps.root.children}</TreeItemProvider>
<TreeItemProvider value={contextValues.treeItem}>
<TreeItemSlotsProvider value={contextValues.treeItemSlots}>{slotProps.root.children}</TreeItemSlotsProvider>
</TreeItemProvider>
</slots.root>
);
};
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -21,15 +29,35 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi

const value = useId('fuiTreeItemValue-', props.value?.toString());

const { onClick, onKeyDown, as = 'div', itemType = 'leaf', 'aria-level': level = contextLevel, ...rest } = props;
const {
onClick,
onKeyDown,
as = 'div',
itemType = 'leaf',
'aria-level': level = contextLevel,
expandIcon,
aside,
...rest
} = props;

const requestTreeResponse = useTreeContext_unstable(ctx => 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));
Expand All @@ -39,6 +67,12 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
const layoutRef = React.useRef<HTMLDivElement>(null);
const subtreeRef = React.useRef<HTMLDivElement>(null);

const actionsRefs = useMergedRefs(
isResolvedShorthand(actions) ? actions.ref : undefined,
handleActionsRef,
actionsRef,
);

const handleClick = useEventCallback((event: React.MouseEvent<HTMLDivElement>) => {
onClick?.(event);
if (event.isDefaultPrevented()) {
Expand Down Expand Up @@ -110,16 +144,14 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
}
});

const isBranch = itemType === 'branch';

return {
value,
open,
subtreeRef,
actionsRef: useMergedRefs(actionsRef, handleActionsRef),
expandIconRef,
layoutRef,
itemType,
isActionsVisible,
isAsideVisible,
level,
components: {
root: 'div',
Expand All @@ -131,13 +163,23 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
role: 'treeitem',
'aria-level': level,
[dataTreeItemValueAttrName]: value,
'aria-expanded': itemType === 'branch' ? open : undefined,
'aria-expanded': isBranch ? open : undefined,
onClick: handleClick,
onKeyDown: handleKeyDown,
onMouseOver: handleActionsVisible,
onFocus: handleActionsVisible,
onMouseOut: handleActionsInvisible,
onBlur: handleActionsInvisible,
}),
actions: isActionsVisible ? resolveShorthand(actions, { defaultProps: { ref: actionsRefs } }) : undefined,
aside: isAsideVisible ? resolveShorthand(aside) : undefined,
expandIcon: resolveShorthand(expandIcon, {
required: isBranch,
defaultProps: {
children: <TreeItemChevron />,
'aria-hidden': true,
ref: useMergedRefs(isResolvedShorthand(expandIcon) ? expandIcon.ref : undefined, expandIconRef),
},
}),
};
}
Original file line number Diff line number Diff line change
@@ -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<TreeItemState, keyof TreeItemContextValue>,
): 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],
);
Comment thread
layershifter marked this conversation as resolved.

return { treeItem, treeItemSlots };
}
Original file line number Diff line number Diff line change
@@ -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<TreeItemSlots> = {
export const treeItemClassNames: SlotClassNames<TreeItemInternalSlot> = {
root: 'fui-TreeItem',
};

Expand Down
Loading