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
4 changes: 4 additions & 0 deletions packages/core/src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ export const NUMERIC_INPUT_STEP_SIZE_NON_POSITIVE =
ns + ` <NumericInput> requires stepSize to be strictly greater than zero.`;
export const NUMERIC_INPUT_STEP_SIZE_NULL = ns + ` <NumericInput> requires stepSize to be defined.`;

export const PANEL_STACK_INITIAL_PANEL_STACK_MUTEX =
ns + ` <PanelStack> requires exactly one of initialPanel and stack prop`;
export const PANEL_STACK_REQUIRES_PANEL = ns + ` <PanelStack> requires at least one panel in the stack`;

export const OVERFLOW_LIST_OBSERVE_PARENTS_CHANGED =
ns + ` <OverflowList> does not support changing observeParents after mounting.`;

Expand Down
57 changes: 46 additions & 11 deletions packages/core/src/components/panel-stack/panelStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import classNames from "classnames";
import * as React from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";

import { AbstractPureComponent } from "../../common/abstractPureComponent";
import * as Classes from "../../common/classes";
import * as Errors from "../../common/errors";
import { IProps } from "../../common/props";
import { safeInvoke } from "../../common/utils";
import { IPanel } from "./panelProps";
Expand All @@ -28,8 +30,10 @@ export interface IPanelStackProps extends IProps {
/**
* The initial panel to show on mount. This panel cannot be removed from the
* stack and will appear when the stack is empty.
* This prop is only used in uncontrolled mode and is thus mutually
* exclusive with the `stack` prop.
*/
initialPanel: IPanel<any>;
initialPanel?: IPanel<any>;

/**
* Callback invoked when the user presses the back button or a panel invokes
Expand All @@ -48,6 +52,12 @@ export interface IPanelStackProps extends IProps {
* @default true
*/
showPanelHeader?: boolean;

/**
* The full stack of panels in controlled mode. The last panel in the stack
* will be displayed.
*/
stack?: Array<IPanel<any>>;
}

export interface IPanelStackState {
Expand All @@ -58,12 +68,21 @@ export interface IPanelStackState {
stack: IPanel[];
}

export class PanelStack extends React.PureComponent<IPanelStackProps, IPanelStackState> {
export class PanelStack extends AbstractPureComponent<IPanelStackProps, IPanelStackState> {
public state: IPanelStackState = {
direction: "push",
stack: [this.props.initialPanel],
stack: this.props.stack != null ? this.props.stack.slice().reverse() : [this.props.initialPanel],
};

public componentWillReceiveProps(nextProps: IPanelStackProps) {
if (this.props.stack !== nextProps.stack && this.props.stack != null && nextProps.stack != null) {
this.setState({
direction: this.props.stack.length - nextProps.stack.length < 0 ? "push" : "pop",
stack: nextProps.stack.slice().reverse(),
});
}
}

public render() {
const classes = classNames(
Classes.PANEL_STACK,
Expand All @@ -77,6 +96,18 @@ export class PanelStack extends React.PureComponent<IPanelStackProps, IPanelStac
);
}

protected validateProps(props: IPanelStackProps) {
if (
(props.initialPanel == null && props.stack == null) ||
(props.initialPanel != null && props.stack != null)
) {
throw new Error(Errors.PANEL_STACK_INITIAL_PANEL_STACK_MUTEX);
}
if (props.stack != null && props.stack.length === 0) {
throw new Error(Errors.PANEL_STACK_REQUIRES_PANEL);
}
}

private renderCurrentPanel() {
const { showPanelHeader = true } = this.props;
const { stack } = this.state;
Expand Down Expand Up @@ -104,17 +135,21 @@ export class PanelStack extends React.PureComponent<IPanelStackProps, IPanelStac
return;
}
safeInvoke(this.props.onClose, panel);
this.setState(state => ({
direction: "pop",
stack: state.stack.filter(p => p !== panel),
}));
if (this.props.stack == null) {
this.setState(state => ({
direction: "pop",
stack: state.stack.filter(p => p !== panel),
}));
}
};

private handlePanelOpen = (panel: IPanel) => {
safeInvoke(this.props.onOpen, panel);
this.setState(state => ({
direction: "push",
stack: [panel, ...state.stack],
}));
if (this.props.stack == null) {
this.setState(state => ({
direction: "push",
stack: [panel, ...state.stack],
}));
}
};
}
56 changes: 56 additions & 0 deletions packages/core/test/panel-stack/panelStackTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,62 @@ describe("<PanelStack>", () => {
assert.equal(backButtonWithTitle.text(), "chevron-left");
});

it("can render a panel stack in controlled mode", () => {
const stack = [initialPanel];
panelStackWrapper = renderPanelStack({ stack });
assert.exists(panelStackWrapper);

const newPanelButton = panelStackWrapper.find("#new-panel-button");
assert.exists(newPanelButton);
newPanelButton.simulate("click");

// Expect the same panel as before since onOpen is not handled
const newPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
assert.exists(newPanelHeader);
assert.equal(newPanelHeader.at(0).text(), "Test Title");
});

it("can open a panel in controlled mode", () => {
let stack = [initialPanel];
panelStackWrapper = renderPanelStack({ onOpen: panel => (stack = [...stack, panel]), stack });
assert.exists(panelStackWrapper);

const newPanelButton = panelStackWrapper.find("#new-panel-button");
assert.exists(newPanelButton);
newPanelButton.simulate("click");
panelStackWrapper.setProps({ stack });

const newPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
assert.exists(newPanelHeader);
assert.equal(newPanelHeader.at(0).text(), "New Panel 1");
});

it("can render a panel stack with multiple initial panels and close one", () => {
let stack: Array<IPanel<any>> = [initialPanel, { component: TestPanel, title: "New Panel 1" }];
panelStackWrapper = renderPanelStack({
onClose: () => {
const newStack = stack.slice();
newStack.pop();
stack = newStack;
},
stack,
});
assert.exists(panelStackWrapper);

const panelHeader = panelStackWrapper.findClass(Classes.HEADING);
assert.exists(panelHeader);
assert.equal(panelHeader.at(0).text(), "New Panel 1");

const backButton = panelStackWrapper.findClass(Classes.PANEL_STACK_HEADER_BACK);
assert.exists(backButton);
backButton.simulate("click");
panelStackWrapper.setProps({ stack });

const firstPanelHeader = panelStackWrapper.findClass(Classes.HEADING);
assert.exists(firstPanelHeader);
assert.equal(firstPanelHeader.at(0).text(), "Test Title");
});

interface IPanelStackWrapper extends ReactWrapper<IPanelStackProps, any> {
findClass(className: string): ReactWrapper<React.HTMLAttributes<HTMLElement>, any>;
}
Expand Down