Restyle the color picker a touch (#920)

This commit is contained in:
Jed Fox 2020-03-15 13:26:52 -04:00 committed by GitHub
parent d834ff4d89
commit e44801123a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 203 additions and 148 deletions

View File

@ -1,28 +1,18 @@
.color-picker { .color-picker {
width: 205px;
background: rgb(255, 255, 255); background: rgb(255, 255, 255);
border: 0px solid rgba(0, 0, 0, 0.25); border: 0px solid rgba(0, 0, 0, 0.25);
box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 4px; box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 4px;
border-radius: 4px; border-radius: 4px;
position: relative; position: absolute;
left: -5.5px;
} }
.color-picker-control-container { .color-picker-control-container {
display: flex; display: grid;
grid-template-columns: auto 1fr;
align-items: center; align-items: center;
} }
.color-picker-triangle-shadow {
width: 0px;
height: 0px;
border-style: solid;
border-width: 0px 9px 10px;
border-color: transparent transparent rgba(0, 0, 0, 0.1);
position: absolute;
top: -11px;
left: 12px;
}
.color-picker-triangle { .color-picker-triangle {
width: 0px; width: 0px;
height: 0px; height: 0px;
@ -34,13 +24,20 @@
left: 12px; left: 12px;
} }
.color-picker-content { .color-picker-triangle-shadow {
padding: 1rem 0.5rem 0.5rem 1rem; border-color: transparent transparent rgba(0, 0, 0, 0.1);
top: -11px;
} }
.colors-gallery { .color-picker-content {
display: flex; padding: 0.5rem 0.5rem;
flex-wrap: wrap; display: grid;
grid-template-columns: repeat(5, auto);
grid-gap: 0.5rem;
}
.color-picker-content .color-input-container {
grid-column: 1 / span 5;
} }
.color-picker-swatch { .color-picker-swatch {
@ -49,7 +46,7 @@
width: 1.875rem; width: 1.875rem;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
margin: 0px 0.375rem 0.375rem 0px; margin: 0;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
@ -68,6 +65,9 @@
right: 0px; right: 0px;
bottom: 0px; bottom: 0px;
left: 0px; left: 0px;
}
.color-picker-transparent,
.color-picker-label-swatch {
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
left center; left center;
} }
@ -81,6 +81,27 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1;
position: relative;
}
.color-input-container:focus-within .color-picker-hash {
box-shadow: 0 0 0 2px #a5d8ff;
}
.color-input-container:focus-within .color-picker-hash::before,
.color-input-container:focus-within .color-picker-hash::after {
content: "";
width: 1px;
height: 100%;
position: absolute;
top: 0;
}
.color-input-container:focus-within .color-picker-hash::before {
background: #dee2e6;
right: -1px;
}
.color-input-container:focus-within .color-picker-hash::after {
background: #fff;
right: -2px;
} }
.color-input-container { .color-input-container {
@ -88,14 +109,14 @@
} }
.color-picker-input { .color-picker-input {
width: 6.25em; width: 12ch; /* length of `transparent` + 1 */
margin: 0;
font-size: 1rem; font-size: 1rem;
color: #343a40; color: #343a40;
border: 0px; border: 0px;
outline: none; outline: none;
height: 1.75em; height: 1.75em;
box-shadow: #dee2e6 0px 0px 0px 1px inset; box-shadow: #dee2e6 0px 0px 0px 1px inset;
box-sizing: content-box;
border-radius: 0px 4px 4px 0px; border-radius: 0px 4px 4px 0px;
float: left; float: left;
padding: 1px; padding: 1px;
@ -108,6 +129,19 @@
width: 1.875rem; width: 1.875rem;
margin-right: 0.25rem; margin-right: 0.25rem;
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
position: relative;
overflow: hidden;
background-color: transparent !important;
}
.color-picker-label-swatch::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--swatch-color);
} }
.color-picker-keybinding { .color-picker-keybinding {

View File

@ -24,12 +24,14 @@ const Picker = function({
onChange, onChange,
onClose, onClose,
label, label,
showInput = true,
}: { }: {
colors: string[]; colors: string[];
color: string | null; color: string | null;
onChange: (color: string) => void; onChange: (color: string) => void;
onClose: () => void; onClose: () => void;
label: string; label: string;
showInput: boolean;
}) { }) {
const firstItem = React.useRef<HTMLButtonElement>(); const firstItem = React.useRef<HTMLButtonElement>();
const activeItem = React.useRef<HTMLButtonElement>(); const activeItem = React.useRef<HTMLButtonElement>();
@ -72,7 +74,7 @@ const Picker = function({
activeElement, activeElement,
); );
if (index !== -1) { if (index !== -1) {
const length = gallery!.current!.children.length; const length = gallery!.current!.children.length - (showInput ? 1 : 0);
const nextIndex = const nextIndex =
event.key === KEYS.ARROW_RIGHT event.key === KEYS.ARROW_RIGHT
? (index + 1) % length ? (index + 1) % length
@ -108,57 +110,57 @@ const Picker = function({
aria-label={t("labels.colorPicker")} aria-label={t("labels.colorPicker")}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<div className="color-picker-triangle-shadow"></div> <div className="color-picker-triangle color-picker-triangle-shadow"></div>
<div className="color-picker-triangle"></div> <div className="color-picker-triangle"></div>
<div className="color-picker-content"> <div
<div className="color-picker-content"
className="colors-gallery" ref={el => {
ref={el => { if (el) {
if (el) { gallery.current = el;
gallery.current = el; }
} }}
}} >
> {colors.map((_color, i) => (
{colors.map((_color, i) => ( <button
<button className="color-picker-swatch"
className="color-picker-swatch" onClick={() => {
onClick={() => { onChange(_color);
onChange(_color); }}
}} title={`${_color} ${keyBindings[i].toUpperCase()}`}
title={`${_color}${keyBindings[i].toUpperCase()}`} aria-label={_color}
aria-label={_color} aria-keyshortcuts={keyBindings[i]}
aria-keyshortcuts={keyBindings[i]} style={{ backgroundColor: _color }}
style={{ backgroundColor: _color }} key={_color}
key={_color} ref={el => {
ref={el => { if (el && i === 0) {
if (el && i === 0) { firstItem.current = el;
firstItem.current = el; }
} if (el && _color === color) {
if (el && _color === color) { activeItem.current = el;
activeItem.current = el; }
} }}
}} onFocus={() => {
onFocus={() => { onChange(_color);
onChange(_color); }}
}} >
> {_color === "transparent" ? (
{_color === "transparent" ? ( <div className="color-picker-transparent"></div>
<div className="color-picker-transparent"></div> ) : (
) : ( undefined
undefined )}
)} <span className="color-picker-keybinding">{keyBindings[i]}</span>
<span className="color-picker-keybinding">{keyBindings[i]}</span> </button>
</button> ))}
))} {showInput && (
</div> <ColorInput
<ColorInput color={color}
color={color} label={label}
label={label} onChange={color => {
onChange={color => { onChange(color);
onChange(color); }}
}} ref={colorInput}
ref={colorInput} />
/> )}
</div> </div>
</div> </div>
); );
@ -188,7 +190,7 @@ const ColorInput = React.forwardRef(
React.useImperativeHandle(ref, () => inputRef.current); React.useImperativeHandle(ref, () => inputRef.current);
return ( return (
<div className="color-input-container"> <label className="color-input-container">
<div className="color-picker-hash">#</div> <div className="color-picker-hash">#</div>
<input <input
spellCheck={false} spellCheck={false}
@ -206,7 +208,7 @@ const ColorInput = React.forwardRef(
onBlur={() => setInnerValue(color)} onBlur={() => setInnerValue(color)}
ref={inputRef} ref={inputRef}
/> />
</div> </label>
); );
}, },
); );
@ -231,7 +233,11 @@ export function ColorPicker({
<button <button
className="color-picker-label-swatch" className="color-picker-label-swatch"
aria-label={label} aria-label={label}
style={color ? { backgroundColor: color } : undefined} style={
color
? ({ "--swatch-color": color } as React.CSSProperties)
: undefined
}
onClick={() => setActive(!isActive)} onClick={() => setActive(!isActive)}
ref={pickerButton} ref={pickerButton}
/> />
@ -257,6 +263,7 @@ export function ColorPicker({
pickerButton.current?.focus(); pickerButton.current?.focus();
}} }}
label={label} label={label}
showInput={false}
/> />
</Popover> </Popover>
) : null} ) : null}

