1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Add support for resizing Drawers (#4440)

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Sebastian Malton 2021-11-30 09:09:19 -05:00 committed by GitHub
parent df46dd9ef6
commit 7ce4a87ac8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 49 deletions

View File

@ -21,8 +21,6 @@
.Drawer { .Drawer {
--size: 50%;
--full-size: 75%;
--spacing: #{$padding * 3}; --spacing: #{$padding * 3};
--icon-focus-color: white; --icon-focus-color: white;
@ -52,6 +50,7 @@
&.right { &.right {
top: 0; top: 0;
width: var(--size); width: var(--size);
max-width: 95vw; /* Prevent drawer to be bigger than viewport */
} }
&.top, &.top,
@ -108,10 +107,4 @@
border-bottom: 1px solid var(--borderFaintColor); border-bottom: 1px solid var(--borderFaintColor);
} }
} }
@include media("<=900px") {
&.left, &.right {
width: var(--full-size)
}
}
} }

View File

@ -24,19 +24,28 @@ import "./drawer.scss";
import React from "react"; import React from "react";
import { clipboard } from "electron"; import { clipboard } from "electron";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { cssNames, noop } from "../../utils"; import { createStorage, cssNames, noop } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Animate, AnimateName } from "../animate"; import { Animate, AnimateName } from "../animate";
import { history } from "../../navigation"; import { history } from "../../navigation";
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
export type DrawerPosition = "top" | "left" | "right" | "bottom";
export interface DrawerProps { export interface DrawerProps {
open: boolean; open: boolean;
title: React.ReactNode; title: React.ReactNode;
/**
* The width or heigh (depending on `position`) of the Drawer.
*
* If not set then the Drawer will be resizable.
*/
size?: string; // e.g. 50%, 500px, etc. size?: string; // e.g. 50%, 500px, etc.
usePortal?: boolean; usePortal?: boolean;
className?: string | object; className?: string | object;
contentClass?: string | object; contentClass?: string | object;
position?: "top" | "left" | "right" | "bottom"; position?: DrawerPosition;
animation?: AnimateName; animation?: AnimateName;
onClose?: () => void; onClose?: () => void;
toolbar?: React.ReactNode; toolbar?: React.ReactNode;
@ -51,10 +60,22 @@ const defaultProps: Partial<DrawerProps> = {
interface State { interface State {
isCopied: boolean; isCopied: boolean;
width: number;
} }
export class Drawer extends React.Component<DrawerProps> { const resizingAnchorProps = new Map<DrawerPosition, [ResizeDirection, ResizeSide, ResizeGrowthDirection]>();
resizingAnchorProps.set("right", [ResizeDirection.HORIZONTAL, ResizeSide.LEADING, ResizeGrowthDirection.RIGHT_TO_LEFT]);
resizingAnchorProps.set("left", [ResizeDirection.HORIZONTAL, ResizeSide.TRAILING, ResizeGrowthDirection.LEFT_TO_RIGHT]);
resizingAnchorProps.set("top", [ResizeDirection.VERTICAL, ResizeSide.TRAILING, ResizeGrowthDirection.TOP_TO_BOTTOM]);
resizingAnchorProps.set("bottom", [ResizeDirection.VERTICAL, ResizeSide.LEADING, ResizeGrowthDirection.BOTTOM_TO_TOP]);
const defaultDrawerWidth = 725;
const drawerStorage = createStorage("drawer", {
width: defaultDrawerWidth,
});
export class Drawer extends React.Component<DrawerProps, State> {
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
private mouseDownTarget: HTMLElement; private mouseDownTarget: HTMLElement;
@ -66,8 +87,9 @@ export class Drawer extends React.Component<DrawerProps> {
this.restoreScrollPos(); this.restoreScrollPos();
}); });
public state: State = { public state = {
isCopied: false, isCopied: false,
width: drawerStorage.get().width,
}; };
componentDidMount() { componentDidMount() {
@ -86,6 +108,11 @@ export class Drawer extends React.Component<DrawerProps> {
window.removeEventListener("keydown", this.onEscapeKey); window.removeEventListener("keydown", this.onEscapeKey);
} }
resizeWidth = (width: number) => {
this.setState({ width });
drawerStorage.merge({ width });
};
fixUpTripleClick = (ev: MouseEvent) => { fixUpTripleClick = (ev: MouseEvent) => {
// detail: A count of consecutive clicks that happened in a short amount of time // detail: A count of consecutive clicks that happened in a short amount of time
if (ev.detail === 3) { if (ev.detail === 3) {
@ -157,33 +184,54 @@ export class Drawer extends React.Component<DrawerProps> {
}; };
render() { render() {
const { open, position, title, animation, children, toolbar, size, usePortal } = this.props; const { className, contentClass, animation, open, position, title, children, toolbar, size, usePortal } = this.props;
let { className, contentClass } = this.props; const { isCopied, width } = this.state;
const { isCopied } = this.state; const copyTooltip = isCopied ? "Copied!" : "Copy";
const copyTooltip = isCopied? "Copied!" : "Copy"; const copyIcon = isCopied ? "done" : "content_copy";
const copyIcon = isCopied? "done" : "content_copy"; const canCopyTitle = typeof title === "string" && title.length > 0;
const [direction, placement, growthDirection] = resizingAnchorProps.get(position);
const drawerSize = size || `${width}px`;
className = cssNames("Drawer", className, position);
contentClass = cssNames("drawer-content flex column box grow", contentClass);
const style = size ? { "--size": size } as React.CSSProperties : undefined;
const drawer = ( const drawer = (
<Animate name={animation} enter={open}> <Animate name={animation} enter={open}>
<div className={className} style={style} ref={e => this.contentElem = e}> <div
className={cssNames("Drawer", className, position)}
style={{ "--size": drawerSize } as React.CSSProperties}
ref={e => this.contentElem = e}
>
<div className="drawer-wrapper flex column"> <div className="drawer-wrapper flex column">
<div className="drawer-title flex align-center"> <div className="drawer-title flex align-center">
<div className="drawer-title-text flex gaps align-center"> <div className="drawer-title-text flex gaps align-center">
{title} {title}
{title && typeof title == "string" && ( {canCopyTitle && (
<Icon material={copyIcon} tooltip={copyTooltip} onClick={() => this.copyTitle(title)}/> <Icon material={copyIcon} tooltip={copyTooltip} onClick={() => this.copyTitle(title)}/>
)} )}
</div> </div>
{toolbar} {toolbar}
<Icon material="close" onClick={this.close}/> <Icon material="close" onClick={this.close}/>
</div> </div>
<div className={contentClass} onScroll={this.saveScrollPos} ref={e => this.scrollElem = e}> <div
className={cssNames("drawer-content flex column box grow", contentClass)}
onScroll={this.saveScrollPos}
ref={e => this.scrollElem = e}
>
{children} {children}
</div> </div>
</div> </div>
{
!size && (
<ResizingAnchor
direction={direction}
placement={placement}
growthDirection={growthDirection}
getCurrentExtent={() => width}
onDrag={this.resizeWidth}
onDoubleClick={() => this.resizeWidth(defaultDrawerWidth)}
minExtent={300}
maxExtent={window.innerWidth * 0.9}
/>
)
}
</div> </div>
</Animate> </Animate>
); );

View File

@ -41,13 +41,20 @@ body.resizing {
transition: background 0.2s 0s; transition: background 0.2s 0s;
} }
&:hover { &:hover, &.resizing {
&::after { &::after {
background: var(--blue); background: var(--blue);
transition: background 0.2s 0.5s; transition: background 0.2s 0.5s;
} }
} }
&:hover.wasDragging {
&::after {
background: transparent;
transition: background 0.2s 0s;
}
}
&.disabled { &.disabled {
display: none; display: none;
} }
@ -64,16 +71,6 @@ body.resizing {
margin-left: 0; margin-left: 0;
} }
.resizing & {
$expandedHeight: 200px;
height: $expandedHeight;
margin-top: -$expandedHeight * 0.5;
&::after {
margin-top: $expandedHeight * 0.5;
}
}
&.trailing { &.trailing {
bottom: -$dimension * 0.5; bottom: -$dimension * 0.5;
} }
@ -85,17 +82,6 @@ body.resizing {
cursor: col-resize; cursor: col-resize;
width: $dimension; width: $dimension;
// Expand hoverable area while resizing to keep highlighting resizer.
// Otherwise, cursor can move far away dropping hover indicator.
.resizing & {
$expandedWidth: 200px;
width: $expandedWidth;
&.trailing {
right: -$expandedWidth * 0.5;
}
}
&.leading { &.leading {
left: -$dimension * 0.5; left: -$dimension * 0.5;
} }

View File

@ -24,6 +24,7 @@ import React from "react";
import { action, observable, makeObservable } from "mobx"; import { action, observable, makeObservable } from "mobx";
import _ from "lodash"; import _ from "lodash";
import { cssNames, noop } from "../../utils"; import { cssNames, noop } from "../../utils";
import { observer } from "mobx-react";
export enum ResizeDirection { export enum ResizeDirection {
HORIZONTAL = "horizontal", HORIZONTAL = "horizontal",
@ -162,9 +163,12 @@ function directionDelta(P1: number, P2: number, M: number): number | false {
return false; return false;
} }
@observer
export class ResizingAnchor extends React.PureComponent<Props> { export class ResizingAnchor extends React.PureComponent<Props> {
@observable lastMouseEvent?: MouseEvent; @observable lastMouseEvent?: MouseEvent;
@observable.ref ref?: React.RefObject<HTMLDivElement>; ref = React.createRef<HTMLDivElement>();
@observable isDragging = false;
@observable wasDragging = false;
static defaultProps = { static defaultProps = {
onStart: noop, onStart: noop,
@ -192,7 +196,13 @@ export class ResizingAnchor extends React.PureComponent<Props> {
throw new Error("maxExtent must be >= minExtent"); throw new Error("maxExtent must be >= minExtent");
} }
this.ref = React.createRef<HTMLDivElement>(); const cur = props.getCurrentExtent();
if (cur > props.maxExtent) {
props.onDrag(props.maxExtent);
} else if (cur < props.minExtent) {
props.onDrag(props.minExtent);
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -211,6 +221,7 @@ export class ResizingAnchor extends React.PureComponent<Props> {
document.addEventListener("mousemove", this.onDrag); document.addEventListener("mousemove", this.onDrag);
document.addEventListener("mouseup", this.onDragEnd); document.addEventListener("mouseup", this.onDragEnd);
document.body.classList.add(ResizingAnchor.IS_RESIZING); document.body.classList.add(ResizingAnchor.IS_RESIZING);
this.isDragging = true;
this.lastMouseEvent = undefined; this.lastMouseEvent = undefined;
onStart(); onStart();
@ -296,6 +307,10 @@ export class ResizingAnchor extends React.PureComponent<Props> {
document.removeEventListener("mousemove", this.onDrag); document.removeEventListener("mousemove", this.onDrag);
document.removeEventListener("mouseup", this.onDragEnd); document.removeEventListener("mouseup", this.onDragEnd);
document.body.classList.remove(ResizingAnchor.IS_RESIZING); document.body.classList.remove(ResizingAnchor.IS_RESIZING);
this.isDragging = false;
this.wasDragging = true;
setTimeout(() => this.wasDragging = false, 200);
}; };
render() { render() {
@ -303,7 +318,7 @@ export class ResizingAnchor extends React.PureComponent<Props> {
return <div return <div
ref={this.ref} ref={this.ref}
className={cssNames("ResizingAnchor", direction, placement, { disabled })} className={cssNames("ResizingAnchor", direction, placement, { disabled, resizing: this.isDragging, wasDragging: this.wasDragging })}
onMouseDown={this.onDragInit} onMouseDown={this.onDragInit}
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
/>; />;