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
2 changes: 1 addition & 1 deletion packages/react-core/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
? Array.from(this.activeMenu.getElementsByTagName('UL')[0].children).filter(
el => !(el.classList.contains('pf-m-disabled') || el.classList.contains('pf-c-divider'))
)
: Array.from(this.activeMenu.getElementsByTagName('LI')).filter(
: Array.from(this.menuRef.current.getElementsByTagName('LI')).filter(
el => !(el.classList.contains('pf-m-disabled') || el.classList.contains('pf-c-divider'))
);
};
Expand Down
9 changes: 8 additions & 1 deletion packages/react-core/src/components/Panel/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ export interface PanelProps extends React.HTMLProps<HTMLDivElement> {
variant?: 'raised' | 'bordered';
/** Flag to add scrollable styling to the panel */
isScrollable?: boolean;
/** @hide Forwarded ref */
innerRef?: React.Ref<any>;
}

export const Panel: React.FunctionComponent<PanelProps> = ({
const PanelBase: React.FunctionComponent<PanelProps> = ({
className,
children,
variant,
isScrollable,
innerRef,
...props
}: PanelProps) => (
<div
Expand All @@ -28,10 +31,14 @@ export const Panel: React.FunctionComponent<PanelProps> = ({
isScrollable && styles.modifiers.scrollable,
className
)}
ref={innerRef}
{...props}
>
{children}
</div>
);

export const Panel = React.forwardRef((props: PanelProps, ref: React.Ref<any>) => (
<PanelBase innerRef={ref} {...props} />
));
Panel.displayName = 'Panel';
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Composable menus currently require consumer keyboard handling and use of our und

### Composable tree view menu

When rendering a menu-like element that does not contain MenuItem components, [Panel](/components/panel) allows more flexible control and customization.

```ts file="./examples/ComposableTreeViewMenu.tsx"
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import {
MenuToggle,
Menu,
MenuContent,
MenuGroup,
MenuList,
Panel,
PanelMain,
PanelMainBody,
Title,
Popper,
TreeView,
TreeViewDataItem
Expand All @@ -14,6 +14,7 @@ export const ComposableTreeViewMenu: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [checkedItems, setCheckedItems] = React.useState<TreeViewDataItem[]>([]);
const toggleRef = React.useRef<HTMLButtonElement>();
const containerRef = React.useRef<HTMLDivElement>();
const menuRef = React.useRef<HTMLDivElement>();

const statusOptions: TreeViewDataItem[] = [
Expand Down Expand Up @@ -85,7 +86,7 @@ export const ComposableTreeViewMenu: React.FunctionComponent = () => {
customBadgeContent: 0
}
];
// Helper functions
// Helper functions for tree
const isChecked = (dataItem: TreeViewDataItem) => checkedItems.some(item => item.id === dataItem.id);
const areAllDescendantsChecked = (dataItem: TreeViewDataItem) =>
dataItem.children ? dataItem.children.every(child => areAllDescendantsChecked(child)) : isChecked(dataItem);
Expand Down Expand Up @@ -163,18 +164,32 @@ export const ComposableTreeViewMenu: React.FunctionComponent = () => {
);
};

// Controls keys that should open/close the menu
const handleMenuKeys = (event: KeyboardEvent) => {
if (!isOpen) {
return;
}
if (menuRef.current.contains(event.target as Node) || toggleRef.current.contains(event.target as Node)) {
if (event.key === 'Escape' || event.key === 'Tab') {
// The escape key when pressed while inside the menu should close the menu and refocus the toggle
if (event.key === 'Escape') {
setIsOpen(!isOpen);
toggleRef.current.focus();
}

// The tab key when pressed while inside the menu and on the contained last tree view should close the menu and refocus the toggle
// Shift tab should keep the default behavior to return to a previous tree view
if (event.key === 'Tab' && !event.shiftKey) {
const treeList = menuRef.current.querySelectorAll('.pf-c-tree-view');
if (treeList[treeList.length - 1].contains(event.target as Node)) {
event.preventDefault();
setIsOpen(!isOpen);
toggleRef.current.focus();
}
}
}
};

// Controls that a click outside the menu while the menu is open should close the menu
const handleClickOutside = (event: MouseEvent) => {
if (isOpen && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
Expand All @@ -194,47 +209,66 @@ export const ComposableTreeViewMenu: React.FunctionComponent = () => {
ev.stopPropagation(); // Stop handleClickOutside from handling
setTimeout(() => {
if (menuRef.current) {
const firstElement = menuRef.current.querySelector('li > button:not(:disabled)');
const firstElement = menuRef.current.querySelector('li button:not(:disabled)');
firstElement && (firstElement as HTMLElement).focus();
}
}, 0);
setIsOpen(!isOpen);
};

const toggle = (
<MenuToggle ref={toggleRef} onClick={onToggleClick} isExpanded={isOpen}>
{isOpen ? 'Expanded' : 'Collapsed'}
</MenuToggle>
<div ref={containerRef}>
<MenuToggle ref={toggleRef} onClick={onToggleClick} isExpanded={isOpen}>
{isOpen ? 'Expanded' : 'Collapsed'}
</MenuToggle>
</div>
);
const statusMapped = statusOptions.map(mapTree);
const roleMapped = roleOptions.map(mapTree);
const menu = (
<Menu
<Panel
ref={menuRef}
// eslint-disable-next-line no-console
onSelect={(_ev, itemId) => console.log('selected', itemId)}
style={
{
'--pf-c-menu--Width': '300px'
} as React.CSSProperties
}
variant="raised"
style={{
width: '300px'
}}
>
<MenuContent>
<MenuList>
<MenuGroup label="Status">
<PanelMain>
<section>
<PanelMainBody style={{ paddingBottom: 0 }}>
<Title headingLevel="h1" size={'md'}>
Status
</Title>
</PanelMainBody>
<PanelMainBody style={{ padding: 0 }}>
<TreeView
data={statusMapped}
hasBadges
hasChecks
onCheck={(event, item) => onCheck(event, item, 'status')}
/>
</MenuGroup>
<MenuGroup label="Role">
</PanelMainBody>
</section>
<section>
<PanelMainBody style={{ paddingBottom: 0, paddingTop: 0 }}>
<Title headingLevel="h1" size={'md'}>
Roles
</Title>
</PanelMainBody>
<PanelMainBody style={{ padding: 0 }}>
<TreeView data={roleMapped} hasBadges hasChecks onCheck={(event, item) => onCheck(event, item, 'role')} />
</MenuGroup>
</MenuList>
</MenuContent>
</Menu>
</PanelMainBody>
</section>
</PanelMain>
</Panel>
);
return (
<Popper
trigger={toggle}
popper={menu}
isVisible={isOpen}
appendTo={containerRef.current}
popperMatchesTriggerWidth={false}
/>
);
return <Popper trigger={toggle} popper={menu} isVisible={isOpen} popperMatchesTriggerWidth={false} />;
};
9 changes: 7 additions & 2 deletions packages/react-core/src/helpers/KeyboardHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface KeyboardHandlerProps {
validSiblingTags?: string[];
/** Flag indicating that the tabIndex of the currently focused element and next focused element should be updated, in the case of using a roving tabIndex */
updateTabIndex?: boolean;
/** Flag indicating that next focusable element of a horizontal movement will be this element's sibling */
onlyTraverseSiblings?: boolean;
/** Flag indicating that the included vertical arrow key handling should be ignored */
noVerticalArrowHandling?: boolean;
/** Flag indicating that the included horizontal arrow key handling should be ignored */
Expand Down Expand Up @@ -176,6 +178,7 @@ export class KeyboardHandler extends React.Component<KeyboardHandlerProps> {
isActiveElement: (navigableElement: Element) => document.activeElement === navigableElement,
getFocusableElement: (navigableElement: Element) => navigableElement,
validSiblingTags: ['BUTTON', 'A'],
onlyTraverseSiblings: true,
updateTabIndex: true,
noHorizontalArrowHandling: false,
noVerticalArrowHandling: false,
Expand Down Expand Up @@ -212,7 +215,8 @@ export class KeyboardHandler extends React.Component<KeyboardHandlerProps> {
updateTabIndex,
validSiblingTags,
additionalKeyHandler,
createNavigableElements
createNavigableElements,
onlyTraverseSiblings
} = this.props;

// Pass the event off to be handled by any custom handler
Expand Down Expand Up @@ -256,7 +260,8 @@ export class KeyboardHandler extends React.Component<KeyboardHandlerProps> {
validSiblingTags,
noVerticalArrowHandling,
noHorizontalArrowHandling,
updateTabIndex
updateTabIndex,
onlyTraverseSiblings
);
};

Expand Down