View File

@ -1,7 +1,6 @@
@import "../_variables"; @import "../_variables";
.Dialog__title { .Dialog__title {
--metric: calc(var(--space-factor) * 4);
display: grid; display: grid;
align-items: center; align-items: center;
margin-top: 0; margin-top: 0;
@ -18,15 +17,23 @@
} }
@media #{$media-query} { @media #{$media-query} {
.Dialog {
--metric: calc(var(--space-factor) * 4);
--inset-left: #{"max(var(--metric), env(safe-area-inset-left))"};
--inset-right: #{"max(var(--metric), env(safe-area-inset-right))"};
}
.Dialog__title { .Dialog__title {
grid-template-columns: calc(var(--space-factor) * 7) 1fr calc( grid-template-columns: calc(var(--space-factor) * 7) 1fr calc(
var(--space-factor) * 7 var(--space-factor) * 7
); );
position: sticky; position: sticky;
top: calc(-1 * var(--metric)); top: calc(-1 * var(--metric));
margin: calc(-1 * var(--metric)); margin: calc(-1 * var(--inset-right));
margin-top: calc(-1 * var(--metric));
margin-bottom: var(--metric); margin-bottom: var(--metric);
padding: calc(var(--space-factor) * 2) var(--metric); padding: calc(var(--space-factor) * 2);
padding-left: var(--inset-left);
padding-right: var(--inset-right);
background: white; background: white;
font-size: 1.25em; font-size: 1.25em;
@ -38,9 +45,13 @@
text-align: center; text-align: center;
} }
.Dialog .Island { .Dialog .Island {
width: 100vw;
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-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))"};
} }
.Dialog .Modal__close { .Dialog .Modal__close {

View File

@ -16,16 +16,30 @@
} }
.ExportDialog__actions { .ExportDialog__actions {
width: 100%;
display: flex; display: flex;
grid-gap: calc(var(--space-factor) * 2);
align-items: top; align-items: top;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
} }
.ExportDialog__scales { .ExportDialog__name {
display: flex; grid-column: project-name;
align-items: baseline; margin: auto;
justify-content: flex-end; }
@media (max-width: 550px) {
.ExportDialog {
display: flex;
flex-direction: column;
}
.ExportDialog__actions {
flex-direction: column;
align-items: center;
}
.ExportDialog__actions > * {
margin-bottom: calc(var(--space-factor) * 3);
}
} }
@media #{$media-query} { @media #{$media-query} {
@ -40,13 +54,4 @@
.ExportDialog__dialog .Island { .ExportDialog__dialog .Island {
overflow-y: auto; overflow-y: auto;
} }
.ExportDialog__actions {
flex-direction: column;
}
.ExportDialog__actions > * {
margin-bottom: calc(var(--space-factor) * 3);
}
.ExportDialog__scales {
justify-content: flex-start;
}
} }

