diff --git a/app/client/ui2018/ColorSelect.ts b/app/client/ui2018/ColorSelect.ts index a2e94d49..894e0c70 100644 --- a/app/client/ui2018/ColorSelect.ts +++ b/app/client/ui2018/ColorSelect.ts @@ -1,6 +1,8 @@ import { darker, lighter } from "app/client/ui2018/ColorPalette"; import { colors, testId, vars } from 'app/client/ui2018/cssVars'; +import { textInput } from "app/client/ui2018/editableLabel"; import { icon } from "app/client/ui2018/icons"; +import { isValidHex } from "app/common/gutil"; import { Computed, Disposable, dom, DomArg, Observable, onKeyDown, styled } from "grainjs"; import { defaultMenuOptions, IOpenController, setPopupToCreateDom } from "popweasel"; @@ -84,7 +86,11 @@ function buildColorPicker(ctl: IOpenController, textColor: Observable, f onKeyDown({ Escape: () => { revert(); }, Enter: () => { ctl.close(); }, - }) + }), + + // Set focus when `focusout` is bubbling from a children element. This is to allow to receive + // keyboard event again after user interacted with the hex box text input. + dom.on('focusout', (ev, elem) => (ev.target !== elem) && elem.focus()), ); } @@ -150,12 +156,13 @@ class PickerComponent extends Disposable { testId(`${title}-input`), ), ), - // TODO: make it possible to type in hex value. cssHexBox( - dom.attr('value', this._color), - {readonly: true}, - dom.on('click', (ev, e) => e.select()), + this._color, + async (val) => { if (isValidHex(val)) {this._model.setValue(val); }}, testId(`${title}-hex`), + // select the hex value on click. Doing it using settimeout allows to avoid some + // sporadically losing the selection just after the click. + dom.on('click', (ev, elem) => setTimeout(() => elem.select(), 0)), ) ), title, @@ -275,7 +282,7 @@ const cssContent = styled('div', ` align-items: center; `); -const cssHexBox = styled('input', ` +const cssHexBox = styled(textInput, ` border: 1px solid #D9D9D9; border-left: none; font-size: ${vars.smallFontSize}; @@ -285,6 +292,8 @@ const cssHexBox = styled('input', ` width: 56px; outline: none; padding: 0 3px; + height: unset; + border-radius: unset; `); const cssLightBorder = styled('div', ` diff --git a/app/client/ui2018/editableLabel.ts b/app/client/ui2018/editableLabel.ts index 44b05ca0..e5762217 100644 --- a/app/client/ui2018/editableLabel.ts +++ b/app/client/ui2018/editableLabel.ts @@ -10,7 +10,7 @@ * TODO: Consider merging this into grainjs's input widget. */ import { colors } from 'app/client/ui2018/cssVars'; -import { dom, DomElementArg, styled } from 'grainjs'; +import { dom, DomArg, styled } from 'grainjs'; import { Observable } from 'grainjs'; import noop = require('lodash/noop'); @@ -69,7 +69,7 @@ type SaveFunc = (value: string) => Promise; * cancels editing. Label grows in size with typed input. Validation logic (if any) should happen in * the save function, to reject a value simply throw an error, this will revert to the saved one . */ -export function editableLabel(label: Observable, save: SaveFunc, ...args: DomElementArg[]) { +export function editableLabel(label: Observable, save: SaveFunc, ...args: Array>) { let input: HTMLInputElement; let sizer: HTMLSpanElement; @@ -92,7 +92,7 @@ export function editableLabel(label: Observable, save: SaveFunc, ...args * of focus. Escape cancels editing. Validation logic (if any) should happen in the save function, * to reject a value simply throw an error, this will revert to the the saved one. */ -export function textInput(label: Observable, save: SaveFunc, ...args: DomElementArg[]) { +export function textInput(label: Observable, save: SaveFunc, ...args: Array>) { return rawTextInput(label, save, noop, dom.cls(cssTextInput.className), ...args); } @@ -100,7 +100,7 @@ export function textInput(label: Observable, save: SaveFunc, ...args: Do * A helper that implements all the saving logic for both editableLabel and textInput. */ export function rawTextInput(value: Observable, save: SaveFunc, onChange: () => void, - ...args: DomElementArg[]) { + ...args: Array>) { let status: Status = Status.NORMAL; let inputEl: HTMLInputElement; diff --git a/app/common/gutil.ts b/app/common/gutil.ts index 3b7d2156..cf8b670c 100644 --- a/app/common/gutil.ts +++ b/app/common/gutil.ts @@ -777,6 +777,15 @@ export function isColorDark(hexColor: string, isDarkBelow: number = 220): boolea return luma < isDarkBelow; } +/** + * Returns true if val is a valid hex color value. For instance: #aabbaa is valid, #aabba is not. Do + * not accept neither short notation nor hex with transparency, ie: #aab, #aabb and #aabbaabb are + * invalid. + */ +export function isValidHex(val: string): boolean { + return /^#([0-9A-F]{6})$/i.test(val); +} + /** * Returns a promise that resolves to true if promise takes longer than timeoutMsec to resolve. If not