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:
parent
df46dd9ef6
commit
7ce4a87ac8
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
/>;
|
/>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user