-
Notifications
You must be signed in to change notification settings - Fork 53
feat(Menu): Make Submenu open state controlled #1125
Changes from 1 commit
2b3138a
c660c35
0dab983
fd49061
f257d26
9e17e04
47e9223
c7e52d7
dc22ae6
452c04f
4a2d187
7dcf70c
3ba0657
6fcb8f1
94db879
00c4dd6
e92ad25
31945f6
96329f2
ac3c1d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import * as React from 'react' | ||
import { Menu } from '@stardust-ui/react' | ||
|
||
class MenuWithSubmenuControlledExample extends React.Component { | ||
state = { editorialsMenuOpen: false } | ||
|
||
handleMenuOpenChange = (e, { menuOpen }) => { | ||
this.setState({ editorialsMenuOpen: menuOpen }) | ||
} | ||
|
||
renderItems = () => { | ||
return [ | ||
{ | ||
key: 'editorials', | ||
content: 'Editorials', | ||
menuOpen: this.state.editorialsMenuOpen, | ||
onMenuOpenChange: this.handleMenuOpenChange, | ||
menu: { | ||
items: [ | ||
{ key: '1', content: 'item1' }, | ||
{ | ||
key: '2', | ||
content: 'item2', | ||
menu: [{ key: '1', content: 'item2.1' }, { key: '2', content: 'item2.2' }], | ||
}, | ||
{ | ||
key: '3', | ||
content: 'item3', | ||
menu: [{ key: '1', content: 'item3.1' }, { key: '2', content: 'item3.2' }], | ||
}, | ||
], | ||
}, | ||
}, | ||
{ key: 'events', content: 'Upcoming Events' }, | ||
] | ||
} | ||
|
||
render() { | ||
return ( | ||
<> | ||
<span>{`Editorials menu item requested to change its submenu open state to "${ | ||
this.state.editorialsMenuOpen | ||
}".`}</span> | ||
<Menu defaultActiveIndex={0} items={this.renderItems()} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks unreadable 🤔 What about something like this: <Header as="h5" content="Current state:" />
<pre>{JSON.stringify(this.state, null, 2)}</pre>
<Divider />
<Menu defaultActiveIndex={0} items={this.renderItems()} />
|
||
</> | ||
) | ||
} | ||
} | ||
|
||
export default MenuWithSubmenuControlledExample |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -122,6 +122,13 @@ export interface MenuItemProps | |||||
|
||||||
/** Shorthand for the submenu indicator. */ | ||||||
indicator?: ShorthandValue | ||||||
|
||||||
/** | ||||||
* Event for request to change 'open' value. | ||||||
* @param {SyntheticEvent} event - React's original SyntheticEvent. | ||||||
* @param {object} data - All props and proposed value. | ||||||
*/ | ||||||
onMenuOpenChange?: ComponentEventHandler<MenuItemProps> | ||||||
} | ||||||
|
||||||
export interface MenuItemState { | ||||||
|
@@ -165,6 +172,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt | |||||
onActiveChanged: PropTypes.func, | ||||||
inSubmenu: PropTypes.bool, | ||||||
indicator: customPropTypes.itemShorthand, | ||||||
onMenuOpenChange: PropTypes.func, | ||||||
} | ||||||
|
||||||
static defaultProps = { | ||||||
|
@@ -270,7 +278,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt | |||||
|
||||||
private handleWrapperBlur = e => { | ||||||
if (!this.props.inSubmenu && !e.currentTarget.contains(e.relatedTarget)) { | ||||||
this.setState({ menuOpen: false }) | ||||||
this.trySetMenuOpen(false, e) | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -288,19 +296,20 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt | |||||
!doesNodeContainClick(this.itemRef.current, e) && | ||||||
!doesNodeContainClick(this.menuRef.current, e) | ||||||
) { | ||||||
this.trySetState({ menuOpen: false }) | ||||||
this.trySetMenuOpen(false, e) | ||||||
} | ||||||
} | ||||||
|
||||||
private performClick = e => { | ||||||
const { active, menu } = this.props | ||||||
|
||||||
if (menu) { | ||||||
if (doesNodeContainClick(this.menuRef.current, e)) { | ||||||
// submenu was clicked => close it and propagate | ||||||
this.setState({ menuOpen: false }, () => focusAsync(this.itemRef.current)) | ||||||
this.trySetMenuOpen(false, e, () => focusAsync(this.itemRef.current)) | ||||||
} else { | ||||||
// the menuItem element was clicked => toggle the open/close and stop propagation | ||||||
this.trySetState({ menuOpen: active ? !this.state.menuOpen : true }) | ||||||
this.trySetMenuOpen(active ? !this.state.menuOpen : true, e) | ||||||
e.stopPropagation() | ||||||
e.preventDefault() | ||||||
} | ||||||
|
@@ -335,7 +344,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt | |||||
const { menu, inSubmenu } = this.props | ||||||
const { menuOpen } = this.state | ||||||
if (menu && menuOpen) { | ||||||
this.setState({ menuOpen: false }, () => { | ||||||
this.trySetMenuOpen(false, e, () => { | ||||||
if (!inSubmenu && (!focusNextParent || this.props.vertical)) { | ||||||
focusAsync(this.itemRef.current) | ||||||
} | ||||||
|
@@ -348,7 +357,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt | |||||
const { menuOpen } = this.state | ||||||
const shouldStopPropagation = inSubmenu || this.props.vertical | ||||||
if (menu && menuOpen) { | ||||||
this.setState({ menuOpen: false }, () => { | ||||||
this.trySetMenuOpen(false, e, () => { | ||||||
if (shouldStopPropagation) { | ||||||
focusAsync(this.itemRef.current) | ||||||
} | ||||||
|
@@ -363,12 +372,20 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt | |||||
const { menu } = this.props | ||||||
const { menuOpen } = this.state | ||||||
if (menu && !menuOpen) { | ||||||
this.setState({ menuOpen: true }) | ||||||
this.trySetMenuOpen(true, e) | ||||||
_.invoke(this.props, 'onActiveChanged', e, { ...this.props, active: true }) | ||||||
e.stopPropagation() | ||||||
e.preventDefault() | ||||||
} | ||||||
} | ||||||
|
||||||
private trySetMenuOpen(newValue: boolean, eventArgs: any, onStateChanged?: any) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
nit: Can we place There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fyi, the order is the same as in Popup for |
||||||
this.trySetState({ menuOpen: newValue }, null, onStateChanged) | ||||||
_.invoke(this.props, 'onMenuOpenChange', eventArgs, { | ||||||
...this.props, | ||||||
...{ menuOpen: newValue }, | ||||||
sophieH29 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
}) | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like this method, but I don't have a better option now... |
||||||
} | ||||||
|
||||||
MenuItem.create = createShorthandFactory({ Component: MenuItem, mappedProp: 'content' }) | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -187,8 +187,9 @@ export default class AutoControlledComponent<P = {}, S = {}> extends UIComponent | |
* Second argument is a state object that is always passed to setState. | ||
* @param {object} maybeState State that corresponds to controlled props. | ||
* @param {object} [state] Actual state, useful when you also need to setState. | ||
* @param {object} onStateChanged Callback which is called after setState applied. | ||
*/ | ||
trySetState = (maybeState, state?) => { | ||
trySetState = (maybeState, state?, onStateChanged?: () => void) => { | ||
sophieH29 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const { autoControlledProps } = this.constructor as any | ||
if (process.env.NODE_ENV !== 'production') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check should be removed, otherwise we will get incorrect warnings |
||
const { name } = this.constructor | ||
|
@@ -218,6 +219,6 @@ export default class AutoControlledComponent<P = {}, S = {}> extends UIComponent | |
|
||
if (state) newState = { ...newState, ...state } | ||
|
||
if (Object.keys(newState).length > 0) this.setState(newState) | ||
if (Object.keys(newState).length > 0) this.setState(newState, onStateChanged) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to make it more compact:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As it will become more compact, do we need
renderItems()
at all?