Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: openItems property added to TreeOpenChangeData + minor internal improvements #28491

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "chore: openItems property added to TreeOpenChangeData + minor internal improvements",
"packageName": "@fluentui/react-tree",
"email": "bernardo.sunderhus@gmail.com",
"dependentChangeType": "patch"
}
39 changes: 20 additions & 19 deletions packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@

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

import { ArrowDown } from '@fluentui/keyboard-keys';
import { ArrowLeft } from '@fluentui/keyboard-keys';
import { ArrowRight } from '@fluentui/keyboard-keys';
import { ArrowUp } from '@fluentui/keyboard-keys';
import type { ArrowDown } from '@fluentui/keyboard-keys';
import type { ArrowLeft } from '@fluentui/keyboard-keys';
import type { ArrowRight } from '@fluentui/keyboard-keys';
import type { ArrowUp } from '@fluentui/keyboard-keys';
import type { AvatarContextValue } from '@fluentui/react-avatar';
import type { AvatarSize } from '@fluentui/react-avatar';
import { ButtonContextValue } from '@fluentui/react-button';
import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import { ContextSelector } from '@fluentui/react-context-selector';
import { End } from '@fluentui/keyboard-keys';
import { Enter } from '@fluentui/keyboard-keys';
import type { End } from '@fluentui/keyboard-keys';
import type { Enter } from '@fluentui/keyboard-keys';
import type { ExtractSlotProps } from '@fluentui/react-utilities';
import { FC } from 'react';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { Home } from '@fluentui/keyboard-keys';
import type { Home } from '@fluentui/keyboard-keys';
import { Provider } from 'react';
import { ProviderProps } from 'react';
import * as React_2 from 'react';
Expand Down Expand Up @@ -51,19 +51,20 @@ export type FlatTreeItem<Props extends FlatTreeItemProps = FlatTreeItemProps> =
};

// @public (undocumented)
export type FlatTreeItemProps = Omit<TreeItemProps, 'itemType'> & Partial<Pick<TreeItemProps, 'itemType'>> & {
export type FlatTreeItemProps = Omit<TreeItemProps, 'itemType' | 'value'> & Partial<Pick<TreeItemProps, 'itemType'>> & {
value: TreeItemValue;
parentValue?: TreeItemValue;
};

// @public (undocumented)
export type FlatTreeProps = Required<Pick<TreeProps, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'>> & {
ref: React_2.Ref<HTMLDivElement>;
openItems: ImmutableSet<string>;
openItems: ImmutableSet<TreeItemValue>;
};

// @public (undocumented)
export type NestedTreeItem<Props extends TreeItemProps> = Omit<Props, 'subtree' | 'itemType'> & {
value: TreeItemValue;
subtree?: NestedTreeItem<Props>[];
};

Expand All @@ -90,7 +91,7 @@ export type TreeContextValue = {
level: number;
appearance: 'subtle' | 'subtle-alpha' | 'transparent';
size: 'small' | 'medium';
openItems: ImmutableSet<unknown>;
openItems: ImmutableSet<TreeItemValue>;
requestTreeResponse(request: TreeItemRequest): void;
};

Expand Down Expand Up @@ -175,10 +176,13 @@ export type TreeItemState = ComponentState<TreeItemInternalSlot> & TreeItemConte
itemType: TreeItemType;
};

// @public (undocumented)
export type TreeItemValue = string | number;