View File

@ -111,10 +111,10 @@ function ExportModal({
} }
return ( return (
<div onKeyDown={handleKeyDown}> <div onKeyDown={handleKeyDown} className="ExportDialog">
<div className="ExportDialog__preview" ref={previewRef}></div> <div className="ExportDialog__preview" ref={previewRef}></div>
<div className="ExportDialog__actions"> <Stack.Col gap={2} align="center">
<Stack.Col gap={1}> <div className="ExportDialog__actions">
<Stack.Row gap={2}> <Stack.Row gap={2}>
<ToolButton <ToolButton
type="button" type="button"
@ -148,44 +148,42 @@ function ExportModal({
onClick={() => onExportToBackend(exportedElements)} onClick={() => onExportToBackend(exportedElements)}
/> />
</Stack.Row> </Stack.Row>
</Stack.Col> <div className="ExportDialog__name">
{actionManager.renderAction("changeProjectName")} {actionManager.renderAction("changeProjectName")}
<Stack.Col gap={1}>
<div className="ExportDialog__scales">
<Stack.Row gap={2} align="baseline">
{scales.map(s => (
<ToolButton
key={s}
size="s"
type="radio"
icon={`x${s}`}
name="export-canvas-scale"
aria-label={`Scale ${s} x`}
id="export-canvas-scale"
checked={s === scale}
onChange={() => setScale(s)}
/>
))}
</Stack.Row>
</div> </div>
{actionManager.renderAction("changeExportBackground")} <Stack.Row gap={2}>
{someElementIsSelected && ( {scales.map(s => (
<div> <ToolButton
<label> key={s}
<input size="s"
type="checkbox" type="radio"
checked={exportSelected} icon={`x${s}`}
onChange={event => name="export-canvas-scale"
setExportSelected(event.currentTarget.checked) aria-label={`Scale ${s} x`}
} id="export-canvas-scale"
ref={onlySelectedInput} checked={s === scale}
/>{" "} onChange={() => setScale(s)}
{t("labels.onlySelected")} />
</label> ))}
</div> </Stack.Row>
)} </div>
</Stack.Col> {actionManager.renderAction("changeExportBackground")}
</div> {someElementIsSelected && (
<div>
<label>
<input
type="checkbox"
checked={exportSelected}
onChange={event =>
setExportSelected(event.currentTarget.checked)
}
ref={onlySelectedInput}
/>{" "}
{t("labels.onlySelected")}
</label>
</div>
)}
</Stack.Col>
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { useLayoutEffect, useRef } from "react"; import React, { useLayoutEffect, useRef, useEffect } from "react";
import "./Popover.css"; import "./Popover.css";
type Props = { type Props = {
@ -35,18 +35,20 @@ export function Popover({
} }
}, [fitInViewport]); }, [fitInViewport]);
useEffect(() => {
if (onCloseRequest) {
const handler = (e: Event) => {
if (!popoverRef.current?.contains(e.target as Node)) {
onCloseRequest();
}
};
document.addEventListener("pointerdown", handler, false);
return () => document.removeEventListener("pointerdown", handler, false);
}
}, [onCloseRequest]);
return ( return (
<div className="popover" style={{ top: top, left: left }} ref={popoverRef}> <div className="popover" style={{ top: top, left: left }} ref={popoverRef}>
<div
className="cover"
onClick={onCloseRequest}
onContextMenu={event => {
event.preventDefault();
if (onCloseRequest) {
onCloseRequest();
}
}}
/>
{children} {children}
</div> </div>
); );

View File

@ -2,9 +2,8 @@
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
border: 1.5px solid #eee; border: 1.5px solid #eee;
height: 2.5rem; line-height: 1;
line-height: 2.5rem; padding: 0.75rem;
padding: 0 0.5rem;
white-space: nowrap; white-space: nowrap;
border-radius: var(--space-factor); border-radius: var(--space-factor);
} }

View File

@ -13,7 +13,6 @@
cursor: pointer; cursor: pointer;
background-color: var(--button-gray-1); background-color: var(--button-gray-1);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor); border-radius: var(--space-factor);
} }