From beefef26636430afcb670df3802f76617c4087d2 Mon Sep 17 00:00:00 2001 From: Acko Date: Tue, 26 Mar 2019 21:39:34 +0100 Subject: [PATCH] [Core] Add position prop to Drawer component, deprecate vertical prop (#3386) --- packages/core/src/common/classes.ts | 12 ++ packages/core/src/common/errors.ts | 4 + packages/core/src/common/position.ts | 24 +++ .../core/src/components/drawer/_drawer.scss | 107 ++++++++++- .../core/src/components/drawer/drawer.tsx | 41 ++++- packages/core/test/drawer/drawerTests.tsx | 172 +++++++++++++++++- .../examples/core-examples/drawerExample.tsx | 31 +++- 7 files changed, 375 insertions(+), 16 deletions(-) diff --git a/packages/core/src/common/classes.ts b/packages/core/src/common/classes.ts index a81a4de246..730aff545d 100644 --- a/packages/core/src/common/classes.ts +++ b/packages/core/src/common/classes.ts @@ -7,6 +7,7 @@ import { Alignment } from "./alignment"; import { Elevation } from "./elevation"; import { Intent } from "./intent"; +import { Position } from "./position"; const NS = process.env.BLUEPRINT_NAMESPACE || "bp3"; @@ -28,6 +29,10 @@ export const MULTILINE = `${NS}-multiline`; export const ROUND = `${NS}-round`; export const SMALL = `${NS}-small`; export const VERTICAL = `${NS}-vertical`; +export const POSITION_TOP = positionClass(Position.TOP); +export const POSITION_BOTTOM = positionClass(Position.BOTTOM); +export const POSITION_LEFT = positionClass(Position.LEFT); +export const POSITION_RIGHT = positionClass(Position.RIGHT); export const ELEVATION_0 = elevationClass(Elevation.ZERO); export const ELEVATION_1 = elevationClass(Elevation.ONE); @@ -304,3 +309,10 @@ export function intentClass(intent?: Intent) { } return `${NS}-intent-${intent.toLowerCase()}`; } + +export function positionClass(position: Position) { + if (position == null) { + return undefined; + } + return `${NS}-position-${position}`; +} diff --git a/packages/core/src/common/errors.ts b/packages/core/src/common/errors.ts index 0f59460be6..047df85e78 100644 --- a/packages/core/src/common/errors.ts +++ b/packages/core/src/common/errors.ts @@ -80,3 +80,7 @@ export const TOASTER_WARN_INLINE = ns + ` Toaster.create() ignores inline prop a export const DIALOG_WARN_NO_HEADER_ICON = ns + ` iconName is ignored if title is omitted.`; export const DIALOG_WARN_NO_HEADER_CLOSE_BUTTON = ns + ` isCloseButtonShown prop is ignored if title is omitted.`; + +export const DRAWER_VERTICAL_IS_IGNORED = ns + ` vertical is ignored if position is defined`; +export const DRAWER_ANGLE_POSITIONS_ARE_CASTED = + ns + ` all angle positions are casted into pure position (TOP, BOTTOM, LEFT or RIGHT)`; diff --git a/packages/core/src/common/position.ts b/packages/core/src/common/position.ts index 2178efa96c..8070c11bc5 100644 --- a/packages/core/src/common/position.ts +++ b/packages/core/src/common/position.ts @@ -43,3 +43,27 @@ export function isPositionVertical(position: Position) { position === Position.RIGHT_BOTTOM ); } + +export function getPositionIgnoreAngles(position: Position) { + if ( + position === Position.TOP || + position === Position.TOP_LEFT || + position === Position.TOP_RIGHT + ) { + return Position.TOP; + } else if ( + position === Position.BOTTOM || + position === Position.BOTTOM_LEFT || + position === Position.BOTTOM_RIGHT + ) { + return Position.BOTTOM; + } else if ( + position === Position.LEFT || + position === Position.LEFT_TOP || + position === Position.LEFT_BOTTOM + ) { + return Position.LEFT; + } else { + return Position.RIGHT; + } +} diff --git a/packages/core/src/components/drawer/_drawer.scss b/packages/core/src/components/drawer/_drawer.scss index 627535e006..90745eeac4 100644 --- a/packages/core/src/components/drawer/_drawer.scss +++ b/packages/core/src/components/drawer/_drawer.scss @@ -23,11 +23,11 @@ $drawer-default-size: 50%; outline: 0; } - &:not(.#{$ns}-vertical) { + &.#{$ns}-position-top { @include react-transition-phase( "#{$ns}-overlay", "enter", - (transform: (translateX(100%), translateX(0))), + (transform: (translateY(-100%), translateY(0))), $pt-transition-duration * 2, $pt-transition-ease, $before: "&" @@ -35,18 +35,18 @@ $drawer-default-size: 50%; @include react-transition-phase( "#{$ns}-overlay", "exit", - (transform: (translateX(100%), translateX(0))), + (transform: (translateY(-100%), translateY(0))), $pt-transition-duration, $before: "&" ); top: 0; right: 0; - bottom: 0; - width: $drawer-default-size; + left: 0; + height: $drawer-default-size; } - &.#{$ns}-vertical { + &.#{$ns}-position-bottom { @include react-transition-phase( "#{$ns}-overlay", "enter", @@ -69,6 +69,101 @@ $drawer-default-size: 50%; height: $drawer-default-size; } + &.#{$ns}-position-left { + @include react-transition-phase( + "#{$ns}-overlay", + "enter", + (transform: (translateX(-100%), translateX(0))), + $pt-transition-duration * 2, + $pt-transition-ease, + $before: "&" + ); + @include react-transition-phase( + "#{$ns}-overlay", + "exit", + (transform: (translateX(-100%), translateX(0))), + $pt-transition-duration, + $before: "&" + ); + + top: 0; + bottom: 0; + left: 0; + width: $drawer-default-size; + } + + &.#{$ns}-position-right { + @include react-transition-phase( + "#{$ns}-overlay", + "enter", + (transform: (translateX(100%), translateX(0))), + $pt-transition-duration * 2, + $pt-transition-ease, + $before: "&" + ); + @include react-transition-phase( + "#{$ns}-overlay", + "exit", + (transform: (translateX(100%), translateX(0))), + $pt-transition-duration, + $before: "&" + ); + + top: 0; + right: 0; + bottom: 0; + width: $drawer-default-size; + } + + &:not(.#{$ns}-position-top):not(.#{$ns}-position-bottom):not(.#{$ns}-position-left):not( + .#{$ns}-position-right) { + &:not(.#{$ns}-vertical) { + @include react-transition-phase( + "#{$ns}-overlay", + "enter", + (transform: (translateX(100%), translateX(0))), + $pt-transition-duration * 2, + $pt-transition-ease, + $before: "&" + ); + @include react-transition-phase( + "#{$ns}-overlay", + "exit", + (transform: (translateX(100%), translateX(0))), + $pt-transition-duration, + $before: "&" + ); + + top: 0; + right: 0; + bottom: 0; + width: $drawer-default-size; + } + + &.#{$ns}-vertical { + @include react-transition-phase( + "#{$ns}-overlay", + "enter", + (transform: (translateY(100%), translateY(0))), + $pt-transition-duration * 2, + $pt-transition-ease, + $before: "&" + ); + @include react-transition-phase( + "#{$ns}-overlay", + "exit", + (transform: (translateY(100%), translateY(0))), + $pt-transition-duration, + $before: "&" + ); + + right: 0; + bottom: 0; + left: 0; + height: $drawer-default-size; + } + } + &.#{$ns}-dark, .#{$ns}-dark & { box-shadow: $pt-dark-dialog-box-shadow; diff --git a/packages/core/src/components/drawer/drawer.tsx b/packages/core/src/components/drawer/drawer.tsx index 0593be97b3..1a1664ad0d 100644 --- a/packages/core/src/components/drawer/drawer.tsx +++ b/packages/core/src/components/drawer/drawer.tsx @@ -10,6 +10,7 @@ import * as React from "react"; import { AbstractPureComponent } from "../../common/abstractPureComponent"; import * as Classes from "../../common/classes"; import * as Errors from "../../common/errors"; +import { getPositionIgnoreAngles, isPositionHorizontal, Position } from "../../common/position"; import { DISPLAYNAME_PREFIX, IProps, MaybeElement } from "../../common/props"; import { Button } from "../button/buttons"; import { H4 } from "../html/html"; @@ -37,6 +38,13 @@ export interface IDrawerProps extends IOverlayableProps, IBackdropProps, IProps */ isOpen: boolean; + /** + * Position of a drawer. All angled positions will be casted into pure positions + * (TOP, BOTTOM, LEFT or RIGHT). + * @default Position.RIGHT + */ + position?: Position; + /** * CSS size of the drawer. This sets `width` if `vertical={false}` (default) * and `height` otherwise. @@ -70,7 +78,9 @@ export interface IDrawerProps extends IOverlayableProps, IBackdropProps, IProps /** * Whether the drawer should appear with vertical styling. + * It will be ignored if `position` prop is set * @default false + * @deprecated use `position` instead */ vertical?: boolean; } @@ -80,6 +90,7 @@ export class Drawer extends AbstractPureComponent { public static defaultProps: IDrawerProps = { canOutsideClickClose: true, isOpen: false, + position: null, style: {}, vertical: false, }; @@ -89,9 +100,25 @@ export class Drawer extends AbstractPureComponent { public static readonly SIZE_LARGE = "90%"; public render() { - const { size, style, vertical } = this.props; - const classes = classNames(Classes.DRAWER, { [Classes.VERTICAL]: vertical }, this.props.className); - const styleProp = size == null ? style : { ...style, [vertical ? "height" : "width"]: size }; + const { size, style, position, vertical } = this.props; + const realPosition = position ? getPositionIgnoreAngles(position) : null; + + const classes = classNames( + Classes.DRAWER, + { + [Classes.VERTICAL]: !realPosition && vertical, + [realPosition ? Classes.positionClass(realPosition) : ""]: true, + }, + this.props.className, + ); + + const styleProp = + size == null + ? style + : { + ...style, + [(realPosition ? isPositionHorizontal(realPosition) : vertical) ? "height" : "width"]: size, + }; return (
@@ -111,6 +138,14 @@ export class Drawer extends AbstractPureComponent { console.warn(Errors.DIALOG_WARN_NO_HEADER_CLOSE_BUTTON); } } + if (props.position != null) { + if (props.vertical) { + console.warn(Errors.DRAWER_VERTICAL_IS_IGNORED); + } + if (props.position !== getPositionIgnoreAngles(props.position)) { + console.warn(Errors.DRAWER_ANGLE_POSITIONS_ARE_CASTED); + } + } } private maybeRenderCloseButton() { diff --git a/packages/core/test/drawer/drawerTests.tsx b/packages/core/test/drawer/drawerTests.tsx index 86f64836f7..54b46e515d 100644 --- a/packages/core/test/drawer/drawerTests.tsx +++ b/packages/core/test/drawer/drawerTests.tsx @@ -7,10 +7,11 @@ import { assert } from "chai"; import { mount } from "enzyme"; import * as React from "react"; -import { spy } from "sinon"; +import { spy, stub } from "sinon"; +import { DRAWER_ANGLE_POSITIONS_ARE_CASTED, DRAWER_VERTICAL_IS_IGNORED } from "../../src/common/errors"; import * as Keys from "../../src/common/keys"; -import { Button, Classes, Drawer } from "../../src/index"; +import { Button, Classes, Drawer, Position } from "../../src/index"; describe("", () => { it("renders its content correctly", () => { @@ -24,6 +25,173 @@ describe("", () => { }); }); + describe("position", () => { + it("casts angle positions into pure positions (with console warning)", () => { + const warnSpy = stub(console, "warn"); + + const drawerTop = mount( + + {createDrawerContents()} + , + ); + const drawerLeft = mount( + + {createDrawerContents()} + , + ); + const drawerTopRight = mount( + + {createDrawerContents()} + , + ); + const drawerTopLeft = mount( + + {createDrawerContents()} + , + ); + const drawerLeftTop = mount( + + {createDrawerContents()} + , + ); + + assert.isTrue( + drawerTop.find(`.${Classes.DRAWER}`).equals(drawerTopRight.find(`.${Classes.DRAWER}`).getElement()), + ); + assert.isTrue( + drawerTop.find(`.${Classes.DRAWER}`).equals(drawerTopLeft.find(`.${Classes.DRAWER}`).getElement()), + ); + assert.isFalse( + drawerTop.find(`.${Classes.DRAWER}`).equals(drawerLeftTop.find(`.${Classes.DRAWER}`).getElement()), + ); + assert.isTrue( + drawerLeft.find(`.${Classes.DRAWER}`).equals(drawerLeftTop.find(`.${Classes.DRAWER}`).getElement()), + ); + + assert.isTrue(warnSpy.alwaysCalledWith(DRAWER_ANGLE_POSITIONS_ARE_CASTED)); + warnSpy.restore(); + }); + + it("overrides vertical (with console warning)", () => { + const warnSpy = stub(console, "warn"); + + const drawerLeft = mount( + + {createDrawerContents()} + , + ); + + // vertical size becomes height (opposite test) + assert.equal(drawerLeft.find(`.${Classes.DRAWER}`).prop("style").width, 100); + // vertical adds class (opposite test) + assert.isFalse(drawerLeft.find(`.${Classes.VERTICAL}`).exists()); + + assert.isTrue(warnSpy.alwaysCalledWith(DRAWER_VERTICAL_IS_IGNORED)); + warnSpy.restore(); + }); + + describe("RIGHT", () => { + it("position right is default", () => { + const drawerDefault = mount( + + {createDrawerContents()} + , + ); + const drawerRight = mount( + + {createDrawerContents()} + , + ); + assert.equal( + drawerDefault.find(`.${Classes.DRAWER}`).prop("style").width, + drawerRight.find(`.${Classes.DRAWER}`).prop("style").width, + ); + assert.equal( + drawerDefault.find(`.${Classes.DRAWER}`).prop("style").height, + drawerRight.find(`.${Classes.DRAWER}`).prop("style").height, + ); + }); + + it("position right, size becomes width", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.equal(drawer.find(`.${Classes.DRAWER}`).prop("style").width, 100); + }); + + it("position right, adds appropriate classes (default behavior)", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.isTrue(drawer.find(`.${Classes.POSITION_RIGHT}`).exists()); + }); + }); + + describe("TOP", () => { + it("position top, size becomes height", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.equal(drawer.find(`.${Classes.DRAWER}`).prop("style").height, 100); + }); + + it("position top, adds appropriate classes (vertical, reverse)", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.isTrue(drawer.find(`.${Classes.POSITION_TOP}`).exists()); + }); + }); + + describe("BOTTOM", () => { + it("position bottom, size becomes height", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.equal(drawer.find(`.${Classes.DRAWER}`).prop("style").height, 100); + }); + + it("position bottom, adds appropriate classes (vertical)", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.isTrue(drawer.find(`.${Classes.POSITION_BOTTOM}`).exists()); + }); + }); + + describe("LEFT", () => { + it("position left, size becomes width", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.equal(drawer.find(`.${Classes.DRAWER}`).prop("style").width, 100); + }); + + it("position left, adds appropriate classes (reverse)", () => { + const drawer = mount( + + {createDrawerContents()} + , + ); + assert.isTrue(drawer.find(`.${Classes.POSITION_LEFT}`).exists()); + }); + }); + }); + it("size becomes width", () => { const drawer = mount( diff --git a/packages/docs-app/src/examples/core-examples/drawerExample.tsx b/packages/docs-app/src/examples/core-examples/drawerExample.tsx index 5ce0f56e07..1cf3010c2c 100644 --- a/packages/docs-app/src/examples/core-examples/drawerExample.tsx +++ b/packages/docs-app/src/examples/core-examples/drawerExample.tsx @@ -6,7 +6,19 @@ import * as React from "react"; -import { Button, Classes, Code, Divider, Drawer, H5, HTMLSelect, IOptionProps, Label, Switch } from "@blueprintjs/core"; +import { + Button, + Classes, + Code, + Divider, + Drawer, + H5, + HTMLSelect, + IOptionProps, + Label, + Position, + Switch, +} from "@blueprintjs/core"; import { Example, handleBooleanChange, handleStringChange, IExampleProps } from "@blueprintjs/docs-theme"; import { IBlueprintExampleData } from "../../tags/reactExamples"; @@ -17,9 +29,9 @@ export interface IDrawerExampleState { enforceFocus: boolean; hasBackdrop: boolean; isOpen: boolean; + position?: Position; size: string; usePortal: boolean; - vertical: boolean; } export class DrawerExample extends React.PureComponent, IDrawerExampleState> { public state: IDrawerExampleState = { @@ -29,9 +41,9 @@ export class DrawerExample extends React.PureComponent this.setState({ autoFocus })); @@ -39,8 +51,8 @@ export class DrawerExample extends React.PureComponent this.setState({ enforceFocus })); private handleEscapeKeyChange = handleBooleanChange(canEscapeKeyClose => this.setState({ canEscapeKeyClose })); private handleUsePortalChange = handleBooleanChange(usePortal => this.setState({ usePortal })); + private handlePositionChange = handleStringChange((position: Position) => this.setState({ position })); private handleOutsideClickChange = handleBooleanChange(val => this.setState({ canOutsideClickClose: val })); - private handleVerticalChange = handleBooleanChange(vertical => this.setState({ vertical })); private handleSizeChange = handleStringChange(size => this.setState({ size })); public render() { @@ -95,11 +107,18 @@ export class DrawerExample extends React.PureComponent
Props
+ - @@ -129,3 +148,5 @@ const SIZES: Array = [ "72%", "560px", ]; + +const VALID_POSITIONS: Position[] = [Position.TOP, Position.RIGHT, Position.BOTTOM, Position.LEFT];