Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Add panel stack component #2642

Merged
merged 32 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7b4d4d1
Added panel stack
Jul 3, 2018
f781b5a
First test integrated
Jul 3, 2018
5c106b5
Tests added
Jul 4, 2018
22ac407
Transitions adjusted
Jul 5, 2018
dce1189
Updated readme
Jul 5, 2018
f7c207a
Made lower case
Jul 5, 2018
e0f7728
Dark theme added
Jul 10, 2018
b637e94
Linting fixed
Jul 10, 2018
7da109d
Review items fixed, transitions reverted
Jul 23, 2018
a128368
Removed extra panelClassName
Jul 23, 2018
49e7845
Merge branch 'develop' of github.com:palantir/blueprint into km/panel…
Jul 23, 2018
7c6a914
_panel-stack.scss
Jul 23, 2018
8fbe610
minor docs
Jul 23, 2018
82bcdee
updated tests
Jul 24, 2018
11dff80
Merge branch 'km/panel-stack' of github.com:palantir/blueprint into k…
Jul 24, 2018
069c88d
Tests adjusted and fixed
Jul 24, 2018
0d006ad
[PanelStack] refactors & fix transitions (#2717)
giladgray Jul 24, 2018
eb7b00a
Merged
Jul 24, 2018
4faf1c6
Adjusted transition to include opacity and -50% left movement
Jul 24, 2018
a4eb098
Removed useless type checking
Jul 24, 2018
2a7a459
Fixed opacity
Jul 24, 2018
7f8f6e1
Opacity
Jul 24, 2018
223c310
Opacity flipped
Jul 24, 2018
634e211
Removed 0
Jul 24, 2018
0246e3f
Reversed opacities
Jul 24, 2018
b0e19c0
One more attempt
Jul 24, 2018
6088348
[PanelStack] merge some files (#2719)
giladgray Jul 24, 2018
0fb825d
changed easing settings
Jul 26, 2018
c6acec0
fix copyrights
Jul 30, 2018
fa37554
onClose/Open param names
Jul 30, 2018
9dac016
[PanelStack] use IPanel in openPanel() public API (#2738)
giladgray Jul 30, 2018
be7d617
[PanelStack] documentation (#2737)
giladgray Jul 30, 2018
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/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