Swap out the resizer lib for something more stable

react-resizer appears to be okay at tracking state, but it often desyncs from reality. re-resizer is more maintained and more broadly used (160k downloads vs 110k), and appears to generally do a better job of tracking the cursor.

The new library has some oddities though, such as deltas, touch support (hence the polyfill), and calling handles "Enable".

For https://github.com/vector-im/riot-web/issues/14022
This commit is contained in:
Travis Ralston 2020-07-07 19:36:26 -06:00
parent 7f757cd0f3
commit 15b6a273c9
6 changed files with 114 additions and 47 deletions

View file

@ -89,11 +89,11 @@
"prop-types": "^15.5.8", "prop-types": "^15.5.8",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"qs": "^6.6.0", "qs": "^6.6.0",
"re-resizable": "^6.5.2",
"react": "^16.9.0", "react": "^16.9.0",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1", "react-focus-lock": "^2.2.1",
"react-resizable": "^1.10.1",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.0", "resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4", "sanitize-html": "^1.18.4",

View file

@ -254,24 +254,26 @@ limitations under the License.
// Class name comes from the ResizableBox component // Class name comes from the ResizableBox component
// The hover state needs to use the whole sublist, not just the resizable box, // The hover state needs to use the whole sublist, not just the resizable box,
// so that selector is below and one level higher. // so that selector is below and one level higher.
.react-resizable-handle { .mx_RoomSublist2_resizerHandle {
cursor: ns-resize; cursor: ns-resize;
border-radius: 3px; border-radius: 3px;
// Update RESIZE_HANDLE_HEIGHT if this changes // Override styles from library
height: 4px; width: unset !important;
height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes
// This is positioned directly below the 'show more' button. // This is positioned directly below the 'show more' button.
position: absolute; position: absolute;
bottom: 0; bottom: 0 !important; // override from library
// Together, these make the bar 64px wide // Together, these make the bar 64px wide
left: calc(50% - 32px); // These are also overridden from the library
right: calc(50% - 32px); left: calc(50% - 32px) !important;
right: calc(50% - 32px) !important;
} }
&:hover, &.mx_RoomSublist2_hasMenuOpen { &:hover, &.mx_RoomSublist2_hasMenuOpen {
.react-resizable-handle { .mx_RoomSublist2_resizerHandle {
opacity: 0.8; opacity: 0.8;
background-color: $primary-fg-color; background-color: $primary-fg-color;
} }

36
src/@types/polyfill.ts Normal file
View file

@ -0,0 +1,36 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export function polyfillTouchEvent() {
// Firefox doesn't have touch events, so create a fake one we can rely on lying about.
if (!window.TouchEvent) {
// We have no intention of actually using this, so just lie.
window.TouchEvent = class TouchEvent extends UIEvent {
public get altKey(): boolean { return false; }
public get changedTouches(): any { return []; }
public get ctrlKey(): boolean { return false; }
public get metaKey(): boolean { return false; }
public get shiftKey(): boolean { return false; }
public get targetTouches(): any { return []; }
public get touches(): any { return []; }
public get rotation(): number { return 0.0; }
public get scale(): number { return 0.0; }
constructor(eventType: string, params?: any) {
super(eventType, params);
}
};
}
}

View file

@ -24,7 +24,6 @@ import {RovingAccessibleButton, RovingTabIndexWrapper} from "../../../accessibil
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2"; import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout"; import { ListLayout } from "../../../stores/room-list/ListLayout";
import { import {
ContextMenu, ContextMenu,
@ -40,7 +39,9 @@ import NotificationBadge from "./NotificationBadge";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import StyledCheckbox from "../elements/StyledCheckbox"; import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@ -58,6 +59,9 @@ const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT; const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
// HACK: We really shouldn't have to do this.
polyfillTouchEvent();
interface IProps { interface IProps {
forRooms: boolean; forRooms: boolean;
rooms?: Room[]; rooms?: Room[];
@ -124,10 +128,25 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (this.props.onAddRoom) this.props.onAddRoom(); if (this.props.onAddRoom) this.props.onAddRoom();
}; };
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => { private onResize = (
const direction = e.movementY < 0 ? -1 : +1; e: MouseEvent | TouchEvent,
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction; travelDirection: Direction,
this.props.layout.setVisibleTilesWithin(tileDiff, this.numTiles); refToElement: HTMLDivElement,
delta: { width: number, height: number }, // TODO: Use NumberSize from re-resizer when it is exposed
) => {
// Do some sanity checks, but in reality we shouldn't need these.
if (travelDirection !== "bottom") return;
if (delta.height === 0) return; // something went wrong, so just ignore it.
// NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate
// for our purposes. The delta provided by the library is also a change *from when
// resizing started*, meaning it is fairly useless for us. This is why we just use
// the client height and run with it.
const heightBefore = this.props.layout.visibleTiles;
const heightInTiles = this.props.layout.pixelsToTiles(refToElement.clientHeight);
this.props.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
if (heightBefore === this.props.layout.visibleTiles) return; // no-op
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
}; };
@ -556,9 +575,19 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
// Figure out if we need a handle // Figure out if we need a handle
let handles = ['s']; const handles: Enable = {
bottom: true, // the only one we need, but the others must be explicitly false
bottomLeft: false,
bottomRight: false,
left: false,
right: false,
top: false,
topLeft: false,
topRight: false,
};
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) { if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
handles = []; // no handles, we're at a minimum // we're at a minimum, don't have a bottom handle
handles.bottom = false;
} }
// We have to account for padding so we can accommodate a 'show more' button and // We have to account for padding so we can accommodate a 'show more' button and
@ -582,22 +611,25 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles); const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding); const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
const dimensions = {
height: tilesPx,
};
content = ( content = (
<ResizableBox <Resizable
width={-1} size={dimensions as any}
height={tilesPx} minHeight={minTilesPx}
axis="y" maxHeight={maxTilesPx}
minConstraints={[-1, minTilesPx]}
maxConstraints={[-1, maxTilesPx]}
resizeHandles={handles}
onResize={this.onResize}
className="mx_RoomSublist2_resizeBox"
onResizeStart={this.onResizeStart} onResizeStart={this.onResizeStart}
onResizeStop={this.onResizeStop} onResizeStop={this.onResizeStop}
onResize={this.onResize}
handleWrapperClass="mx_RoomSublist2_resizerHandles"
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
className="mx_RoomSublist2_resizeBox"
enable={handles}
> >
{visibleTiles} {visibleTiles}
{showNButton} {showNButton}
</ResizableBox> </Resizable>
); );
} }

