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
10 changes: 6 additions & 4 deletions packages/react-core/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ export interface DropdownProps extends MenuProps, OUIAProps {
/** Function callback called when user selects item. */
onSelect?: (event?: React.MouseEvent<Element, MouseEvent>, itemId?: string | number) => void;
/** Callback to allow the dropdown component to change the open state of the menu.
* Triggered by clicking outside of the menu, or by pressing either tab or escape. */
* Triggered by clicking outside of the menu, or by pressing any keys specificed in onOpenChangeKeys. */
onOpenChange?: (isOpen: boolean) => void;
/** @beta Keys that trigger onOpenChange, defaults to tab and escape. It is highly recommended to include Escape in the array, while Tab may be omitted if the menu contains non-menu items that are focusable. */
onOpenChangeKeys?: string[];
/** Indicates if the menu should be without the outer box-shadow. */
isPlain?: boolean;
/** Indicates if the menu should be scrollable. */
Expand Down Expand Up @@ -76,6 +78,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
ouiaSafe = true,
zIndex = 9999,
popperProps,
onOpenChangeKeys = ['Escape', 'Tab'],
...props
}: DropdownProps) => {
const localMenuRef = React.useRef<HTMLDivElement>();
Expand All @@ -96,8 +99,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
onOpenChange &&
(menuRef.current?.contains(event.target as Node) || toggleRef.current?.contains(event.target as Node))
) {
if (event.key === 'Escape' || event.key === 'Tab') {
event.preventDefault();
if (onOpenChangeKeys.includes(event.key)) {
onOpenChange(false);
toggleRef.current?.focus();
}
Expand Down Expand Up @@ -130,7 +132,7 @@ const DropdownBase: React.FunctionComponent<DropdownProps> = ({
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef, toggleRef, onOpenChange]);
}, [isOpen, menuRef, toggleRef, onOpenChange, onOpenChangeKeys]);

const menu = (
<Menu
Expand Down
4 changes: 2 additions & 2 deletions packages/react-core/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ class MenuBase extends React.Component<MenuProps, MenuState> {
(document.activeElement.closest('ol') && document.activeElement.closest('ol').firstChild === element)
}
getFocusableElement={(navigableElement: Element) =>
(navigableElement.tagName === 'DIV' && navigableElement.querySelector('input')) || // for MenuSearchInput
((navigableElement.firstChild as Element).tagName === 'LABEL' &&
(navigableElement?.tagName === 'DIV' && navigableElement.querySelector('input')) || // for MenuSearchInput
((navigableElement.firstChild as Element)?.tagName === 'LABEL' &&
navigableElement.querySelector('input')) || // for MenuItem checkboxes
(navigableElement.firstChild as Element)
}
Expand Down
109 changes: 109 additions & 0 deletions packages/react-core/src/components/Menu/MenuContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import { Popper } from '../../helpers/Popper/Popper';

export interface MenuPopperProps {
/** Vertical direction of the popper. If enableFlip is set to true, this will set the initial direction before the popper flips. */
direction?: 'up' | 'down';
/** Horizontal position of the popper */
position?: 'right' | 'left' | 'center';
/** Custom width of the popper. If the value is "trigger", it will set the width to the dropdown toggle's width */
width?: string | 'trigger';
/** Minimum width of the popper. If the value is "trigger", it will set the min width to the dropdown toggle's width */
minWidth?: string | 'trigger';
/** Maximum width of the popper. If the value is "trigger", it will set the max width to the dropdown toggle's width */
maxWidth?: string | 'trigger';
/** Enable to flip the popper when it reaches the boundary */
enableFlip?: boolean;
}
export interface MenuContainerProps {
/** Menu to be rendered */
menu: React.ReactElement<any, string | React.JSXElementConstructor<any>>;
/** Reference to the menu */
menuRef: React.RefObject<any>;
/** Toggle to be rendered */
toggle: React.ReactNode;
/** Reference to the toggle */
toggleRef: React.RefObject<any>;
/** Flag to indicate if menu is opened.*/
isOpen: boolean;
/** Callback to change the open state of the menu.
* Triggered by clicking outside of the menu, or by pressing any keys specificed in onOpenChangeKeys. */
onOpenChange?: (isOpen: boolean) => void;
/** Keys that trigger onOpenChange, defaults to tab and escape. It is highly recommended to include Escape in the array, while Tab may be omitted if the menu contains non-menu items that are focusable. */
onOpenChangeKeys?: string[];
/** z-index of the dropdown menu */
zIndex?: number;
/** Additional properties to pass to the Popper */
popperProps?: MenuPopperProps;
}

/**
* Container that links a menu and menu toggle together, to handle basic keyboard input and control the opening and closing of a menu.
* This component is currently in beta and is subject to change.
*/
export const MenuContainer: React.FunctionComponent<MenuContainerProps> = ({
menu,
menuRef,
isOpen,
toggle,
toggleRef,
onOpenChange,
zIndex = 9999,
popperProps,
onOpenChangeKeys = ['Escape', 'Tab']
}: MenuContainerProps) => {
React.useEffect(() => {
const handleMenuKeys = (event: KeyboardEvent) => {
// Close the menu on tab or escape if onOpenChange is provided
if (
(isOpen && onOpenChange && menuRef.current?.contains(event.target as Node)) ||
toggleRef.current?.contains(event.target as Node)
) {
if (onOpenChangeKeys.includes(event.key)) {
onOpenChange(false);
toggleRef.current?.focus();
}
}
};

const handleClick = (event: MouseEvent) => {
// toggle was clicked open via keyboard, focus on first menu item
if (isOpen && toggleRef.current?.contains(event.target as Node) && event.detail === 0) {
setTimeout(() => {
const firstElement = menuRef?.current?.querySelector(
'li button:not(:disabled),li input:not(:disabled),li a:not([aria-disabled="true"])'
);
firstElement && (firstElement as HTMLElement).focus();
}, 0);
}

// If the event is not on the toggle and onOpenChange callback is provided, close the menu
if (isOpen && onOpenChange && !toggleRef?.current?.contains(event.target as Node)) {
if (isOpen && !menuRef.current?.contains(event.target as Node)) {
onOpenChange(false);
}
}
};

window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClick);

return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClick);
};
}, [isOpen, menuRef, onOpenChange, onOpenChangeKeys, toggleRef]);

return (
<Popper
trigger={toggle}
triggerRef={toggleRef}
popper={menu}
popperRef={menuRef}
isVisible={isOpen}
zIndex={zIndex}
{...popperProps}
/>
);
};
MenuContainer.displayName = 'MenuContainer';
14 changes: 13 additions & 1 deletion packages/react-core/src/components/Menu/examples/Menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,19 @@ id: Menu
section: components
subsection: menus
cssPrefix: pf-c-menu
propComponents: ['Menu', 'MenuList', 'MenuItem', 'MenuItemAction', 'MenuContent', 'MenuSearch', 'MenuSearchInput', 'MenuGroup']
propComponents:
[
'Menu',
'MenuList',
'MenuItem',
'MenuItemAction',
'MenuContent',
'MenuSearch',
'MenuSearchInput',
'MenuGroup',
'MenuContainer',
'MenuPopperProps'
]
ouia: true
---

Expand Down
1 change: 1 addition & 0 deletions packages/react-core/src/components/Menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './MenuList';
export * from './MenuItemAction';
export * from './DrilldownMenu';
export * from './MenuBreadcrumb';
export * from './MenuContainer';
118 changes: 0 additions & 118 deletions packages/react-core/src/demos/ComposableMenu/ComposableMenu.md

This file was deleted.

Loading