Skip to content

Commit

Permalink
Implement editing of domain bounds
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed Mar 11, 2021
1 parent 25133bc commit 0530da1
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 118 deletions.
10 changes: 4 additions & 6 deletions src/h5web/toolbar/Toolbar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@
padding: 0 0.25rem;
}

.btn:disabled,
.btn[aria-disabled='true'] {
opacity: 0.5;
pointer-events: none;
}

.btn[data-small] {
padding: 0 0.125rem;
}
Expand Down Expand Up @@ -88,6 +82,10 @@
box-shadow: var(--btn-shadow-idle) var(--btn-shadow-color);
}

.btn:active > .btnLike {
box-shadow: var(--btn-shadow-pressed) var(--btn-shadow-color);
}

.btn[data-raised]:hover > .btnLike {
box-shadow: var(--btn-shadow-raised) var(--btn-shadow-color),
var(--btn-shadow-idle) var(--btn-shadow-color);
Expand Down
78 changes: 78 additions & 0 deletions src/h5web/toolbar/controls/DomainSlider/BoundEditor.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.boundEditor {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
font-weight: bold;
}

.label {
width: 2.5em;
margin: 0 1rem 0 0;
text-transform: uppercase;
font-size: inherit;
color: var(--near-black);
}

.value {
flex: 1 1 0%;
width: 0; /* ensures this input shrinks before filling the available space */
height: 1.875rem; /* maintain height regardless of font size */
margin-right: 0.375rem;
padding: 0.25rem 0.375rem;
background-color: rgba(255, 255, 255, 0.5);
border: 1px solid transparent;
border-radius: 0.125rem;
box-shadow: 0 0 2px var(--dark-slate-gray);
text-align: right;
color: var(--near-black);
font-weight: inherit;
line-height: inherit;
transition: background-color 0.05s ease-in-out, box-shadow 0.05s ease-in-out;
cursor: text;
}

.value:hover {
box-shadow: 1px 1px 2px 1px var(--dark-gray);
}

.value:focus {
box-shadow: 1px 1px 2px 1px var(--secondary-dark);
outline: none;
}

.boundEditor[data-error] .label,
.boundEditor[data-error] .value {
color: var(--warn);
}

.boundEditor[data-editing='true'] .value {
background-color: var(--white);
border-color: var(--secondary-dark);
outline: none;
font-weight: 600;
font-size: 0.9375em;
line-height: calc(1.2 / 0.9375);
}

.boundEditor[data-editing='true'] .value:hover {
box-shadow: 1px 1px 2px 1px var(--secondary-dark);
}

.action {
composes: btn-clean from global;
display: flex;
align-items: center;
padding: 0.25rem;
font-size: 1.125em;
border-radius: 0.5rem;
transition: background-color 0.05s ease-in-out, box-shadow 0.05s ease-in-out;
}

.action:hover {
background-color: var(--secondary-light);
box-shadow: var(--btn-shadow-idle) var(--btn-shadow-color);
}

.action:active {
box-shadow: var(--btn-shadow-pressed) var(--btn-shadow-color);
}
85 changes: 85 additions & 0 deletions src/h5web/toolbar/controls/DomainSlider/BoundEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ReactElement, useEffect, useRef, useState } from 'react';
import { FiCheck, FiSlash } from 'react-icons/fi';
import { formatValue } from '../../../utils';
import styles from './BoundEditor.module.css';

interface Props {
label: string;
value: number;
isEditing: boolean;
hasError: boolean;
onEditToggle: (force: boolean) => void;
onChange: (val: number) => void;
}

function BoundEditor(props: Props): ReactElement {
const { label, value, isEditing, hasError, onEditToggle, onChange } = props;

const id = `${label}-bound`;
const inputRef = useRef<HTMLInputElement>(null);

const [inputValue, setInputValue] = useState(formatValue(value));

useEffect(() => {
setInputValue(isEditing ? value.toString() : formatValue(value));
}, [isEditing, value, setInputValue]);

useEffect(() => {
if (!isEditing) {
// Remove focus from min field when editing is turned off
inputRef.current?.blur();
}

if (isEditing && label === 'Min') {
// Give focus to min field when opening tooltip in edit mode
inputRef.current?.focus();
}
}, [isEditing, label]);

return (
<form
className={styles.boundEditor}
data-error={hasError || undefined}
data-editing={isEditing}
onSubmit={(evt) => {
evt.preventDefault();
onChange(Number.parseFloat(inputValue));
onEditToggle(false);
}}
>
<label id={`${id}-label`} className={styles.label} htmlFor={id}>
<abbr title={value.toString()}>{label}</abbr>
</label>

<input
id={id}
ref={inputRef}
className={styles.value}
type="text"
name="bound"
value={inputValue}
aria-labelledby={`${id}-label`} // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/718
onChange={(evt) => setInputValue(evt.target.value)}
onFocus={() => {
if (!isEditing) {
onEditToggle(true);
}
}}
/>

<button className={styles.action} type="submit" disabled={!isEditing}>
<FiCheck>Apply</FiCheck>
</button>
<button
className={styles.action}
type="button"
disabled={!isEditing}
onClick={() => onEditToggle(false)}
>
<FiSlash>Cancel</FiSlash>
</button>
</form>
);
}

export default BoundEditor;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import { FiCornerDownRight } from 'react-icons/fi';
import { Bound, BoundError } from '../../../vis-packs/core/models';
import styles from './DomainSlider.module.css';
import styles from './DomainTooltip.module.css';