View file

@ -89,11 +89,12 @@ export class ListLayout {
return 5 + RESIZER_BOX_FACTOR; return 5 + RESIZER_BOX_FACTOR;
} }
public setVisibleTilesWithin(diff: number, maxPossible: number) { public setVisibleTilesWithin(newVal: number, maxPossible: number) {
maxPossible = maxPossible + RESIZER_BOX_FACTOR;
if (this.visibleTiles > maxPossible) { if (this.visibleTiles > maxPossible) {
this.visibleTiles = maxPossible + diff; this.visibleTiles = maxPossible;
} else { } else {
this.visibleTiles += diff; this.visibleTiles = newVal;
} }
} }

View file

@ -2499,7 +2499,7 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@^2.1.2, classnames@^2.2.5: classnames@^2.1.2:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -3779,6 +3779,11 @@ fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-memoize@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
fb-watchman@^2.0.0: fb-watchman@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
@ -6882,7 +6887,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0" object.assign "^4.1.0"
reflect.ownkeys "^0.2.0" reflect.ownkeys "^0.2.0"
prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -7053,6 +7058,13 @@ rc@1.2.8, rc@^1.2.8:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
re-resizable@^6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.5.2.tgz#7eb1928c673285d4dcf654211e47acb9a3801c3e"
integrity sha512-Pjo3ydkr/meTr6j3YZqyv+9fRS5UNOj5SaAI06gHFQ35BnpsZKmwNvupCnbo11gjQ1I62Uy+UzlHLO9xPQEuWQ==
dependencies:
fast-memoize "^2.5.1"
react-beautiful-dnd@^4.0.1: react-beautiful-dnd@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
@ -7086,14 +7098,6 @@ react-dom@^16.9.0:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.19.1" scheduler "^0.19.1"
react-draggable@^4.0.3:
version "4.4.2"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.2.tgz#f3cefecee25f467f865144cda0d066e5f05f94a0"
integrity sha512-zLQs4R4bnBCGnCVTZiD8hPsHtkiJxgMpGDlRESM+EHQo8ysXhKJ2GKdJ8UxxLJdRVceX1j19jy+hQS2wHislPQ==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-focus-lock@^2.2.1: react-focus-lock@^2.2.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47"
@ -7138,14 +7142,6 @@ react-redux@^5.0.6:
react-is "^16.6.0" react-is "^16.6.0"
react-lifecycles-compat "^3.0.0" react-lifecycles-compat "^3.0.0"
react-resizable@^1.10.1:
version "1.10.1"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.10.1.tgz#f0c2cf1d83b3470b87676ce6d6b02bbe3f4d8cd4"
integrity sha512-Jd/bKOKx6+19NwC4/aMLRu/J9/krfxlDnElP41Oc+oLiUWs/zwV1S9yBfBZRnqAwQb6vQ/HRSk3bsSWGSgVbpw==
dependencies:
prop-types "15.x"
react-draggable "^4.0.3"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0: react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"