Skip to content

Commit

Permalink
Styles for Tree (#25949)
Browse files Browse the repository at this point in the history
* feat: creates TreeItem styles

* chore: hoist appearance and size from item to parent

* feat: uses css variables to provide only level dynamically

* Update packages/react-components/react-tree/src/components/TreeItem/useTreeItemStyles.ts

Co-authored-by: Sean Monahan <[email protected]>

* fix: updates snapshots

* feat: adds badges slots

* chore: switch from stop propagation to preventing default

Co-authored-by: Bernardo Sunderhus <[email protected]>
Co-authored-by: Sean Monahan <[email protected]>
  • Loading branch information
3 people authored Dec 22, 2022
1 parent 2d76331 commit 68c1cd9
Show file tree
Hide file tree
Showing 21 changed files with 617 additions and 28 deletions.
11 changes: 8 additions & 3 deletions packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export const treeClassNames: SlotClassNames<TreeSlots>;
export type TreeContextValue = {
level: number;
openSubtrees: string[];
appearance: 'subtle' | 'subtle-alpha' | 'transparent';
size: 'small' | 'medium';
focusFirstSubtreeItem(target: HTMLElement): void;
focusSubtreeOwnerItem(target: HTMLElement): void;
requestOpenChange(data: TreeOpenChangeData): void;
Expand All @@ -70,21 +72,24 @@ export const TreeItem: ForwardRefComponent<TreeItemProps>;
export const treeItemClassNames: SlotClassNames<TreeItemSlots>;

// @public
export type TreeItemProps = ComponentProps<TreeItemSlots> & BaseTreeItemProps;
export type TreeItemProps = ComponentProps<Partial<TreeItemSlots>> & BaseTreeItemProps;

// @public (undocumented)
export type TreeItemSlots = BaseTreeItemSlots & {
expandIcon?: Slot<'span'>;
iconBefore?: Slot<'span'>;
iconAfter?: Slot<'span'>;
actionIcon?: Slot<'span'>;
badges?: Slot<'span'>;
actions?: Slot<'span'>;
};

// @public
export type TreeItemState = ComponentState<TreeItemSlots>;
export type TreeItemState = ComponentState<TreeItemSlots> & BaseTreeItemState;

// @public (undocumented)
export type TreeProps = ComponentProps<TreeSlots> & {
appearance?: 'subtle' | 'subtle-alpha' | 'transparent';
size?: 'small' | 'medium';
openSubtrees?: string | string[];
defaultOpenSubtrees?: string | string[];
onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export type TreeSlots = {
};

export type TreeOpenChangeData = { open: boolean; id: string } & (
| {
event: React.MouseEvent<BaseTreeItemElement>;
type: 'expandIconClick';
}
| {
event: React.MouseEvent<BaseTreeItemElement>;
type: 'click';
Expand All @@ -25,6 +29,20 @@ export type TreeContextValues = {
};

export type TreeProps = ComponentProps<TreeSlots> & {
/**
* A tree item can have various appearances:
* - 'subtle' (default): The default tree item styles.
* - 'subtle-alpha': Minimizes emphasis on hovered or focused states.
* - 'transparent': Removes background color.
* @default 'subtle'
*/
appearance?: 'subtle' | 'subtle-alpha' | 'transparent';

/**
* Size of the tree item.
* @default 'medium'
*/
size?: 'small' | 'medium';
/**
* Controls the state of the open subtrees.
* These property is ignored for subtrees.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const useTree_unstable = (props: TreeProps, ref: React.Ref<HTMLElement>):
* @param ref - reference to root HTMLElement of Tree
*/
function useSubtree(props: TreeProps, ref: React.Ref<HTMLElement>): TreeState {
const { appearance = 'subtle', size = 'medium' } = props;
const parentLevel = useTreeContext_unstable(ctx => ctx.level);
const focusFirstSubtreeItem = useTreeContext_unstable(ctx => ctx.focusFirstSubtreeItem);
const focusSubtreeOwnerItem = useTreeContext_unstable(ctx => ctx.focusSubtreeOwnerItem);
Expand All @@ -62,6 +63,8 @@ function useSubtree(props: TreeProps, ref: React.Ref<HTMLElement>): TreeState {
components: {
root: 'div',
},
appearance,
size,
open,
level: parentLevel + 1,
openSubtrees,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ import { TreeContextValue } from '../../contexts';
import type { TreeContextValues, TreeState } from './Tree.types';

export function useTreeContextValues_unstable(state: TreeState): TreeContextValues {
const { openSubtrees, level, requestOpenChange, focusFirstSubtreeItem, focusSubtreeOwnerItem } = state;
const {
openSubtrees,
level,
appearance,
size,
requestOpenChange,
focusFirstSubtreeItem,
focusSubtreeOwnerItem,
} = state;
/**
* This context is created with "@fluentui/react-context-selector",
* there is no sense to memoize it
*/
const tree: TreeContextValue = {
appearance,
size,
level,
openSubtrees,
requestOpenChange,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { mergeClasses } from '@griffel/react';
import { makeStyles, mergeClasses } from '@griffel/react';
import type { TreeSlots, TreeState } from './Tree.types';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { tokens } from '@fluentui/react-theme';

export const treeClassNames: SlotClassNames<TreeSlots> = {
root: 'fui-Tree',
};

const useStyles = makeStyles({
root: {
display: 'flex',
flexDirection: 'column',
rowGap: tokens.spacingVerticalXXS,
},
});

export const useTreeStyles_unstable = (state: TreeState): TreeState => {
state.root.className = mergeClasses(treeClassNames.root, state.root.className);
const styles = useStyles();
state.root.className = mergeClasses(treeClassNames.root, styles.root, state.root.className);

return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('TreeItem', () => {
expandIcon: 'Test Expand Icon',
iconBefore: 'Test Icon Before',
iconAfter: 'Test Icon After',
actionIcon: 'test Action Icon',
actions: 'test Actions',
badges: 'test Badges',
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BaseTreeItemElementIntersection,
BaseTreeItemProps,
BaseTreeItemSlots,
BaseTreeItemState,
} from '../BaseTreeItem/index';

export type TreeItemElement = BaseTreeItemElement;
Expand All @@ -26,17 +27,22 @@ export type TreeItemSlots = BaseTreeItemSlots & {
*/
iconAfter?: Slot<'span'>;
/**
* Icon slot that renders on the end of the main content
* Actions slot that renders on the end of tree item
*/
actionIcon?: Slot<'span'>;
badges?: Slot<'span'>;
/**
* Actions slot that renders on the end of tree item
* when the item is hovered/focused
*/
actions?: Slot<'span'>;
};

/**
* TreeItem Props
*/
export type TreeItemProps = ComponentProps<TreeItemSlots> & BaseTreeItemProps;
export type TreeItemProps = ComponentProps<Partial<TreeItemSlots>> & BaseTreeItemProps;

/**
* State used in rendering TreeItem
*/
export type TreeItemState = ComponentState<TreeItemSlots>;
export type TreeItemState = ComponentState<TreeItemSlots> & BaseTreeItemState;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`TreeItem renders a default state 1`] = `
aria-level="0"
class="fui-TreeItem"
role="treeitem"
style="--fluent-TreeItem--level: -1;"
tabindex="0"
>
Default TreeItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export const renderTreeItem_unstable = (state: TreeItemState) => {
{slots.iconBefore && <slots.iconBefore {...slotProps.iconBefore} />}
{slotProps.root.children}
{slots.iconAfter && <slots.iconAfter {...slotProps.iconAfter} />}
{slots.actionIcon && <slots.actionIcon {...slotProps.actionIcon} />}
{slots.badges && <slots.badges {...slotProps.badges} />}
{slots.actions && <slots.actions {...slotProps.actions} />}
</slots.root>
);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { resolveShorthand } from '@fluentui/react-utilities';
import { isResolvedShorthand, resolveShorthand } from '@fluentui/react-utilities';
import type { TreeItemElement, TreeItemProps, TreeItemState } from './TreeItem.types';
import { ChevronRightRegular } from '@fluentui/react-icons';
import { useFluent_unstable } from '@fluentui/react-shared-contexts';
import { useBaseTreeItem_unstable } from '../BaseTreeItem/index';
import { useEventCallback } from '@fluentui/react-utilities';

/**
* Create the state required to render TreeItem.
Expand All @@ -16,17 +17,27 @@ import { useBaseTreeItem_unstable } from '../BaseTreeItem/index';
*/
export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref<TreeItemElement>): TreeItemState => {
const treeItemState = useBaseTreeItem_unstable(props, ref);
const { expandIcon, iconBefore, iconAfter, actionIcon } = props;
const { expandIcon, iconBefore, iconAfter, actions, badges } = props;
const { dir } = useFluent_unstable();
const expandIconRotation = treeItemState.open ? 90 : dir !== 'rtl' ? 0 : 180;

// prevent default of a click from actions to ensure it doesn't open the treeitem
const handleActionsClick = useEventCallback((event: React.MouseEvent<HTMLElement>) => {
if (isResolvedShorthand(actions)) {
actions.onClick?.(event);
}
event.preventDefault();
});

return {
...treeItemState,
components: {
...treeItemState.components,
expandIcon: 'span',
iconBefore: 'span',
iconAfter: 'span',
actionIcon: 'span',
actions: 'span',
badges: 'span',
},
iconBefore: resolveShorthand(iconBefore, {
defaultProps: { 'aria-hidden': true },
Expand All @@ -41,8 +52,18 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref<TreeIt
'aria-hidden': true,
},
}),
actionIcon: resolveShorthand(actionIcon, {
defaultProps: { 'aria-hidden': true },
badges: resolveShorthand(badges, {
defaultProps: {
'aria-hidden': true,
},
}),
actions: resolveShorthand(actions, {
defaultProps: {
// FIXME: this should not be aria-hidden as this should be reachable through tab
// without aria-hidden tabster navigation is breaking.
'aria-hidden': true,
onClick: handleActionsClick,
},
}),
};
};
Loading

0 comments on commit 68c1cd9

Please sign in to comment.