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
5 changes: 5 additions & 0 deletions packages/core/src/components/panel-stack/panel-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ the stack. Panels use
[`CSSTransition`](http://reactcommunity.org/react-transition-group/css-transition)
for seamless transitions.

By default, only the currently active panel is rendered to the DOM. This means
that other panels are unmounted and can lose their component state as a user
transitions between the panels. You can notice this in the example below as
the numeric counter is reset. To render all panels to the DOM and keep their
React trees mounted, change the `renderActivePanelOnly` prop.

@reactExample PanelStackExample

Expand Down
25 changes: 17 additions & 8 deletions packages/core/src/components/panel-stack/panelStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ export interface IPanelStackProps extends IProps {
onOpen?: (addedPanel: IPanel) => void;

/**
* If false, PanelStack will render all panels in the stack to the DOM, allowing their React component trees to maintain state as a user navigates through the stack. Panels other than the currently active one will be invisible.
* If false, PanelStack will render all panels in the stack to the DOM, allowing their
* React component trees to maintain state as a user navigates through the stack.
* Panels other than the currently active one will be invisible.
* @default true
*/
renderCurrentPanelOnly?: boolean;
renderActivePanelOnly?: boolean;

/**
* Whether to show the header with the "back" button in each panel.
Expand Down Expand Up @@ -122,22 +124,29 @@ export class PanelStack extends AbstractPureComponent2<IPanelStackProps, IPanelS
}

private renderPanels() {
const { renderCurrentPanelOnly = true } = this.props;
const { renderActivePanelOnly = true } = this.props;
const { stack } = this.state;
if (stack.length === 0) {
return null;
}
const panelsToRender = renderCurrentPanelOnly ? [stack[0]] : stack;
const panelsToRender = renderActivePanelOnly ? [stack[0]] : stack;
const panelViews = panelsToRender.map(this.renderPanel).reverse();
return panelViews;
}

private renderPanel = (panel: IPanel, index: number) => {
const { showPanelHeader = true } = this.props;
const { renderActivePanelOnly, showPanelHeader = true } = this.props;
const { stack } = this.state;
const active = index === 0;

// With renderActivePanelOnly={false} we would keep all the CSSTransitions rendered,
// therefore they would not trigger the "enter" transition event as they were entered.
// To force the enter event, we want to change the key, but stack.length is not enough
// and a single panel should not rerender as long as it's hidden.
// This key contains two parts: first one, stack.length - index is constant (and unique) for each panel,
// second one, active changes only when the panel becomes or stops being active.
const layer = stack.length - index;
const key = `${layer}-${active}`;
const key = renderActivePanelOnly ? stack.length : layer;

return (
<CSSTransition classNames={Classes.PANEL_STACK} key={key} timeout={400}>
<PanelView
Expand All @@ -161,7 +170,7 @@ export class PanelStack extends AbstractPureComponent2<IPanelStackProps, IPanelS
if (this.props.stack == null) {
this.setState(state => ({
direction: "pop",
stack: state.stack.filter(p => p !== panel),
stack: state.stack.slice(1),
}));
}
};
Expand Down
14 changes: 10 additions & 4 deletions packages/core/test/panel-stack/panelStackTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ describe("<PanelStack>", () => {
});

it("renders only one panel by default", () => {
const stack = [{ component: TestPanel, title: "Panel A" }, { component: TestPanel, title: "Panel B" }];
const stack = [
{ component: TestPanel, title: "Panel A" },
{ component: TestPanel, title: "Panel B" },
];
panelStackWrapper = renderPanelStack({ stack });

const panelHeaders = panelStackWrapper.findClass(Classes.HEADING);
Expand All @@ -244,9 +247,12 @@ describe("<PanelStack>", () => {
assert.equal(panelHeaders.at(0).text(), stack[1].title);
});

it("renders all panels with renderCurrentPanelOnly disabled", () => {
const stack = [{ component: TestPanel, title: "Panel A" }, { component: TestPanel, title: "Panel B" }];
panelStackWrapper = renderPanelStack({ renderCurrentPanelOnly: false, stack });
it("renders all panels with renderActivePanelOnly disabled", () => {
const stack = [
{ component: TestPanel, title: "Panel A" },
{ component: TestPanel, title: "Panel B" },
];
panelStackWrapper = renderPanelStack({ renderActivePanelOnly: false, stack });

const panelHeaders = panelStackWrapper.findClass(Classes.HEADING);
assert.exists(panelHeaders);
Expand Down
21 changes: 17 additions & 4 deletions packages/docs-app/src/examples/core-examples/panelStackExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import * as React from "react";

import { Button, H5, Intent, IPanel, IPanelProps, PanelStack, Switch, UL } from "@blueprintjs/core";
import { Button, H5, Intent, IPanel, IPanelProps, NumericInput, PanelStack, Switch, UL } from "@blueprintjs/core";
import { Example, handleBooleanChange, IExampleProps } from "@blueprintjs/docs-theme";

export interface IPanelStackExampleState {
Expand Down Expand Up @@ -67,7 +67,7 @@ export class PanelStackExample extends React.PureComponent<IExampleProps, IPanel
initialPanel={this.state.currentPanelStack[0]}
onOpen={this.addToPanelStack}
onClose={this.removeFromPanelStack}
renderCurrentPanelOnly={this.state.activePanelOnly}
renderActivePanelOnly={this.state.activePanelOnly}
showPanelHeader={this.state.showHeader}
/>
</Example>
Expand All @@ -85,16 +85,25 @@ export class PanelStackExample extends React.PureComponent<IExampleProps, IPanel
};
}

interface IPanelExampleProps {
interface IPanelExampleProps extends IPanelProps {
panelNumber: number;
}

interface IPanelExampleState {
counter: number;
}

// tslint:disable-next-line:max-classes-per-file
class PanelExample extends React.PureComponent<IPanelProps & IPanelExampleProps> {
class PanelExample extends React.PureComponent<IPanelExampleProps> {
public state: IPanelExampleState = {
counter: 0,
};

public render() {
return (
<div className="docs-panel-stack-contents-example">
<Button intent={Intent.PRIMARY} onClick={this.openNewPanel} text="Open new panel" />
<NumericInput value={this.state.counter} stepSize={1} onValueChange={this.updateCounter} />
</div>
);
}
Expand All @@ -107,4 +116,8 @@ class PanelExample extends React.PureComponent<IPanelProps & IPanelExampleProps>
title: `Panel ${panelNumber}`,
});
};

private updateCounter = (counter: number) => {
this.setState({ counter });
};
}
3 changes: 2 additions & 1 deletion packages/docs-app/src/styles/_examples.scss
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,9 @@
.docs-panel-stack-contents-example {
display: flex;
flex: 1 0 auto;
flex-direction: column;
align-items: center;
justify-content: center;
justify-content: space-around;
padding: 10px;
}

Expand Down