Commit eae5627e authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Allow parent components to control `Menu` open state

Add `open` prop to allow components to do their own state management
of a `Menu`'s open/closed state.
parent 2874ac0a
......@@ -41,9 +41,13 @@ let ignoreNextClick = false;
* `false`, the consumer is responsible for positioning.
* @prop {string} [contentClass] - Additional CSS classes to apply to the menu.
* @prop {boolean} [defaultOpen] - Whether the menu is open or closed when initially rendered.
* Ignored if `open` is present.
* @prop {(open: boolean) => any} [onOpenChanged] - Callback invoked when the menu is
* opened or closed. This can be used, for example, to reset any ephemeral state that the
* menu content may have.
* @prop {boolean} [open] - Whether the menu is currently open; overrides internal state
* management for openness. External components managing state in this way should
* also pass an `onOpenChanged` handler to respond when the user closes the menu.
* @prop {string} title -
* A title for the menu. This is important for accessibility if the menu's toggle button
* has only an icon as a label.
......@@ -51,6 +55,8 @@ let ignoreNextClick = false;
* Whether to display an indicator next to the label that there is a dropdown menu.
*/
const noop = () => {};
/**
* A drop-down menu.
*
......@@ -79,11 +85,17 @@ export default function Menu({
contentClass,
defaultOpen = false,
label,
open,
onOpenChanged,
menuIndicator = true,
title,
}) {
const [isOpen, setOpen] = useState(defaultOpen);
/** @type {[boolean, (open: boolean) => void]} */
let [isOpen, setOpen] = useState(defaultOpen);
if (typeof open === 'boolean') {
isOpen = open;
setOpen = onOpenChanged || noop;
}
// Notify parent when menu is opened or closed.
const wasOpen = useRef(isOpen);
......@@ -109,6 +121,7 @@ export default function Menu({
event.preventDefault();
return;
}
setOpen(!isOpen);
};
const closeMenu = useCallback(() => setOpen(false), [setOpen]);
......
......@@ -54,6 +54,17 @@ describe('Menu', () => {
assert.isFalse(isOpen(wrapper));
});
it('leaves the management of open/closed state to parent component if `open` prop present', () => {
// When `open` is present, `Menu` will invoke `onOpenChanged` on interactions
// but will not modify the its open state directly.
const wrapper = createMenu({ open: true });
assert.isTrue(isOpen(wrapper));
wrapper.find('button').simulate('click');
assert.isTrue(isOpen(wrapper));
});
it('calls `onOpenChanged` prop when menu is opened or closed', () => {
const onOpenChanged = sinon.stub();
const wrapper = createMenu({ onOpenChanged });
......@@ -74,6 +85,12 @@ describe('Menu', () => {
assert.isTrue(isOpen(wrapper));
});
it('gives precedence to `open` over `defaultOpen`', () => {
const wrapper = createMenu({ open: true, defaultOpen: false });
assert.isTrue(isOpen(wrapper));
});
it('renders the label', () => {
const wrapper = createMenu();
assert.isTrue(wrapper.exists(TestLabel));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment