Restyle the mobile UI a bit (#1002)

* Restyle the bottom bar on mobile as an Island

* Shorter label for collaboration button, truncate too-long button labels

* Refactor safe area things to global vars

* Fix scroll bar positioning, don’t block scrollbars with menu island

* Update text
This commit is contained in:
Jed Fox 2020-03-18 11:31:40 -04:00 committed by GitHub
parent f889f6da91
commit d8bbe536a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 104 deletions

View File

@ -19,7 +19,7 @@
@media #{$media-query} { @media #{$media-query} {
.Dialog { .Dialog {
--metric: calc(var(--space-factor) * 4); --metric: calc(var(--space-factor) * 4);
--inset-left: #{"max(var(--metric), env(safe-area-inset-left))"}; --inset-left: #{"max(var(--metric), var(--sal))"};
--inset-right: #{"max(var(--metric), env(safe-area-inset-right))"}; --inset-right: #{"max(var(--metric), env(safe-area-inset-right))"};
} }
.Dialog__title { .Dialog__title {
@ -49,9 +49,9 @@
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
overflow-y: auto; overflow-y: auto;
padding-left: #{"max(calc(var(--padding) * var(--space-factor)), env(safe-area-inset-left))"}; padding-left: #{"max(calc(var(--padding) * var(--space-factor)), var(--sal))"};
padding-right: #{"max(calc(var(--padding) * var(--space-factor)), env(safe-area-inset-right))"}; padding-right: #{"max(calc(var(--padding) * var(--space-factor)), var(--sar))"};
padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), env(safe-area-inset-bottom))"}; padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), var(--sab))"};
} }
.Dialog .Modal__close { .Dialog .Modal__close {

View File

@ -13,6 +13,7 @@ import { calculateScrollCenter, getTargetElement } from "../scene";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section"; import { Section } from "./Section";
import { RoomDialog } from "./RoomDialog"; import { RoomDialog } from "./RoomDialog";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: AppState;
@ -37,45 +38,6 @@ export function MobileMenu({
}: MobileMenuProps) { }: MobileMenuProps) {
return ( return (
<> <>
{appState.openMenu === "canvas" ? (
<Section className="App-mobile-menu" heading="canvasActions">
<div className="App-mobile-menu-scroller panelColumn">
<Stack.Col gap={4}>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{exportButton}
{actionManager.renderAction("clearCanvas")}
<RoomDialog
isCollaborating={appState.isCollaborating}
collaboratorCount={appState.collaborators.size}
onRoomCreate={onRoomCreate}
onRoomDestroy={onRoomDestroy}
/>
{actionManager.renderAction("changeViewBackgroundColor")}
<fieldset>
<legend>{t("labels.language")}</legend>
<LanguageList
onChange={lng => {
setLanguage(lng);
setAppState({});
}}
/>
</fieldset>
</Stack.Col>
</div>
</Section>
) : appState.openMenu === "shape" &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<div className="App-mobile-menu-scroller">
<SelectedShapeActions
targetElements={getTargetElement(elements, appState)}
renderAction={actionManager.renderAction}
elementType={appState.elementType}
/>
</div>
</Section>
) : null}
<FixedSideContainer side="top"> <FixedSideContainer side="top">
<Section heading="shapes"> <Section heading="shapes">
{heading => ( {heading => (
@ -98,26 +60,74 @@ export function MobileMenu({
</Section> </Section>
<HintViewer appState={appState} elements={elements} /> <HintViewer appState={appState} elements={elements} />
</FixedSideContainer> </FixedSideContainer>
<footer className="App-toolbar"> <div
<div className="App-toolbar-content"> className="App-bottom-bar"
{actionManager.renderAction("toggleCanvasMenu")} style={{
{actionManager.renderAction("toggleEditMenu")} marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
{actionManager.renderAction("undo")} marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
{actionManager.renderAction("redo")} marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
{actionManager.renderAction("finalize")} }}
{actionManager.renderAction("deleteSelectedElements")} >
</div> <Island padding={3}>
{appState.scrolledOutside && ( {appState.openMenu === "canvas" ? (
<button <Section className="App-mobile-menu" heading="canvasActions">
className="scroll-back-to-content" <div className="panelColumn">
onClick={() => { <Stack.Col gap={4}>
setAppState({ ...calculateScrollCenter(elements) }); {actionManager.renderAction("loadScene")}
}} {actionManager.renderAction("saveScene")}
> {exportButton}
{t("buttons.scrollBackToContent")} {actionManager.renderAction("clearCanvas")}
</button> <RoomDialog
)} isCollaborating={appState.isCollaborating}
</footer> collaboratorCount={appState.collaborators.size}
onRoomCreate={onRoomCreate}
onRoomDestroy={onRoomDestroy}
/>
{actionManager.renderAction("changeViewBackgroundColor")}
<fieldset>
<legend>{t("labels.language")}</legend>
<LanguageList
onChange={lng => {
setLanguage(lng);
setAppState({});
}}
/>
</fieldset>
</Stack.Col>
</div>
</Section>
) : appState.openMenu === "shape" &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
targetElements={getTargetElement(elements, appState)}
renderAction={actionManager.renderAction}
elementType={appState.elementType}
/>
</Section>
) : null}
<footer className="App-toolbar">
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{actionManager.renderAction("finalize")}
{actionManager.renderAction("deleteSelectedElements")}
</div>
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({ ...calculateScrollCenter(elements) });
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>
</> </>
); );
} }

View File

@ -39,6 +39,7 @@
.ToolIcon__label { .ToolIcon__label {
font-family: var(--ui-font); font-family: var(--ui-font);
margin: 0 0.8em; margin: 0 0.8em;
text-overflow: ellipsis;
} }
.ToolIcon_size_s .ToolIcon__icon { .ToolIcon_size_s .ToolIcon__icon {

View File

@ -66,7 +66,7 @@
"edit": "Edit", "edit": "Edit",
"undo": "Undo", "undo": "Undo",
"redo": "Redo", "redo": "Redo",
"roomDialog": "Share a live-collaboration session", "roomDialog": "Start live collaboration",
"createNewRoom": "Create new room" "createNewRoom": "Create new room"
}, },
"alerts": { "alerts": {

View File

@ -2,8 +2,9 @@ import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element"; import { getCommonBounds } from "../element";
import { FlooredNumber } from "../types"; import { FlooredNumber } from "../types";
import { ScrollBars } from "./types"; import { ScrollBars } from "./types";
import { getGlobalCSSVariable } from "../utils";
const SCROLLBAR_MARGIN = 4; export const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6; export const SCROLLBAR_WIDTH = 6;
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)"; export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
@ -36,11 +37,18 @@ export function getScrollBars(
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom; const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom; const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
const safeArea = {
top: parseInt(getGlobalCSSVariable("sat")),
bottom: parseInt(getGlobalCSSVariable("sab")),
left: parseInt(getGlobalCSSVariable("sal")),
right: parseInt(getGlobalCSSVariable("sar")),
};
// The viewport is the rectangle currently visible for the user // The viewport is the rectangle currently visible for the user
const viewportMinX = -scrollX + viewportWidthDiff / 2; const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left;
const viewportMinY = -scrollY + viewportHeightDiff / 2; const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top;
const viewportMaxX = viewportMinX + viewportWidthWithZoom; const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
const viewportMaxY = viewportMinY + viewportHeightWithZoom; const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
// The scene is the bounding box of both the elements and viewport // The scene is the bounding box of both the elements and viewport
const sceneMinX = Math.min(elementsMinX, viewportMinX); const sceneMinX = Math.min(elementsMinX, viewportMinX);
@ -56,30 +64,36 @@ export function getScrollBars(
? null ? null
: { : {
x: x:
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) * ((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
viewportWidth + viewportWidth,
SCROLLBAR_MARGIN, y:
y: viewportHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN, viewportHeight -
SCROLLBAR_WIDTH -
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
width: width:
((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) * ((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
viewportWidth - viewportWidth -
SCROLLBAR_MARGIN * 2, Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
height: SCROLLBAR_WIDTH, height: SCROLLBAR_WIDTH,
}, },
vertical: vertical:
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
? null ? null
: { : {
x: viewportWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN, x:
viewportWidth -
SCROLLBAR_WIDTH -
Math.max(safeArea.right, SCROLLBAR_MARGIN),
y: y:
((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) * ((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
viewportHeight + viewportHeight +
SCROLLBAR_MARGIN, Math.max(safeArea.top, SCROLLBAR_MARGIN),
width: SCROLLBAR_WIDTH, width: SCROLLBAR_WIDTH,
height: height:
((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) * ((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
viewportHeight - viewportHeight -
SCROLLBAR_MARGIN * 2, Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
}, },
}; };
} }

View File

@ -1,6 +1,13 @@
@import "./theme.css"; @import "./theme.css";
@import "./_variables"; @import "./_variables";
:root {
--sat: env(safe-area-inset-top);
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
--sar: env(safe-area-inset-right);
}
body { body {
margin: 0; margin: 0;
--ui-font: Arial, Helvetica, sans-serif; --ui-font: Arial, Helvetica, sans-serif;
@ -119,6 +126,7 @@ button,
cursor: pointer; cursor: pointer;
&:focus { &:focus {
outline: transparent;
box-shadow: 0 0 0 2px #a5d8ff; box-shadow: 0 0 0 2px #a5d8ff;
} }
@ -146,24 +154,38 @@ button,
} }
} }
.App-toolbar, .App-bottom-bar {
.App-mobile-menu {
--spacing: 0.5rem;
--padding: calc(4 * var(--space-factor));
padding: var(--padding);
padding-left: #{"max(var(--padding), env(safe-area-inset-left))"};
padding-right: #{"max(var(--padding), env(safe-area-inset-right))"};
background: #fcfcfc;
border-top: 1px solid #ccc;
box-sizing: border-box;
}
.App-toolbar {
padding-bottom: #{"max(var(--padding), env(safe-area-inset-bottom))"};
width: 100%;
box-sizing: border-box;
overflow: auto;
position: absolute; position: absolute;
top: 0;
bottom: 0; bottom: 0;
left: 0;
right: 0;
--bar-padding: calc(4 * var(--space-factor));
padding-top: #{"max(var(--bar-padding), var(--sat))"};
padding-left: var(--sal);
padding-right: var(--sar);
padding-bottom: var(--sab);
z-index: 4;
display: flex;
align-items: flex-end;
pointer-events: none;
> .Island {
width: 100%;
max-width: 100%;
min-width: 100%;
box-sizing: border-box;
max-height: 100%;
display: flex;
flex-direction: column;
pointer-events: initial;
}
}
.App-toolbar {
width: 100%;
box-sizing: border-box;
} }
.App-toolbar-content { .App-toolbar-content {
display: flex; display: flex;
@ -171,18 +193,11 @@ button,
justify-content: space-between; justify-content: space-between;
} }
.App-mobile-menu { .App-mobile-menu {
--bottom: calc(3rem - 1px + max(var(--padding), env(safe-area-inset-bottom)));
display: grid;
position: fixed;
width: 100%; width: 100%;
bottom: var(--bottom); overflow-x: visible;
z-index: 4; overflow-y: auto;
max-height: calc(100% - var(--bottom)); box-sizing: border-box;
overflow-y: scroll; margin-bottom: var(--bar-padding);
}
.App-mobile-menu .App-mobile-menu-scroller {
background: #fcfcfc;
box-shadow: none;
} }
.App-menu { .App-menu {
@ -358,6 +373,7 @@ button,
bottom: 30px; bottom: 30px;
transform: translateX(-50%); transform: translateX(-50%);
padding: 10px 20px; padding: 10px 20px;
z-index: -1;
} }
@media #{$media-query} { @media #{$media-query} {
@ -366,6 +382,6 @@ button,
} }
.scroll-back-to-content { .scroll-back-to-content {
bottom: 80px; bottom: 80px;
bottom: calc(80px + env(safe-area-inset-bottom)); bottom: calc(80px + var(--sab));
} }
} }

View File

@ -194,3 +194,9 @@ export function sceneCoordsToViewportCoords(
return { x, y }; return { x, y };
} }
export function getGlobalCSSVariable(name: string) {
return getComputedStyle(document.documentElement).getPropertyValue(
`--${name}`,
);
}