// @public (undocumented)
export type TreeNavigationData_unstable = {
target: HTMLElement;
value: string;
value: TreeItemValue;
} & ({
event: React_2.MouseEvent<HTMLElement>;
type: 'Click';
Expand Down Expand Up @@ -211,26 +215,23 @@ export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event'];
// @public (undocumented)
export type TreeOpenChangeData = {
open: boolean;
value: string;
value: TreeItemValue;
target: HTMLElement;
openItems: ImmutableSet<TreeItemValue>;
} & ({
event: React_2.MouseEvent<HTMLElement>;
target: HTMLElement;
type: 'ExpandIconClick';
} | {
event: React_2.MouseEvent<HTMLElement>;
target: HTMLElement;
type: 'Click';
} | {
event: React_2.KeyboardEvent<HTMLElement>;
target: HTMLElement;
type: typeof Enter;
} | {
event: React_2.KeyboardEvent<HTMLElement>;
target: HTMLElement;
type: typeof ArrowRight;
} | {
event: React_2.KeyboardEvent<HTMLElement>;
target: HTMLElement;
type: typeof ArrowLeft;
});

Expand All @@ -241,8 +242,8 @@ export type TreeOpenChangeEvent = TreeOpenChangeData['event'];
export type TreeProps = ComponentProps<TreeSlots> & {
appearance?: 'subtle' | 'subtle-alpha' | 'transparent';
size?: 'small' | 'medium';
openItems?: Iterable<string>;
defaultOpenItems?: Iterable<string>;
openItems?: Iterable<TreeItemValue>;
defaultOpenItems?: Iterable<TreeItemValue>;
onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void;
onNavigation_unstable?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import * as React from 'react';
import type * as React from 'react';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { TreeContextValue } from '../../contexts/treeContext';
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Enter, Home } from '@fluentui/keyboard-keys';
import type { TreeContextValue } from '../../contexts/treeContext';
import type { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Enter, Home } from '@fluentui/keyboard-keys';
import type { TreeItemValue } from '../TreeItem/TreeItem.types';
import { ImmutableSet } from '../../utils/ImmutableSet';

export type TreeSlots = {
root: Slot<'div'>;
};

// eslint-disable-next-line @typescript-eslint/naming-convention
export type TreeNavigationData_unstable = { target: HTMLElement; value: string } & (
export type TreeNavigationData_unstable = { target: HTMLElement; value: TreeItemValue } & (
| { event: React.MouseEvent<HTMLElement>; type: 'Click' }
| { event: React.KeyboardEvent<HTMLElement>; type: 'TypeAhead' }
| { event: React.KeyboardEvent<HTMLElement>; type: typeof ArrowRight }
Expand All @@ -22,32 +24,17 @@ export type TreeNavigationData_unstable = { target: HTMLElement; value: string }
// eslint-disable-next-line @typescript-eslint/naming-convention
export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event'];

export type TreeOpenChangeData = { open: boolean; value: string } & (
| {
event: React.MouseEvent<HTMLElement>;
target: HTMLElement;
type: 'ExpandIconClick';
}
| {
event: React.MouseEvent<HTMLElement>;
target: HTMLElement;
type: 'Click';
}
| {
event: React.KeyboardEvent<HTMLElement>;
target: HTMLElement;
type: typeof Enter;
}
| {
event: React.KeyboardEvent<HTMLElement>;
target: HTMLElement;
type: typeof ArrowRight;
}
| {
event: React.KeyboardEvent<HTMLElement>;
target: HTMLElement;
type: typeof ArrowLeft;
}
export type TreeOpenChangeData = {
open: boolean;
value: TreeItemValue;
target: HTMLElement;
openItems: ImmutableSet<TreeItemValue>;
} & (
| { event: React.MouseEvent<HTMLElement>; type: 'ExpandIconClick' }
| { event: React.MouseEvent<HTMLElement>; type: 'Click' }
| { event: React.KeyboardEvent<HTMLElement>; type: typeof Enter }
| { event: React.KeyboardEvent<HTMLElement>; type: typeof ArrowRight }
| { event: React.KeyboardEvent<HTMLElement>; type: typeof ArrowLeft }
);

export type TreeOpenChangeEvent = TreeOpenChangeData['event'];
Expand Down Expand Up @@ -75,13 +62,13 @@ export type TreeProps = ComponentProps<TreeSlots> & {
* Controls the state of the open tree items.
* These property is ignored for subtrees.
*/
openItems?: Iterable<string>;
openItems?: Iterable<TreeItemValue>;
/**
* This refers to a list of ids of opened tree items.
* Default value for the uncontrolled state of open tree items.
* These property is ignored for subtrees.
*/
defaultOpenItems?: Iterable<string>;
defaultOpenItems?: Iterable<TreeItemValue>;
/**
* Callback fired when the component changes value from open state.
* These property is ignored for subtrees.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { getNativeElementProps, useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import { TreeOpenChangeData, TreeProps, TreeState, TreeNavigationData_unstable } from './Tree.types';
import { useNestedTreeNavigation, useOpenItemsState } from '../../hooks';
import type { TreeOpenChangeData, TreeProps, TreeState, TreeNavigationData_unstable } from './Tree.types';
import { createNextOpenItems, useControllableOpenItems, useNestedTreeNavigation } from '../../hooks';
import { treeDataTypes } from '../../utils/tokens';
import { TreeItemRequest } from '../../contexts/index';
import { TreeItemRequest } from '../../contexts';

/**
* Create the state required to render the root level Tree.
Expand All @@ -17,15 +17,17 @@ export function useRootTree(props: TreeProps, ref: React.Ref<HTMLElement>): Tree

const { appearance = 'subtle', size = 'medium' } = props;

const [openItems, updateOpenItems] = useOpenItemsState(props);
const [openItems, setOpenItems] = useControllableOpenItems(props);

const [navigate, navigationRef] = useNestedTreeNavigation();

const requestOpenChange = (data: TreeOpenChangeData) => {
props.onOpenChange?.(data.event, data);
const requestOpenChange = (data: Omit<TreeOpenChangeData, 'openItems'>) => {
const nextOpenItems = createNextOpenItems(data, openItems);
props.onOpenChange?.(data.event, { ...data, openItems: nextOpenItems } as TreeOpenChangeData);
if (data.event.isDefaultPrevented()) {
return;
}
return updateOpenItems(data);
return setOpenItems(nextOpenItems);
};

const requestNavigation = (data: TreeNavigationData_unstable) => {
Expand All @@ -39,82 +41,42 @@ export function useRootTree(props: TreeProps, ref: React.Ref<HTMLElement>): Tree
}
};

const handleTreeItemClick = ({
event,
value,
itemType,
type,
}: Extract<TreeItemRequest, { type: 'Click' | 'ExpandIconClick' }>) => {
ReactDOM.unstable_batchedUpdates(() => {
requestOpenChange({
event,
value,
open: itemType === 'branch' && !openItems.has(value),
type,
target: event.currentTarget,
});
requestNavigation({ event, value, target: event.currentTarget, type: treeDataTypes.Click });
});
};

const handleTreeItemKeyDown = ({
event,
type,
value,
itemType,
}: Exclude<TreeItemRequest, { type: 'Click' | 'ExpandIconClick' }>) => {
const open = openItems.has(value);
switch (type) {
case treeDataTypes.ArrowRight:
if (itemType === 'leaf') {
const requestTreeResponse = useEventCallback((request: TreeItemRequest) => {
switch (request.type) {
case treeDataTypes.Click:
case treeDataTypes.ExpandIconClick: {
return ReactDOM.unstable_batchedUpdates(() => {
requestOpenChange({ ...request, open: request.itemType === 'branch' && !openItems.has(request.value) });
requestNavigation({ ...request, type: treeDataTypes.Click });
});
}
case treeDataTypes.ArrowRight: {
if (request.itemType === 'leaf') {
return;
}
const open = openItems.has(request.value);
if (!open) {
return requestOpenChange({
event,
value,
open: true,
type: treeDataTypes.ArrowRight,
target: event.currentTarget,
});
return requestOpenChange({ ...request, open: true });
}
return requestNavigation({ event, value, type, target: event.currentTarget });
case treeDataTypes.Enter:
return requestOpenChange({
event,
value,
open: itemType === 'branch' && !open,
type: treeDataTypes.Enter,
target: event.currentTarget,
});
case treeDataTypes.ArrowLeft:
if (open && itemType === 'branch') {
return requestOpenChange({
event,
value,
open: false,
type: treeDataTypes.ArrowLeft,
target: event.currentTarget,
});
return requestNavigation(request);
}
case treeDataTypes.Enter: {
const open = openItems.has(request.value);
return requestOpenChange({ ...request, open: request.itemType === 'branch' && !open });
}
case treeDataTypes.ArrowLeft: {
const open = openItems.has(request.value);
if (open && request.itemType === 'branch') {
return requestOpenChange({ ...request, open: false, type: treeDataTypes.ArrowLeft });
}
return requestNavigation({ event, value, target: event.currentTarget, type: treeDataTypes.ArrowLeft });
return requestNavigation({ ...request, type: treeDataTypes.ArrowLeft });
}
case treeDataTypes.End:
case treeDataTypes.Home:
case treeDataTypes.ArrowUp:
case treeDataTypes.ArrowDown:
case treeDataTypes.TypeAhead:
return requestNavigation({ event, value, type, target: event.currentTarget });
}
};

const requestTreeResponse = useEventCallback((request: TreeItemRequest) => {
switch (request.event.type) {
case 'click':
// casting is required here as we're narrowing down the event to only click events
return handleTreeItemClick(request as Extract<TreeItemRequest, { type: 'Click' | 'ExpandIconClick' }>);
case 'keydown':
// casting is required here as we're narrowing down the event to only keyboard events
return handleTreeItemKeyDown(request as Exclude<TreeItemRequest, { type: 'Click' | 'ExpandIconClick' }>);
return requestNavigation({ ...request, target: request.event.currentTarget });
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,17 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
const isFromExpandIcon = expandIconRef.current && elementContains(expandIconRef.current, event.target as Node);
requestTreeResponse({
event,
itemType,
value,
itemType,
target: event.currentTarget,
type: isFromExpandIcon ? treeDataTypes.ExpandIconClick : treeDataTypes.Click,
});
});

const handleKeyDown = useEventCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
onKeyDown?.(event);
if (event.isDefaultPrevented()) {
return;
}
if (event.currentTarget !== event.target) {
// Ignore keyboard events that do not originate from the current tree item.
if (event.isDefaultPrevented() || event.currentTarget !== event.target) {
return;
}
switch (event.key) {
Expand All @@ -112,12 +111,12 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
case treeDataTypes.ArrowDown:
case treeDataTypes.ArrowLeft:
case treeDataTypes.ArrowRight:
return requestTreeResponse({ event, value, itemType, type: event.key });
return requestTreeResponse({ event, target: event.currentTarget, value, itemType, type: event.key });
}
const isTypeAheadCharacter =
event.key.length === 1 && event.key.match(/\w/) && !event.altKey && !event.ctrlKey && !event.metaKey;
if (isTypeAheadCharacter) {
requestTreeResponse({ event, value, itemType, type: treeDataTypes.TypeAhead });
requestTreeResponse({ event, target: event.currentTarget, value, itemType, type: treeDataTypes.TypeAhead });
}
});

Expand Down
Loading