Skip to content

Commit

Permalink
[Core] Add panel stack component (#2642)
Browse files Browse the repository at this point in the history
* Added panel stack

* First test integrated

* Tests added

* Transitions adjusted

* Updated readme

* Made lower case

* Dark theme added

* Linting fixed

* Review items fixed, transitions reverted

* Removed extra panelClassName

* _panel-stack.scss

* minor docs

* updated tests

* Tests adjusted and fixed

* [PanelStack] refactors & fix transitions (#2717)

* fix the transitions!

* example refactor: move stack to options bar

* lint example

* refactor header styles to use fewer classes

* Classes.PANEL_STACK

* fix tests

* style fixes, simpler transition

* no stack limit

* back button nowrap

* Adjusted transition to include opacity and -50% left movement

* Removed useless type checking

* Fixed opacity

* Opacity

* Opacity flipped

* Removed 0

* Reversed opacities

* One more attempt

* [PanelStack] merge some files (#2719)

* merge PanelHeader into PanelView

* panelProps file with more docs

* panelProps.ts

* export panelProps, more docs

* example key

* docs

* safeInvoke

* changed easing settings

* fix copyrights

* onClose/Open param names

* [PanelStack] use IPanel in openPanel() public API (#2738)

* openPanel accepts IPanel instead of three args

* PanelView onClose/Open props skips getPanelProps() step

* fix tests

* remove IPanelPptions

* [PanelStack] documentation (#2737)

* PanelStack docs

* more docs refactors

* adjust docs-modifiers margins

* remove "pop the stack"
  • Loading branch information
kadhirvelm authored and giladgray committed Jul 30, 2018
1 parent 5922413 commit 6581f83
Show file tree
Hide file tree
Showing 16 changed files with 658 additions and 2 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ export const OVERLAY_INLINE = `${OVERLAY}-inline`;
export const OVERLAY_OPEN = `${OVERLAY}-open`;
export const OVERLAY_SCROLL_CONTAINER = `${OVERLAY}-scroll-container`;

export const PANEL_STACK = `${NS}-panel-stack`;
export const PANEL_STACK_HEADER = `${PANEL_STACK}-header`;
export const PANEL_STACK_HEADER_BACK = `${PANEL_STACK}-header-back`;
export const PANEL_STACK_VIEW = `${PANEL_STACK}-view`;

export const POPOVER = `${NS}-popover`;
export const POPOVER_ARROW = `${POPOVER}-arrow`;
export const POPOVER_BACKDROP = `${POPOVER}-backdrop`;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@import "non-ideal-state/non-ideal-state";
@import "overflow-list/overflow-list";
@import "overlay/overlay";
@import "panel-stack/panel-stack";
@import "popover/popover";
@import "portal/portal";
@import "progress-bar/progress-bar";
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@page navbar
@page non-ideal-state
@page overflow-list
@page panel-stack
@page progress-bar
@page skeleton
@page spinner
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export * from "./non-ideal-state/nonIdealState";
export * from "./overflow-list/overflowList";
export * from "./overlay/overlay";
export * from "./text/text";
export * from "./panel-stack/panelProps";
export * from "./panel-stack/panelStack";
export * from "./popover/popover";
export * from "./popover/popoverSharedProps";
export * from "./portal/portal";
Expand Down
97 changes: 97 additions & 0 deletions packages/core/src/components/panel-stack/_panel-stack.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2018 Palantir Technologies, Inc. All rights reserved.
// Licensed under the terms of the LICENSE file distributed with this project.

@import "../../common/variables";
@import "~@blueprintjs/core/src/common/react-transition";

.#{$ns}-panel-stack {
position: relative;
overflow: hidden;
}

.#{$ns}-panel-stack-header {
display: flex;
flex-shrink: 0;
align-items: center;
box-shadow: 0 1px $pt-divider-black;
height: $pt-grid-size * 3;

.#{$ns}-dark & {
box-shadow: 0 1px $pt-dark-divider-white;
}

// two span children act as spacers to keep the title centered.
> span {
display: flex;
flex: 1;
align-items: stretch;
}

.#{$ns}-heading {
margin: 0 ($pt-grid-size / 2);
}
}

.#{$ns}-button.#{$ns}-panel-stack-header-back {
margin-left: $pt-grid-size / 2;
padding-left: 0;
white-space: nowrap;

.#{$ns}-icon {
// reduce margins around icon so it fits better in tight header
margin: 0 2px;
}
}

.#{$ns}-panel-stack-view {
@include position-all(absolute, 0);
display: flex;
flex-direction: column;

// border between panels, visible during transition
margin-right: -1px;
border-right: 1px solid $pt-divider-black;

background-color: $white;
overflow-y: auto;

.#{$ns}-dark & {
background-color: $dark-gray4;
}
}

// PUSH transition: enter from right (100%), existing panel moves off left.
.#{$ns}-panel-stack-push {
@include react-transition-phase(
"#{$ns}-panel-stack",
"enter",
(transform: translateX(100%) translate(0%), opacity: 0 1),
$easing: ease,
$duration: $pt-transition-duration * 4
);
@include react-transition-phase(
"#{$ns}-panel-stack",
"exit",
(transform: translateX(-50%) translate(0%), opacity: 0 1),
$easing: ease,
$duration: $pt-transition-duration * 4
);
}

// POP transition: enter from left (-50%), existing panel moves off right.
.#{$ns}-panel-stack-pop {
@include react-transition-phase(
"#{$ns}-panel-stack",
"enter",
(transform: translateX(-50%) translate(0%), opacity: 0 1),
$easing: ease,
$duration: $pt-transition-duration * 4
);
@include react-transition-phase(
"#{$ns}-panel-stack",
"exit",
(transform: translateX(100%) translate(0%), opacity: 0 1),
$easing: ease,
$duration: $pt-transition-duration * 4
);
}
61 changes: 61 additions & 0 deletions packages/core/src/components/panel-stack/panel-stack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
@# Panel stack

