Browse Source

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
Jed Fox 5 years ago
parent
commit
d8bbe536a7
7 changed files with 149 additions and 102 deletions
  1. 4 4
      src/components/Dialog.scss
  2. 69 59
      src/components/MobileMenu.tsx
  3. 1 0
      src/components/ToolIcon.scss
  4. 1 1
      src/locales/en.json
  5. 26 12
      src/scene/scrollbars.ts
  6. 42 26
      src/styles.scss
  7. 6 0
      src/utils.ts

+ 4 - 4
src/components/Dialog.scss

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

+ 69 - 59
src/components/MobileMenu.tsx

@@ -13,6 +13,7 @@ import { calculateScrollCenter, getTargetElement } from "../scene";
 import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
 import { Section } from "./Section";
 import { RoomDialog } from "./RoomDialog";
+import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
 
 type MobileMenuProps = {
   appState: AppState;
@@ -37,45 +38,6 @@ export function MobileMenu({
 }: MobileMenuProps) {
   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">
         <Section heading="shapes">
           {heading => (
@@ -98,26 +60,74 @@ export function MobileMenu({
         </Section>
         <HintViewer appState={appState} elements={elements} />
       </FixedSideContainer>
-      <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>
+      <div
+        className="App-bottom-bar"
+        style={{
+          marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+          marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+          marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+        }}
+      >
+        <Island padding={3}>
+          {appState.openMenu === "canvas" ? (
+            <Section className="App-mobile-menu" heading="canvasActions">
+              <div className="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">
+              <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>
     </>
   );
 }

+ 1 - 0
src/components/ToolIcon.scss

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

+ 1 - 1
src/locales/en.json

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

+ 26 - 12
src/scene/scrollbars.ts

@@ -2,8 +2,9 @@ import { ExcalidrawElement } from "../element/types";
 import { getCommonBounds } from "../element";
 import { FlooredNumber } 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_COLOR = "rgba(0,0,0,0.3)";
 
@@ -36,11 +37,18 @@ export function getScrollBars(
   const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
   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
-  const viewportMinX = -scrollX + viewportWidthDiff / 2;
-  const viewportMinY = -scrollY + viewportHeightDiff / 2;
-  const viewportMaxX = viewportMinX + viewportWidthWithZoom;
-  const viewportMaxY = viewportMinY + viewportHeightWithZoom;
+  const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left;
+  const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top;
+  const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
+  const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
 
   // The scene is the bounding box of both the elements and viewport
   const sceneMinX = Math.min(elementsMinX, viewportMinX);
@@ -56,30 +64,36 @@ export function getScrollBars(
         ? null
         : {
             x:
+              Math.max(safeArea.left, SCROLLBAR_MARGIN) +
               ((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
-                viewportWidth +
-              SCROLLBAR_MARGIN,
-            y: viewportHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
+                viewportWidth,
+            y:
+              viewportHeight -
+              SCROLLBAR_WIDTH -
+              Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
             width:
               ((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
                 viewportWidth -
-              SCROLLBAR_MARGIN * 2,
+              Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
             height: SCROLLBAR_WIDTH,
           },
     vertical:
       viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
         ? null
         : {
-            x: viewportWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
+            x:
+              viewportWidth -
+              SCROLLBAR_WIDTH -
+              Math.max(safeArea.right, SCROLLBAR_MARGIN),
             y:
               ((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
                 viewportHeight +
-              SCROLLBAR_MARGIN,
+              Math.max(safeArea.top, SCROLLBAR_MARGIN),
             width: SCROLLBAR_WIDTH,
             height:
               ((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
                 viewportHeight -
-              SCROLLBAR_MARGIN * 2,
+              Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
           },
   };
 }

+ 42 - 26
src/styles.scss

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

+ 6 - 0
src/utils.ts

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