More Arrowheads: dot, bar (#2486)

Co-authored-by: Jed Fox <git@jedfox.com>
Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
Steve Ruiz
2020-12-11 17:17:28 +00:00
committed by GitHub
parent 7c7fb4903b
commit c742225f43
10 changed files with 550 additions and 70 deletions

View File

@ -0,0 +1,137 @@
@import "open-color/open-color.scss";
.excalidraw {
.picker-container {
display: inline-block;
box-sizing: border-box;
margin-right: 0.25rem;
}
.picker {
background: var(--popup-background-color);
border: 0px solid transparentize($oc-white, 0.75);
box-shadow: transparentize($oc-black, 0.75) 0px 1px 4px;
border-radius: 4px;
position: absolute;
}
.picker-container button,
.picker button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
&:focus {
outline: transparent;
background-color: var(--button-gray-2);
& svg {
opacity: 1;
}
}
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
&:disabled {
cursor: not-allowed;
}
svg {
margin: 0;
width: 36px;
height: 18px;
opacity: 0.6;
pointer-events: none;
}
}
.picker button {
padding: 0.25rem 0.28rem 0.35rem 0.25rem;
}
.picker-triangle {
width: 0px;
height: 0px;
position: relative;
top: -10px;
:root[dir="ltr"] & {
left: 12px;
}
:root[dir="rtl"] & {
right: 12px;
}
z-index: 10;
&:before {
content: "";
position: absolute;
border-style: solid;
border-width: 0px 9px 10px;
border-color: transparent transparent transparentize($oc-black, 0.9);
top: -1px;
}
&:after {
content: "";
position: absolute;
border-style: solid;
border-width: 0px 9px 10px;
border-color: transparent transparent var(--popup-background-color);
}
}
.picker-content {
padding: 0.5rem;
display: grid;
grid-auto-flow: column;
grid-gap: 0.5rem;
border-radius: 4px;
}
.picker-keybinding {
position: absolute;
bottom: 2px;
:root[dir="ltr"] & {
right: 2px;
}
:root[dir="rtl"] & {
left: 2px;
}
font-size: 0.7em;
}
.picker-type-canvasBackground .picker-keybinding {
color: #aaa;
}
.picker-type-elementBackground .picker-keybinding {
color: #fff;
}
.picker-swatch[aria-label="transparent"] .picker-keybinding {
color: #aaa;
}
.picker-type-elementStroke .picker-keybinding {
color: #d4d4d4;
}
&.Appearance_dark {
.picker-type-elementBackground .picker-keybinding {
color: #000;
}
.picker-swatch[aria-label="transparent"] .picker-keybinding {
color: #000;
}
}
}

View File