interface Props {
bound: Bound;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
.slider {
display: flex;
width: 8rem;
margin-right: 0.25rem;
margin-right: -0.25rem;
font-size: 0.75rem;
cursor: pointer;
}
Expand Down
86 changes: 49 additions & 37 deletions src/h5web/toolbar/controls/DomainSlider/DomainSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
import { ReactElement, useEffect, useState } from 'react';
import { ReactElement, useEffect, useRef, useState } from 'react';
import styles from './DomainSlider.module.css';
import type {
CustomDomain,
Domain,
ScaleType,
} from '../../../vis-packs/core/models';
import ToggleBtn from '../ToggleBtn';
import { FiZap } from 'react-icons/fi';
import { useKey, useToggle } from 'react-use';
import { MdTune } from 'react-icons/md';
import { useClickAway, useKey, useToggle } from 'react-use';
import DomainTooltip from './DomainTooltip';
import ScaledSlider from './ScaledSlider';
import { useVisDomain } from '../../../vis-packs/core/heatmap/hooks';
import {
useSafeDomain,
useVisDomain,
} from '../../../vis-packs/core/heatmap/hooks';

const TOOLTIP_ID = 'domain-tooltip';

function getAutoscaleLabel(isAutoMin: boolean, isAutoMax: boolean): string {
if (isAutoMin && !isAutoMax) {
return 'Min';
}

if (isAutoMax && !isAutoMin) {
return 'Max';
}

return 'Auto';
}

interface Props {
dataDomain: Domain;
customDomain: CustomDomain;
Expand All @@ -38,38 +29,55 @@ function DomainSlider(props: Props): ReactElement {
const { dataDomain, customDomain, scaleType, disabled } = props;
const { onCustomDomainChange } = props;

const [visDomain, errors] = useVisDomain(dataDomain, customDomain, scaleType);
const [sliderDomain, setSliderDomain] = useState(visDomain);
const visDomain = useVisDomain(customDomain, dataDomain);
const [safeDomain, errors] = useSafeDomain(visDomain, dataDomain, scaleType);

const [sliderDomain, setSliderDomain] = useState(visDomain);
useEffect(() => {
setSliderDomain(visDomain);
}, [visDomain, setSliderDomain]);

const isAutoMin = customDomain[0] === undefined;
const isAutoMax = customDomain[1] === undefined;
const isAutoscaling = isAutoMin || isAutoMax;

const [tooltipOpen, toggleTooltip] = useToggle(false);
useKey('Escape', () => toggleTooltip(false));
const [hovered, toggleHovered] = useToggle(false);
const [isEditingMin, toggleEditingMin] = useToggle(false);
const [isEditingMax, toggleEditingMax] = useToggle(false);
const isEditing = isEditingMin || isEditingMax;

function toggleAll(force: boolean) {
toggleEditingMin(force);
toggleEditingMax(force);
toggleHovered(force);
}

const rootRef = useRef(null);
useClickAway(rootRef, () => toggleAll(false));
useKey('Escape', () => toggleAll(false));

return (
<div
ref={rootRef}
className={styles.root}
aria-expanded={tooltipOpen}
aria-expanded={hovered}
aria-describedby={TOOLTIP_ID}
onPointerEnter={() => toggleTooltip(true)}
onPointerLeave={() => toggleTooltip(false)}
onPointerEnter={() => toggleHovered(true)}
onPointerLeave={() => toggleHovered(false)}
>
<ScaledSlider
value={sliderDomain}
safeVisDomain={safeDomain}
dataDomain={dataDomain}
visDomain={visDomain}
scaleType={scaleType}
errors={errors}
disabled={disabled}
isAutoMin={isAutoMin}
isAutoMax={isAutoMax}
onChange={setSliderDomain}
onChange={(newValue) => {
setSliderDomain(newValue);
toggleEditingMin(false);
toggleEditingMax(false);
}}
onAfterChange={(hasMinChanged, hasMaxChanged) => {
onCustomDomainChange([
hasMinChanged ? sliderDomain[0] : customDomain[0],
Expand All @@ -79,34 +87,38 @@ function DomainSlider(props: Props): ReactElement {
/>

<ToggleBtn
small
label={getAutoscaleLabel(isAutoMin, isAutoMax)}
icon={FiZap}
value={isAutoscaling}
iconOnly
label="Edit domain"
icon={MdTune}
value={isEditing}
disabled={disabled}
onChange={() => {
onCustomDomainChange(
isAutoscaling ? dataDomain : [undefined, undefined]
);
}}
onChange={() => toggleAll(!isEditing)}
/>

<DomainTooltip
id={TOOLTIP_ID}
open={tooltipOpen}
domain={sliderDomain}
open={hovered || isEditing}
sliderDomain={sliderDomain}
dataDomain={dataDomain}
errors={errors}
isAutoMin={isAutoMin}
isAutoMax={isAutoMax}
onAutoMinToggle={() => {
const newMin = isAutoMin ? dataDomain[0] : undefined;
onCustomDomainChange([newMin, customDomain[1]]);
toggleEditingMin(isAutoMin);
}}
onAutoMaxToggle={() => {
const newMax = isAutoMax ? dataDomain[1] : undefined;
onCustomDomainChange([customDomain[0], newMax]);
toggleEditingMax(isAutoMax);
}}
isEditingMin={isEditingMin}
isEditingMax={isEditingMax}
onEditMin={toggleEditingMin}
onEditMax={toggleEditingMax}
onChangeMin={(val) => onCustomDomainChange([val, customDomain[1]])}
onChangeMax={(val) => onCustomDomainChange([customDomain[0], val])}
/>
</div>
);
Expand Down
Loading

0 comments on commit 0530da1

Please sign in to comment.