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; ...@@ -41,9 +41,13 @@ let ignoreNextClick = false;
* `false`, the consumer is responsible for positioning. * `false`, the consumer is responsible for positioning.
* @prop {string} [contentClass] - Additional CSS classes to apply to the menu. * @prop {string} [contentClass] - Additional CSS classes to apply to the menu.
* @prop {boolean} [defaultOpen] - Whether the menu is open or closed when initially rendered. * @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 * @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 * opened or closed. This can be used, for example, to reset any ephemeral state that the
* menu content may have. * 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 - * @prop {string} title -
* A title for the menu. This is important for accessibility if the menu's toggle button * A title for the menu. This is important for accessibility if the menu's toggle button
* has only an icon as a label. * has only an icon as a label.
...@@ -51,6 +55,8 @@ let ignoreNextClick = false; ...@@ -51,6 +55,8 @@ let ignoreNextClick = false;
* Whether to display an indicator next to the label that there is a dropdown menu. * Whether to display an indicator next to the label that there is a dropdown menu.
*/ */
const noop = () => {};
/** /**
* A drop-down menu. * A drop-down menu.
* *
...@@ -79,11 +85,17 @@ export default function Menu({ ...@@ -79,11 +85,17 @@ export default function Menu({
contentClass, contentClass,
defaultOpen = false, defaultOpen = false,
label, label,
open,
onOpenChanged, onOpenChanged,
menuIndicator = true, menuIndicator = true,
title, 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. // Notify parent when menu is opened or closed.
const wasOpen = useRef(isOpen); const wasOpen = useRef(isOpen);
...@@ -109,6 +121,7 @@ export default function Menu({ ...@@ -109,6 +121,7 @@ export default function Menu({
event.preventDefault(); event.preventDefault();
return; return;
} }
setOpen(!isOpen); setOpen(!isOpen);
}; };
const closeMenu = useCallback(() => setOpen(false), [setOpen]); const closeMenu = useCallback(() => setOpen(false), [setOpen]);
......
...@@ -54,6 +54,17 @@ describe('Menu', () => { ...@@ -54,6 +54,17 @@ describe('Menu', () => {
assert.isFalse(isOpen(wrapper)); 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', () => { it('calls `onOpenChanged` prop when menu is opened or closed', () => {
const onOpenChanged = sinon.stub(); const onOpenChanged = sinon.stub();
const wrapper = createMenu({ onOpenChanged }); const wrapper = createMenu({ onOpenChanged });
...@@ -74,6 +85,12 @@ describe('Menu', () => { ...@@ -74,6 +85,12 @@ describe('Menu', () => {
assert.isTrue(isOpen(wrapper)); 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', () => { it('renders the label', () => {
const wrapper = createMenu(); const wrapper = createMenu();
assert.isTrue(wrapper.exists(TestLabel)); 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