@ -0,0 +1,188 @@
import React from "react";
import { Popover } from "./Popover";
import "./IconPicker.scss";
import { isArrowKey, KEYS } from "../keys";
import { getLanguage } from "../i18n";
function Picker<T>({
options,
value,
label,
onChange,
onClose,
}: {
label: string;
value: T;
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
onChange: (value: T) => void;
onClose: () => void;
}) {
const rFirstItem = React.useRef<HTMLButtonElement>();
const rActiveItem = React.useRef<HTMLButtonElement>();
const rGallery = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
// After the component is first mounted focus on first input
if (rActiveItem.current) {
rActiveItem.current.focus();
} else if (rGallery.current) {
rGallery.current.focus();
}
}, []);
const handleKeyDown = (event: React.KeyboardEvent) => {
const pressedOption = options.find(
(option) => option.keyBinding === event.key.toLowerCase(),
)!;
if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
// Keybinding navigation
const index = options.indexOf(pressedOption);
(rGallery!.current!.children![index] as any).focus();
event.preventDefault();
} else if (event.key === KEYS.TAB) {
// Tab navigation cycle through options. If the user tabs
// away from the picker, close the picker. We need to use
// a timeout here to let the stack clear before checking.
setTimeout(() => {
const active = rActiveItem.current;
const docActive = document.activeElement;
if (active !== docActive) {
onClose();
}
}, 0);
} else if (isArrowKey(event.key)) {
// Arrow navigation
const { activeElement } = document;
const isRTL = getLanguage().rtl;
const index = Array.prototype.indexOf.call(
rGallery!.current!.children,
activeElement,
);
if (index !== -1) {
const length = options.length;
let nextIndex = index;
switch (event.key) {
// Select the next option
case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT:
case KEYS.ARROW_DOWN: {
nextIndex = (index + 1) % length;
break;
}
// Select the previous option
case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT:
case KEYS.ARROW_UP: {
nextIndex = (length + index - 1) % length;
break;
}
}
(rGallery.current!.children![nextIndex] as any).focus();
}
event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
// Close on escape or enter
event.preventDefault();
onClose();
}
event.nativeEvent.stopImmediatePropagation();
};
return (
<div
className={`picker`}
role="dialog"
aria-modal="true"
aria-label={label}
onKeyDown={handleKeyDown}
>
<div className="picker-content" ref={rGallery}>
{options.map((option, i) => (
<button
className="picker-option"
onClick={(event) => {
(event.currentTarget as HTMLButtonElement).focus();
onChange(option.value);
}}
title={`${option.text}${option.keyBinding.toUpperCase()}`}
aria-label={option.text || "none"}
aria-keyshortcuts={option.keyBinding}
key={option.text}
ref={(el) => {
if (el && i === 0) {
rFirstItem.current = el;
}
if (el && option.value === value) {
rActiveItem.current = el;
}
}}
onFocus={() => {
onChange(option.value);
}}
>
{option.icon}
<span className="picker-keybinding">{option.keyBinding}</span>
</button>
))}
</div>
</div>
);
}
export function IconPicker<T>({
value,
label,
options,
onChange,
group = "",
}: {
label: string;
value: T;
options: { value: T; text: string; icon: JSX.Element; keyBinding: string }[];
onChange: (value: T) => void;
group?: string;
}) {
const [isActive, setActive] = React.useState(false);
const rPickerButton = React.useRef<any>(null);
const isRTL = getLanguage().rtl;
return (
<label className={"picker-container"}>
<button
name={group}
className={isActive ? "active" : ""}
aria-label={label}
onClick={() => setActive(!isActive)}
ref={rPickerButton}
>
{options.find((option) => option.value === value)?.icon}
</button>
<React.Suspense fallback="">
{isActive ? (
<>
<Popover
onCloseRequest={(event) =>
event.target !== rPickerButton.current && setActive(false)
}
{...(isRTL ? { right: 5.5 } : { left: -5.5 })}
>
<Picker
options={options}
value={value}
label={label}
onChange={onChange}
onClose={() => {
setActive(false);
rPickerButton.current?.focus();
}}
/>
</Popover>
<div className="picker-triangle" />
</>
) : null}
</React.Suspense>
</label>
);
}

View File

@ -82,7 +82,7 @@ export const MobileMenu = ({
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}}
>
<Island padding={3}>
<Island padding={0}>
{appState.openMenu === "canvas" ? (
<Section className="App-mobile-menu" heading="canvasActions">
<div className="panelColumn">

View File

@ -725,7 +725,23 @@ export const EdgeRoundIcon = React.memo(
),
);
export const ArrowArrowheadIcon = React.memo(
export const ArrowheadNoneIcon = React.memo(
({ appearance }: { appearance: "light" | "dark" }) =>
createIcon(
<path
d="M6 10H34"
stroke={iconFillColor(appearance)}
strokeWidth={2}
fill="none"
/>,
{
width: 40,
height: 20,
},
),
);
export const ArrowheadArrowIcon = React.memo(
({
appearance,
flip = false,
@ -743,6 +759,48 @@ export const ArrowArrowheadIcon = React.memo(
<path d="M34 10H6M34 10L27 5M34 10L27 15" />
<path d="M27.5 5L34.5 10L27.5 15" />
</g>,
{ width: 40, height: 20, mirror: true },
{ width: 40, height: 20 },
),
);
export const ArrowheadDotIcon = React.memo(
({
appearance,
flip = false,
}: {
appearance: "light" | "dark";
flip?: boolean;
}) =>
createIcon(
<g
stroke={iconFillColor(appearance)}
fill={iconFillColor(appearance)}
transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}
>
<path d="M32 10L6 10" strokeWidth={2} />
<circle r="4" transform="matrix(-1 0 0 1 30 10)" />
</g>,
{ width: 40, height: 20 },
),
);
export const ArrowheadBarIcon = React.memo(
({
appearance,
flip = false,
}: {
appearance: "light" | "dark";
flip?: boolean;
}) =>
createIcon(
<g transform={flip ? "translate(40, 0) scale(-1, 1)" : ""}>
<path
d="M34 10H5.99996M34 10L34 5M34 10L34 15"
stroke={iconFillColor(appearance)}
strokeWidth={2}
fill="none"
/>
</g>,
{ width: 40, height: 20 },
),
);