Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 7 additions & 6 deletions packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@

/// <reference types="react" />

import type { ARIAButtonElement } from '@fluentui/react-aria';
import type { ARIAButtonSlotProps } from '@fluentui/react-aria';
import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import { ContextSelector } from '@fluentui/react-context-selector';
Expand All @@ -30,7 +28,7 @@ export type BaseTreeItemProps = ComponentProps<BaseTreeItemSlots>;

// @public (undocumented)
export type BaseTreeItemSlots = {
root: Slot<ARIAButtonSlotProps>;
root: Slot<'div'>;
};

// @public
Expand Down Expand Up @@ -81,10 +79,13 @@ export type TreeItemSlots = BaseTreeItemSlots & {
iconAfter?: Slot<'span'>;
badges?: Slot<'span'>;
actions?: Slot<'span'>;
groupper: NonNullable<Slot<'span'>>;
};

// @public
export type TreeItemState = ComponentState<TreeItemSlots> & BaseTreeItemState;
export type TreeItemState = ComponentState<TreeItemSlots> & BaseTreeItemState & {
keepActionsOpen: boolean;
};

// @public (undocumented)
export type TreeProps = ComponentProps<TreeSlots> & {
Expand All @@ -109,7 +110,7 @@ export type TreeState = ComponentState<TreeSlots> & TreeContextValue & {
};

// @public
export const useBaseTreeItem_unstable: (props: BaseTreeItemProps, ref: React_2.Ref<BaseTreeItemElement>) => BaseTreeItemState;
export const useBaseTreeItem_unstable: (props: BaseTreeItemProps, ref: React_2.Ref<HTMLDivElement>) => BaseTreeItemState;

// @public
export const useBaseTreeItemStyles_unstable: (state: BaseTreeItemState) => BaseTreeItemState;
Expand All @@ -121,7 +122,7 @@ export const useTree_unstable: (props: TreeProps, ref: React_2.Ref<HTMLElement>)
export const useTreeContext_unstable: <T>(selector: ContextSelector<TreeContextValue, T>) => T;

// @public
export const useTreeItem_unstable: (props: TreeItemProps, ref: React_2.Ref<TreeItemElement>) => TreeItemState;
export const useTreeItem_unstable: (props: TreeItemProps, ref: React_2.Ref<HTMLDivElement>) => TreeItemState;

// @public
export const useTreeItemStyles_unstable: (state: TreeItemState) => TreeItemState;
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-tree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@fluentui/react-shared-contexts": "^9.1.4",
"@fluentui/react-aria": "^9.3.4",
"@fluentui/react-tabster": "^9.3.5",
"@fluentui/react-portal": "^9.0.15",
"@fluentui/keyboard-keys": "^9.0.1",
"@fluentui/react-theme": "^9.1.5",
"@fluentui/react-utilities": "^9.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { ARIAButtonElement, ARIAButtonElementIntersection, ARIAButtonSlotProps } from '@fluentui/react-aria';

export type BaseTreeItemElement = ARIAButtonElement;

/** @internal */
export type BaseTreeItemElementIntersection = ARIAButtonElementIntersection;

export type BaseTreeItemSlots = {
/**
* BaseTreeItem root wraps around `props.content`
*/
root: Slot<ARIAButtonSlotProps>;
root: Slot<'div'>;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import * as React from 'react';
import { getNativeElementProps, useEventCallback } from '@fluentui/react-utilities';
import type {
BaseTreeItemElement,
BaseTreeItemElementIntersection,
BaseTreeItemProps,
BaseTreeItemState,
} from './BaseTreeItem.types';
import { useARIAButtonProps } from '@fluentui/react-aria';
import { ArrowRight, ArrowLeft } from '@fluentui/keyboard-keys';
import type { BaseTreeItemProps, BaseTreeItemState } from './BaseTreeItem.types';
import { ArrowRight, ArrowLeft, Enter } from '@fluentui/keyboard-keys';
import { useTreeContext_unstable } from '../../contexts/treeContext';
/**
* Create the state required to render BaseTreeItem.
Expand All @@ -20,7 +14,7 @@ import { useTreeContext_unstable } from '../../contexts/treeContext';
*/
export const useBaseTreeItem_unstable = (
props: BaseTreeItemProps,
ref: React.Ref<BaseTreeItemElement>,
ref: React.Ref<HTMLDivElement>,
): BaseTreeItemState => {
const { 'aria-owns': ariaOwns, as = 'div', onKeyDown, ...rest } = props;

Expand All @@ -32,33 +26,41 @@ export const useBaseTreeItem_unstable = (
const isBranch = typeof ariaOwns === 'string';
const open = useTreeContext_unstable(ctx => isBranch && ctx.openSubtrees.includes(ariaOwns!));

const handleClick = useEventCallback((event: React.MouseEvent<BaseTreeItemElementIntersection>) => {
const handleClick = useEventCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (isBranch) {
requestOpenChange({ event, open: !open, type: 'click', id: ariaOwns! });
}
});
const handleArrowRight = (event: React.KeyboardEvent<BaseTreeItemElementIntersection>) => {
const handleArrowRight = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (open && isBranch) {
focusFirstSubtreeItem(event.currentTarget);
}
if (isBranch && !open) {
requestOpenChange({ event, open: true, type: 'arrowRight', id: ariaOwns! });
}
};
const handleArrowLeft = (event: React.KeyboardEvent<BaseTreeItemElementIntersection>) => {
const handleArrowLeft = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!isBranch || !open) {
focusSubtreeOwnerItem(event.currentTarget);
}
if (isBranch && open) {
requestOpenChange({ event, open: false, type: 'arrowLeft', id: ariaOwns! });
}
};
const handleKeyDown = useEventCallback((event: React.KeyboardEvent<BaseTreeItemElementIntersection>) => {
const handleEnter = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (isBranch) {
requestOpenChange({ event, open: !open, type: 'enter', id: ariaOwns! });
}
};
const handleKeyDown = useEventCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
onKeyDown?.(event);
if (event.isDefaultPrevented()) {
return;
}
switch (event.code) {
switch (event.key) {
case Enter: {
return handleEnter(event);
Comment thread
bsunderhus marked this conversation as resolved.
}
case ArrowRight: {
return handleArrowRight(event);
}
Expand All @@ -73,20 +75,17 @@ export const useBaseTreeItem_unstable = (
},
isLeaf: !isBranch,
open,
root: getNativeElementProps(
as,
useARIAButtonProps(as, {
...rest,
// casting here is required to convert union to intersection
ref: ref as React.Ref<BaseTreeItemElementIntersection>,
'aria-owns': ariaOwns,
'aria-level': level,
// FIXME: tabster fails to navigate when aria-expanded is true
// 'aria-expanded': isBranch ? isOpen : undefined,
role: 'treeitem',
onClick: handleClick,
onKeyDown: handleKeyDown,
}),
),
root: getNativeElementProps(as, {
...rest,
ref,
tabIndex: 0,
'aria-owns': ariaOwns,
'aria-level': level,
// FIXME: tabster fails to navigate when aria-expanded is true
// 'aria-expanded': isBranch ? isOpen : undefined,
role: 'treeitem',
onClick: handleClick,
onKeyDown: handleKeyDown,
}),
};
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { BaseTreeItemElement } from '../BaseTreeItem/BaseTreeItem.types';
import { TreeContextValue } from '../../contexts/treeContext';

export type TreeSlots = {
Expand All @@ -9,16 +8,24 @@ export type TreeSlots = {

export type TreeOpenChangeData = { open: boolean; id: string } & (
| {
event: React.MouseEvent<BaseTreeItemElement>;
event: React.MouseEvent<HTMLElement>;
type: 'expandIconClick';
}
| {
event: React.MouseEvent<BaseTreeItemElement>;
event: React.MouseEvent<HTMLElement>;
type: 'click';
}
| {
event: React.KeyboardEvent<BaseTreeItemElement>;
type: 'arrowRight' | 'arrowLeft';
event: React.KeyboardEvent<HTMLElement>;
type: 'enter';
}
| {
event: React.KeyboardEvent<HTMLElement>;
type: 'arrowRight';
}
| {
event: React.KeyboardEvent<HTMLElement>;
type: 'arrowLeft';
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { render } from '@testing-library/react';
import { TreeItem } from './TreeItem';
import { isConformant } from '../../testing/isConformant';
import { TreeItemProps } from './TreeItem.types';
import { treeItemClassNames } from './useTreeItemStyles';

describe('TreeItem', () => {
isConformant<TreeItemProps>({
Component: TreeItem,
displayName: 'TreeItem',
// primarySlot: 'groupper',
getTargetElement(renderResult, attr) {
return renderResult.container.querySelector(`.${treeItemClassNames.root}`) ?? renderResult.container;
},
disabledTests: ['component-has-static-classnames-object'],
testOptions: {
'has-static-classnames': [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import {
BaseTreeItemElement,
BaseTreeItemElementIntersection,
BaseTreeItemProps,
BaseTreeItemSlots,
BaseTreeItemState,
} from '../BaseTreeItem/index';

export type TreeItemElement = BaseTreeItemElement;

/** @internal */
export type TreeItemElementIntersection = BaseTreeItemElementIntersection;
import { BaseTreeItemProps, BaseTreeItemSlots, BaseTreeItemState } from '../BaseTreeItem/index';

export type TreeItemSlots = BaseTreeItemSlots & {
/**
Expand All @@ -35,6 +24,7 @@ export type TreeItemSlots = BaseTreeItemSlots & {
* when the item is hovered/focused
*/
actions?: Slot<'span'>;
groupper: NonNullable<Slot<'span'>>;
};

/**
Expand All @@ -45,4 +35,10 @@ export type TreeItemProps = ComponentProps<Partial<TreeItemSlots>> & BaseTreeIte
/**
* State used in rendering TreeItem
*/
export type TreeItemState = ComponentState<TreeItemSlots> & BaseTreeItemState;
export type TreeItemState = ComponentState<TreeItemSlots> &
BaseTreeItemState & {
/**
* boolean indicating that actions should remain open due to focus on some portal
*/
keepActionsOpen: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

exports[`TreeItem renders a default state 1`] = `
<div>
<div
aria-level="0"
class="fui-TreeItem"
role="treeitem"
style="--fluent-TreeItem--level: -1;"
tabindex="0"
<span
class="fui-TreeItem__groupper"
data-tabster="{\\"groupper\\":{}}"
role="presentation"
>
Default TreeItem
</div>
<div
aria-level="0"
class="fui-TreeItem"
role="treeitem"
style="--fluent-TreeItem--level: -1;"
tabindex="0"
>
Default TreeItem
</div>
</span>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ export const renderTreeItem_unstable = (state: TreeItemState) => {
const { slots, slotProps } = getSlots<TreeItemSlots>(state);

return (
<slots.root {...slotProps.root}>
{slots.expandIcon && <slots.expandIcon {...slotProps.expandIcon} />}
{slots.iconBefore && <slots.iconBefore {...slotProps.iconBefore} />}
{slotProps.root.children}
{slots.iconAfter && <slots.iconAfter {...slotProps.iconAfter} />}
{slots.badges && <slots.badges {...slotProps.badges} />}
{slots.actions && <slots.actions {...slotProps.actions} />}
</slots.root>
<slots.groupper {...slotProps.groupper}>
Comment thread
bsunderhus marked this conversation as resolved.
<slots.root {...slotProps.root}>
{slots.expandIcon && <slots.expandIcon {...slotProps.expandIcon} />}
{slots.iconBefore && <slots.iconBefore {...slotProps.iconBefore} />}
{slotProps.root.children}
{slots.iconAfter && <slots.iconAfter {...slotProps.iconAfter} />}
{slots.badges && <slots.badges {...slotProps.badges} />}
{slots.actions && <slots.actions {...slotProps.actions} />}
</slots.root>
</slots.groupper>
);
};
Loading