Skip to content

Commit

Permalink
Update context menu components to React 16.3 lifecycle (#887)
Browse files Browse the repository at this point in the history
* Refactor EuiContextMenu to React 16.3 lifecycle

* Refactor EuiContextMenuPanel to React 16.3 lifecycle, added more context menu examples

* changelog

* Delete commented-out code
  • Loading branch information
chandlerprall authored May 30, 2018
1 parent fcabc85 commit d3da542
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ No public facing changes since `0.0.51`

**Bug fixes**

- `EuiContextMenuPanel` now updates appropriately if its items are modified ([#887](https://github.com/elastic/eui/pull/887))
- `EuiComboBox` is no longer a focus trap, the clear button is now keyboard-accessible, and the virtualized list no longer interferes with the tab order ([#866](https://github.com/elastic/eui/pull/866))
- `EuiButton`, `EuiButtonEmpty`, and `EuiButtonIcon` now look and behave disabled when `isDisabled={true}` ([#862](https://github.com/elastic/eui/pull/862))
- `EuiGlobalToastList` no longer triggers `Uncaught TypeError: _this.callback is not a function` ([#865](https://github.com/elastic/eui/pull/865))
Expand Down
61 changes: 61 additions & 0 deletions src-docs/src/views/context_menu/content_panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, {
Component,
} from 'react';

import {
EuiButton,
EuiContextMenuPanel,
EuiPopover,
} from '../../../../src/components';

export default class extends Component {
constructor(props) {
super(props);

this.interval = undefined;

this.state = {
isPopoverOpen: false,
};
}

onButtonClick = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen
}));
};

closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};

render() {
const button = (
<EuiButton
size="s"
iconType="arrowDown"
iconSide="right"
onClick={this.onButtonClick}
>
Click to show some content
</EuiButton>
);

return (
<EuiPopover
id="contentPanel"
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="s"
anchorPosition="downLeft"
>
<EuiContextMenuPanel>
This context menu doesn&#39;t render items, it passes a child instead.
</EuiContextMenuPanel>
</EuiPopover>
);
}
}
41 changes: 41 additions & 0 deletions src-docs/src/views/context_menu/context_menu_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import SinglePanel from './single_panel';
const singlePanelSource = require('!!raw-loader!./single_panel');
const singlePanelHtml = renderToHtml(SinglePanel);

import ContentPanel from './content_panel';
const contentPanelSource = require('!!raw-loader!./content_panel');
const contentPanelHtml = renderToHtml(ContentPanel);

import ContextMenuWithContent from './context_menu_with_content';
const contextMenuWithContentSource = require('!!raw-loader!./context_menu_with_content');
const contextMenuWithContentHtml = renderToHtml(ContextMenuWithContent);

export const ContextMenuExample = {
title: 'Context Menu',
sections: [{
Expand Down Expand Up @@ -56,5 +64,38 @@ export const ContextMenuExample = {
</p>
),
demo: <SinglePanel />,
}, {
title: `Displaying custom elements`,
source: [{
type: GuideSectionTypes.JS,
code: contentPanelSource,
}, {
type: GuideSectionTypes.HTML,
code: contentPanelHtml,
}],
text: (
<p>
If you have custom content to show instead of a list of options,
you can pass a React element as a child to <EuiCode>EuiContextMenuPanel</EuiCode>.
</p>
),
demo: <ContentPanel />,
}, {
title: `Using panels with mixed items & content`,
source: [{
type: GuideSectionTypes.JS,
code: contextMenuWithContentSource,
}, {
type: GuideSectionTypes.HTML,
code: contextMenuWithContentHtml,
}],
text: (
<p>
Context menu panels can be passed React elements through the
<EuiCode>content</EuiCode> prop instead of <EuiCode>items</EuiCode>. The panel
will display your custom content without modification.
</p>
),
demo: <ContextMenuWithContent />,
}],
};
111 changes: 111 additions & 0 deletions src-docs/src/views/context_menu/context_menu_with_content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, {
Component,
} from 'react';

import {
EuiButton,
EuiContextMenu,
EuiIcon,
EuiPopover,
EuiPanel,
EuiCard
} from '../../../../src/components';

function flattenPanelTree(tree, array = []) {
array.push(tree);

if (tree.items) {
tree.items.forEach(item => {
if (item.panel) {
flattenPanelTree(item.panel, array);
item.panel = item.panel.id;
}
});
}

return array;
}

export default class extends Component {
constructor(props) {
super(props);

this.state = {
isPopoverOpen: false,
};

const panelTree = {
id: 0,
title: 'View options',
items: [{
name: 'Show fullscreen',
icon: (
<EuiIcon
type="search"
size="m"
/>
),
onClick: () => { this.closePopover(); window.alert('Show fullscreen'); },
}, {
name: 'See more',
icon: 'plusInCircle',
panel: {
id: 1,
title: 'See more',
content: (
<EuiPanel>
<EuiCard
icon={<EuiIcon size="l" type="bolt" />}
title="More Details"
description="This menu demonstrates using panels that have items and panels with content."
/>
</EuiPanel>
)
},
}],
};

this.panels = flattenPanelTree(panelTree);
}

onButtonClick = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};

closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};

render() {
const button = (
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={this.onButtonClick}
>
Click me to load mixed content menu
</EuiButton>
);

return (
<EuiPopover
id="contextMenu"
button={button}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
withTitle
anchorPosition="upLeft"
>
<EuiContextMenu
initialPanelId={0}
panels={this.panels}
/>
</EuiPopover>
);
}
}
74 changes: 41 additions & 33 deletions src/components/context_menu/context_menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,31 @@ export class EuiContextMenu extends Component {
panels: [],
};

static getDerivedStateFromProps(nextProps, prevState) {
const { panels } = nextProps;

if ( prevState.prevProps.panels !== panels ) {
return {
prevProps: { panels },
idToPanelMap: mapIdsToPanels(panels),
idToPreviousPanelIdMap: mapIdsToPreviousPanels(panels),
idAndItemIndexToPanelIdMap: mapPanelItemsToPanels(panels)
};
}

return null;
}

constructor(props) {
super(props);

this.idToPanelMap = {};
this.idToPreviousPanelIdMap = {};
this.idAndItemIndexToPanelIdMap = {};
this.idToRenderedItemsMap = {};

this.state = {
prevProps: {},
idToPanelMap: {},
idToPreviousPanelIdMap: {},
idAndItemIndexToPanelIdMap: {},
idToRenderedItemsMap: {},

height: undefined,
outgoingPanelId: undefined,
incomingPanelId: props.initialPanelId,
Expand All @@ -98,8 +114,18 @@ export class EuiContextMenu extends Component {
};
}

componentDidMount() {
this.mapIdsToRenderedItems(this.props.panels);
}

componentDidUpdate(prevProps) {
if (prevProps.panels !== this.props.panels) {
this.mapIdsToRenderedItems(this.props.panels);
}
}

hasPreviousPanel = panelId => {
const previousPanelId = this.idToPreviousPanelIdMap[panelId];
const previousPanelId = this.state.idToPreviousPanelIdMap[panelId];
return typeof previousPanelId !== 'undefined';
};

Expand All @@ -113,7 +139,7 @@ export class EuiContextMenu extends Component {
}

showNextPanel = itemIndex => {
const nextPanelId = this.idAndItemIndexToPanelIdMap[this.state.incomingPanelId][itemIndex];
const nextPanelId = this.state.idAndItemIndexToPanelIdMap[this.state.incomingPanelId][itemIndex];
if (nextPanelId) {
if (this.state.isUsingKeyboardToNavigate) {
this.setState({
Expand All @@ -128,10 +154,10 @@ export class EuiContextMenu extends Component {
showPreviousPanel = () => {
// If there's a previous panel, then we can close the current panel to go back to it.
if (this.hasPreviousPanel(this.state.incomingPanelId)) {
const previousPanelId = this.idToPreviousPanelIdMap[this.state.incomingPanelId];
const previousPanelId = this.state.idToPreviousPanelIdMap[this.state.incomingPanelId];

// Set focus on the item which shows the panel we're leaving.
const previousPanel = this.idToPanelMap[previousPanelId];
const previousPanel = this.state.idToPanelMap[previousPanelId];
const focusedItemIndex = previousPanel.items.findIndex(
item => item.panel === this.state.incomingPanelId
);
Expand Down Expand Up @@ -166,33 +192,15 @@ export class EuiContextMenu extends Component {
}
};

updatePanelMaps(panels) {
this.idToPanelMap = mapIdsToPanels(panels);
this.idToPreviousPanelIdMap = mapIdsToPreviousPanels(panels);
this.idAndItemIndexToPanelIdMap = mapPanelItemsToPanels(panels);
this.mapIdsToRenderedItems(panels);
}

// TODO: React 16.3 - move this into constructor
componentWillMount() {
this.updatePanelMaps(this.props.panels);
}

// TODO: React 16.3 - componentDidUpdate; alternatively refactor the panel mappings
// into state and use getDerivedStateFromProps
componentWillReceiveProps(nextProps) {
if (nextProps.panels !== this.props.panels) {
this.updatePanelMaps(nextProps.panels);
}
}

mapIdsToRenderedItems = panels => {
this.idToRenderedItemsMap = {};
const idToRenderedItemsMap = {};

// Pre-rendering the items lets us check reference equality inside of EuiContextMenuPanel.
panels.forEach(panel => {
this.idToRenderedItemsMap[panel.id] = this.renderItems(panel.items);
idToRenderedItemsMap[panel.id] = this.renderItems(panel.items);
});

this.setState({ idToRenderedItemsMap });
};

renderItems(items = []) {
Expand Down Expand Up @@ -237,7 +245,7 @@ export class EuiContextMenu extends Component {
}

renderPanel(panelId, transitionType) {
const panel = this.idToPanelMap[panelId];
const panel = this.state.idToPanelMap[panelId];

if (!panel) {
return;
Expand All @@ -261,7 +269,7 @@ export class EuiContextMenu extends Component {
transitionType={this.state.isOutgoingPanelVisible ? transitionType : undefined}
transitionDirection={this.state.isOutgoingPanelVisible ? this.state.transitionDirection : undefined}
hasFocus={transitionType === 'in'}
items={this.idToRenderedItemsMap[panelId]}
items={this.state.idToRenderedItemsMap[panelId]}
initialFocusedItemIndex={this.state.isUsingKeyboardToNavigate ? this.state.focusedItemIndex : undefined}
onUseKeyboardToNavigate={this.onUseKeyboardToNavigate}
showNextPanel={this.showNextPanel}
Expand Down
Loading

0 comments on commit d3da542

Please sign in to comment.