Skip to content

Commit

Permalink
chore: ensures functionality with Menu
Browse files Browse the repository at this point in the history
  • Loading branch information
bsunderhus committed Jan 4, 2023
1 parent d5cd7a5 commit 5a33f2e
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 44 deletions.
4 changes: 3 additions & 1 deletion packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export type TreeItemSlots = BaseTreeItemSlots & {
};

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

// @public (undocumented)
export type TreeProps = ComponentProps<TreeSlots> & {
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.14",
"@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
Expand Up @@ -35,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 @@ -5,10 +5,11 @@ import { useFluent_unstable } from '@fluentui/react-shared-contexts';
import { useEventCallback } from '@fluentui/react-utilities';
import { useFocusableGroup } from '@fluentui/react-tabster';
import { expandIconInlineStyles } from './useTreeItemStyles';
import type { TreeItemProps, TreeItemState } from './TreeItem.types';
import { useBaseTreeItem_unstable } from '../BaseTreeItem/index';
import { Enter } from '@fluentui/keyboard-keys';
import { useMergedRefs } from '@fluentui/react-utilities';
import { elementContains } from '@fluentui/react-portal';
import type { TreeItemProps, TreeItemState } from './TreeItem.types';

/**
* Create the state required to render TreeItem.
Expand All @@ -22,14 +23,49 @@ import { useMergedRefs } from '@fluentui/react-utilities';
export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref<HTMLDivElement>): TreeItemState => {
const treeItemState = useBaseTreeItem_unstable(props, ref);
const { expandIcon, iconBefore, iconAfter, actions, badges, groupper } = props;
const { dir } = useFluent_unstable();
const { dir, targetDocument } = useFluent_unstable();
const expandIconRotation = treeItemState.open ? 90 : dir !== 'rtl' ? 0 : 180;
const groupperProps = useFocusableGroup();

const actionsRef = React.useRef<HTMLElement>(null);

const handleClick = useEventCallback((event: React.MouseEvent<HTMLDivElement>) => {
// if click event originates from actions, ignore it
if (actionsRef.current && elementContains(actionsRef.current, event.target as Node)) {
return;
}
treeItemState.root.onClick?.(event);
});

const handleKeyDown = useEventCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === Enter) {
// if Enter keydown event comes from actions, ignore it
if (actionsRef.current && elementContains(actionsRef.current, event.target as Node)) {
return;
}
}
treeItemState.root.onKeyDown?.(event);
});

const [keepActionsOpen, setKeepActionsOpen] = React.useState(false);

// Listens to focusout event on the document to ensure treeitem actions visibility on portal scenarios
// TODO: find a better way to ensure this behavior
React.useEffect(() => {
if (actionsRef.current) {
const handleFocusOut = (event: FocusEvent) => {
setKeepActionsOpen(elementContains(actionsRef.current, event.relatedTarget as Node));
};
targetDocument?.addEventListener('focusout', handleFocusOut, { passive: true });
return () => {
targetDocument?.removeEventListener('focusout', handleFocusOut);
};
}
}, [targetDocument]);

return {
...treeItemState,
keepActionsOpen,
components: {
...treeItemState.components,
expandIcon: 'span',
Expand All @@ -41,26 +77,8 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref<HTMLDi
},
root: {
...treeItemState.root,
onClick: useEventCallback(event => {
// if click event originates from actions, ignore it
if (actionsRef.current && actionsRef.current?.contains(event.target as Node)) {
return;
}
// if click event comes from a portal, e.g: MenuItem click, ignore it
if (!event.currentTarget.contains(event.target as Node)) {
return;
}
treeItemState.root.onClick?.(event);
}),
onKeyDown: useEventCallback(event => {
if (event.key === Enter) {
// if Enter keydown event comes from actions, ignore it
if (actionsRef.current && actionsRef.current.contains(event.target as Node)) {
return;
}
}
treeItemState.root.onKeyDown?.(event);
}),
onClick: handleClick,
onKeyDown: handleKeyDown,
},
groupper: resolveShorthand(groupper, {
required: true,
Expand All @@ -84,7 +102,6 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref<HTMLDi
'aria-hidden': true,
},
}),
// FIXME: Menu only works if it's inline since actions is not available to properly position anchor.
actions: resolveShorthand(actions, {
defaultProps: {
ref: useMergedRefs(isResolvedShorthand(actions) ? actions.ref : undefined, actionsRef),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ const useActionsStyles = makeStyles({
marginLeft: 'auto',
...shorthands.padding(0, tokens.spacingHorizontalXS),
},
open: {
opacity: '1',
position: 'relative',
},
});

export const expandIconInlineStyles = {
Expand Down Expand Up @@ -268,7 +272,12 @@ export const useTreeItemStyles_unstable = (state: TreeItemState): TreeItemState
}

if (actions) {
actions.className = mergeClasses(treeItemClassNames.actions, actionsStyles.base, actions.className);
actions.className = mergeClasses(
treeItemClassNames.actions,
actionsStyles.base,
state.keepActionsOpen && actionsStyles.open,
actions.className,
);
}
if (badges) {
badges.className = mergeClasses(treeItemClassNames.badges, badgesStyles.base, badges.className);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@ import { Tree, TreeItem } from '@fluentui/react-tree';
import { Edit20Regular, MoreHorizontal20Regular } from '@fluentui/react-icons';
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger } from '@fluentui/react-components';

const RenderActions = () => (
<>
<Button appearance="subtle" icon={<Edit20Regular />} />
<Menu inline>
<MenuTrigger disableButtonEnhancement>
<Button appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
const RenderActions = () => {
return (
<>
<Button appearance="subtle" icon={<Edit20Regular />} />
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>

<MenuPopover>
<MenuList>
<MenuItem>New </MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</>
);
<MenuPopover>
<MenuList>
<MenuItem>New </MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</>
);
};

export const Actions = () => (
<Tree aria-label="Tree">
Expand Down

0 comments on commit 5a33f2e

Please sign in to comment.