`PanelStack` manages a stack of panels and displays only the topmost panel.

Each panel appears with a header containing a "back" button to return to the
previous panel. The bottom-most `initialPanel` cannot be closed or removed from
the stack. Panels use
[`CSSTransition`](http://reactcommunity.org/react-transition-group/css-transition)
for seamless transitions.


@reactExample PanelStackExample

@## Panels

Panels are supplied as `IPanel` objects like `{ component, props, title }`,
where `component` and `props` are used to render the panel element and `title`
will appear in the header and back button. This breakdown allows the component
to avoid cloning elements. Note that each panel is only mounted when it is atop
the stack and is unmounted when it is closed or when a panel opens above it.

`PanelStack` injects its own `IPanelProps` into each panel (in addition to the
`props` defined alongside the `component`), providing methods to imperatively
close the current panel or open a new one on top of it.

```tsx
import { Button, IPanelProps, PanelStack } from "@blueprintjs/core";

class MyPanel extends React.Component<IPanelProps> {
public render() {
return <Button onClick={this.openSettingsPanel} text="Settings" />
}

private openSettingsPanel() {
// openPanel (and closePanel) are injected by PanelStack
this.props.openPanel({
component: SettingsPanel, // <- class or stateless function type
props: { enabled: true }, // <- SettingsPanel props without IPanelProps
title: "Settings", // <- appears in header and back button
});
}
}

class SettingsPanel extends React.Component<IPanelProps & { enabled: boolean }> {
// ...
}

<PanelStack initialPanel={{ component: MyPanel, title: "Home" }} />
```

@interface IPanel

@interface IPanelProps

@## Props

The panel stack cannot be controlled but `onClose` and `onOpen` callbacks are
available to listen for changes.

@interface IPanelStackProps

56 changes: 56 additions & 0 deletions packages/core/src/components/panel-stack/panelProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import * as React from "react";

/**
* An object describing a panel in a `PanelStack`.
*/
export interface IPanel<P = {}> {
/**
* The component type to render for this panel. This must be a reference to
* the component class or SFC, _not_ a JSX element, so it can be re-created
* dynamically when needed.
*/
component: React.ComponentType<P & IPanelProps>;

/**
* The props passed to the component type when it is rendered. The methods
* in `IPanelProps` will be injected by `PanelStack`.
*/
props?: P;

/**
* The title to be displayed above this panel. It is also used as the text
* of the back button for any panel opened by this panel.
*/
title?: React.ReactNode;
}

/**
* Include this interface in your panel component's props type to access these
* two functions which are injected by `PanelStack`.
*
* ```tsx
* import { IPanelProps } from "@blueprintjs/core";
* export class SettingsPanel extends React.Component<IPanelProps & ISettingsPanelProps> {...}
* ```
*/
export interface IPanelProps {
/**
* Call this method to programatically close this panel. If this is the only
* panel on the stack then this method will do nothing.
*
* Remember that the panel header always contains a "back" button that
* closes this panel on click (unless there is only one panel on the stack).
*/
closePanel(): void;

/**
* Call this method to open a new panel on the top of the stack.
*/
openPanel<P>(panel: IPanel<P>): void;
}
102 changes: 102 additions & 0 deletions packages/core/src/components/panel-stack/panelStack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import classNames from "classnames";
import * as React from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";

import * as Classes from "../../common/classes";
import { IProps } from "../../common/props";
import { safeInvoke } from "../../common/utils";
import { IPanel } from "./panelProps";
import { PanelView } from "./panelView";

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.
*/
initialPanel: IPanel;

/**
* Callback invoked when the user presses the back button or a panel invokes
* the `closePanel()` injected prop method.
*/
onClose?: (removedPanel: IPanel) => void;

/**
* Callback invoked when a panel invokes the `openPanel(panel)` injected
* prop method.
*/
onOpen?: (addedPanel: IPanel) => void;
}

export interface IPanelStackState {
/** Whether the stack is currently animating the push or pop of a panel. */
direction: "push" | "pop";

/** The current stack of panels. The first panel in the stack will be displayed. */
stack: IPanel[];
}

export class PanelStack extends React.PureComponent<IPanelStackProps, IPanelStackState> {
public state: IPanelStackState = {
direction: "push",
stack: [this.props.initialPanel],
};

public render() {
const classes = classNames(
Classes.PANEL_STACK,
`${Classes.PANEL_STACK}-${this.state.direction}`,
this.props.className,
);
return (
<TransitionGroup className={classes} component="div">
{this.renderCurrentPanel()}
</TransitionGroup>
);
}

private renderCurrentPanel() {
const { stack } = this.state;
if (stack.length === 0) {
return null;
}
const [activePanel, previousPanel] = stack;
return (
<CSSTransition classNames={Classes.PANEL_STACK} key={stack.length} timeout={400}>
<PanelView
onClose={this.handlePanelClose}
onOpen={this.handlePanelOpen}
panel={activePanel}
previousPanel={previousPanel}
/>
</CSSTransition>
);
}

private handlePanelClose = (panel: IPanel) => {
const { stack } = this.state;
// only remove this panel if it is at the top and not the only one.
if (stack[0] !== panel || stack.length <= 1) {
return;
}
safeInvoke(this.props.onClose, panel);
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],
}));
};
}
Loading

1 comment on commit 6581f83

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Core] Add panel stack component (#2642)

Preview: documentation | landing | table

Please sign in to comment.