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

Admin UI: Converted ResizeHandler and ScrollQuery components to custom hooks #2801

Merged
merged 6 commits into from
May 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .changeset/dirty-cougars-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/app-admin-ui': patch
---

Converted ResizeHandler and ScrollQuery components to custom hooks.
151 changes: 82 additions & 69 deletions packages/app-admin-ui/client/components/Nav/ResizeHandler.js
Original file line number Diff line number Diff line change
@@ -1,115 +1,128 @@
import { Component } from 'react';
import { useState, useRef, useEffect } from 'react';
import raf from 'raf-schd';

import { withKeyboardConsumer } from '../KeyboardShortcuts';
import { useKeyboardManager } from '../KeyboardShortcuts';

const LS_KEY = 'KEYSTONE_NAVIGATION_STATE';
const DEFAULT_STATE = { isCollapsed: false, width: 280 };
const MIN_WIDTH = 140;
const MAX_WIDTH = 800;

export const KEYBOARD_SHORTCUT = '[';

function getCache() {
if (typeof localStorage !== 'undefined') {
const getCache = () => {
if (localStorage !== undefined) {
const stored = localStorage.getItem(LS_KEY);
return stored ? JSON.parse(stored) : DEFAULT_STATE;
}
return DEFAULT_STATE;
}
function setCache(state) {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(LS_KEY, JSON.stringify(state));
}
}

class ResizeHandler extends Component {
state = getCache();

componentDidMount() {
this.props.keyManager.subscribe(KEYBOARD_SHORTCUT, this.toggleCollapse);
if (stored) {
return JSON.parse(stored);
}
componentWillUnmount() {
this.props.keyManager.unsubscribe(KEYBOARD_SHORTCUT);
}

storeState = s => {
// only keep the `isCollapsed` and `width` properties in locals storage
const isCollapsed = s.isCollapsed !== undefined ? s.isCollapsed : this.state.isCollapsed;
const width = s.width !== undefined ? s.width : this.state.width;

setCache({ isCollapsed, width });
return DEFAULT_STATE;
};

this.setState(s);
const setCache = state => {
if (localStorage !== undefined) {
localStorage.setItem(LS_KEY, JSON.stringify(state));
}
};

handleResizeStart = (event: MouseEvent) => {
// bail if not "left click"
if (event.button && event.button > 0) return;
export const useResizeHandler = () => {
// TODO: should we be calling this in the function body?
const { width: cachedWidth, isCollapsed: cachedIsCollapsed } = getCache();

// initialize resize gesture
this.setState({ initialX: event.pageX, mouseIsDown: true });
// These should trigger renders
const [width, setWidth] = useState(cachedWidth);
const [isCollapsed, setIsCollapsed] = useState(cachedIsCollapsed);
const [isMouseDown, setIsMouseDown] = useState(false);
const [isDragging, setIsDragging] = useState(false);

// attach handlers (handleResizeStart is a bound to onMouseDown)
window.addEventListener('mousemove', this.handleResize);
window.addEventListener('mouseup', this.handleResizeEnd);
};
// Internal state tracking
const initialX = useRef();
const initialWidth = useRef();

initializeDrag = () => {
let initialWidth = this.state.width;
const { addBinding, removeBinding } = useKeyboardManager();

this.setState({ initialWidth, isDragging: true });
useEffect(() => {
addBinding(KEYBOARD_SHORTCUT, toggleCollapse);
return () => {
removeBinding(KEYBOARD_SHORTCUT);
};
}, []);

handleResize = raf((event: MouseEvent) => {
const { initialX, initialWidth, isDragging, mouseIsDown } = this.state;

// on occasion a mouse move event occurs before the event listeners
// have a chance to detach
if (!mouseIsDown) return;
useEffect(() => {
const handleResize = raf(event => {
// on occasion a mouse move event occurs before the event listeners have a chance to detach
if (!isMouseDown) return;

// initialize dragging
if (!isDragging) {
this.initializeDrag(event);
setIsDragging(true);
initialWidth.current = width;
return;
}

// allow the product nav to be 75% of the available page width
const adjustedMax = MAX_WIDTH - initialWidth;
const adjustedMin = MIN_WIDTH - initialWidth;
const adjustedMax = MAX_WIDTH - initialWidth.current;
const adjustedMin = MIN_WIDTH - initialWidth.current;

const delta = Math.max(Math.min(event.pageX - initialX, adjustedMax), adjustedMin);
const width = initialWidth + delta;
const newDelta = Math.max(Math.min(event.pageX - initialX.current, adjustedMax), adjustedMin);
const newWidth = initialWidth.current + newDelta;

this.setState({ delta, width });
setWidth(newWidth);
});
handleResizeEnd = () => {

const handleResizeEnd = () => {
// reset non-width states
this.setState({ delta: 0, isDragging: false, mouseIsDown: false });
setIsDragging(false);
setIsMouseDown(false);
};

// store the width
this.storeState({ width: this.state.width });
window.addEventListener('mousemove', handleResize, { passive: true });
window.addEventListener('mouseup', handleResizeEnd, { passive: true });

// cleanup
window.removeEventListener('mousemove', this.handleResize);
window.removeEventListener('mouseup', this.handleResizeEnd);
return () => {
window.removeEventListener('mousemove', handleResize, { passive: true });
window.removeEventListener('mouseup', handleResizeEnd, { passive: true });
};
toggleCollapse = () => {
const isCollapsed = !this.state.isCollapsed;
this.storeState({ isCollapsed });
}, [isMouseDown, isDragging]);

// Only keep the `isCollapsed` and `width` properties in locals storage
useEffect(() => {
setCache({ isCollapsed, width });
}, [isCollapsed, width]);

const handleResizeStart = event => {
// bail if not "left click"
if (event.button && event.button > 0) return;

// initialize resize gesture
initialX.current = event.pageX;
setIsMouseDown(true);
};

const toggleCollapse = () => {
setIsCollapsed(prevCollapsed => !prevCollapsed);
};

render() {
const resizeProps = {
title: 'Drag to Resize',
onMouseDown: this.handleResizeStart,
onMouseDown: handleResizeStart,
};

const clickProps = {
onClick: this.toggleCollapse,
onClick: toggleCollapse,
};
const snapshot = this.state;

return this.props.children(resizeProps, clickProps, snapshot);
}
}
const snapshot = {
width,
isCollapsed,
isMouseDown,
isDragging,
initialX: initialX.current,
initialWidth: initialWidth.current,
};

export default withKeyboardConsumer(ResizeHandler);
return { resizeProps, clickProps, snapshot };
};
29 changes: 11 additions & 18 deletions packages/app-admin-ui/client/components/Nav/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { FlexGroup } from '@arch-ui/layout';
import { PersonIcon, SignOutIcon, TerminalIcon, MarkGithubIcon } from '@arch-ui/icons';

import { useAdminMeta } from '../../providers/AdminMeta';
import ResizeHandler, { KEYBOARD_SHORTCUT } from './ResizeHandler';
import ScrollQuery from '../ScrollQuery';
import { useResizeHandler, KEYBOARD_SHORTCUT } from './ResizeHandler';
import { useScrollQuery } from '../ScrollQuery';

import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
Expand Down Expand Up @@ -244,6 +244,7 @@ function PrimaryNavItems({
mouseIsOverNav,
}) {
const isAtDashboard = useRouteMatch({ path: adminPath, exact: true });
const [scrollRef, snapshot] = useScrollQuery({ isPassive: false });

let hasRenderedIndexPage = false;
const onRenderIndexPage = () => {
Expand Down Expand Up @@ -278,23 +279,15 @@ function PrimaryNavItems({
);
return (
<Relative>
<ScrollQuery isPassive={false}>
{(ref, snapshot) => (
<PrimaryNavScrollArea ref={ref} {...snapshot}>
<PrimaryNavScrollArea ref={scrollRef} {...snapshot}>
{hasRenderedIndexPage === false && (
<PrimaryNavItem
to={adminPath}
isSelected={isAtDashboard}
mouseIsOverNav={mouseIsOverNav}
>
<PrimaryNavItem to={adminPath} isSelected={isAtDashboard} mouseIsOverNav={mouseIsOverNav}>
Dashboard
</PrimaryNavItem>
)}

{pageNavItems}
</PrimaryNavScrollArea>
)}
</ScrollQuery>
</Relative>
);
}
Expand Down Expand Up @@ -479,9 +472,12 @@ const Nav = ({ children }) => {
setMouseIsOverNav(false);
};

return (
<ResizeHandler isActive={mouseIsOverNav}>
{(resizeProps, clickProps, { isCollapsed, isDragging, width }) => {
const {
resizeProps,
clickProps,
snapshot: { isCollapsed, isDragging, width },
} = useResizeHandler();

const navWidth = isCollapsed ? 0 : width;
const makeResizeStyles = key => {
const pointers = isDragging ? { pointerEvents: 'none' } : null;
Expand Down Expand Up @@ -552,9 +548,6 @@ const Nav = ({ children }) => {
<Page style={makeResizeStyles('marginLeft')}>{children}</Page>
</PageWrapper>
);
}}
</ResizeHandler>
);
};

export default Nav;
Loading