refactor(color_input): migrate ColorInput to a function component (#33363)

* feat(color_input): migrate ColorInput to function component

* test(color_input): migrate tests from enzyme to react testing library

* test(color_input): remove obsolete snapshots

* test(color_input): update snapshots

* test(color_input): update test

* refactor(color_input): remove unnecessary state update in function body

* Revert "refactor(color_input): remove unnecessary state update in function body"

This reverts commit 2c7647a3e4.

* Fix ColorInput tests

* Simplify click outside handler

By changing the button to always show the picker instead of toggling it
and making it so that the click outside handler checks for clicks
outside of the whole ColorInput, we can get rid of the setTimeout and
the very specific timing needed for the click outside handler.

* Wrap handleColorChange in useCallback

* Update snapshot

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
This commit is contained in:
Vicktor 2026-05-06 21:45:20 +03:00 committed by GitHub
parent e898ccdf3d
commit 3b86b9e14a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 222 additions and 692 deletions

View file

@ -1,479 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`components/ColorInput should match snapshot, click on picker 1`] = `
<div>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxlength="7"
type="text"
value="#ffffff"
/>
<span
class="input-group-addon color-pad"
id="sidebarBg-squareColorIcon"
>
<i
class="color-icon"
id="sidebarBg-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
<div
class="color-popover"
id="sidebarBg-ChromePickerModal"
>
<div
class="chrome-picker "
style="width: 225px; background: rgb(255, 255, 255); border-radius: 2px; box-shadow: 0 0 2px rgba(0,0,0,.3), 0 4px 8px rgba(0,0,0,.3); box-sizing: initial; font-family: Menlo;"
>
<div
style="width: 100%; padding-bottom: 55%; position: relative; border-radius: 2px 2px 0 0; overflow: hidden;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; background: rgb(255, 0, 0);"
>
<style>
.saturation-white {
background: -webkit-linear-gradient(to right, #fff, rgba(255,255,255,0));
background: linear-gradient(to right, #fff, rgba(255,255,255,0));
}
.saturation-black {
background: -webkit-linear-gradient(to top, #000, rgba(0,0,0,0));
background: linear-gradient(to top, #000, rgba(0,0,0,0));
}
</style>
<div
class="saturation-white"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
>
<div
class="saturation-black"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
/>
<div
style="position: absolute; top: 0%; left: 0%; cursor: default;"
>
<div
style="width: 12px; height: 12px; border-radius: 6px; box-shadow: inset 0 0 0 1px #fff; -webkit-transform: translate(-6px, -6px); transform: translate(-6px, -6px);"
/>
</div>
</div>
</div>
</div>
<div
style="padding: 16px 16px 12px;"
>
<div
class="flexbox-fix"
style="display: flex;"
>
<div
style="width: 22px;"
>
<div
style="margin-top: 0px; width: 10px; height: 10px; border-radius: 8px; position: relative; overflow: hidden;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); background: rgb(255, 255, 255); z-index: 2;"
/>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
/>
</div>
</div>
<div
style="flex: 1 1 0%;"
>
<div
style="height: 10px; position: relative; margin-bottom: 0px;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
>
<div
class="hue-horizontal"
style="padding: 0px 2px; position: relative; height: 100%;"
>
<style>
.hue-horizontal {
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0
33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
background: -webkit-linear-gradient(to right, #f00 0%, #ff0
17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.hue-vertical {
background: linear-gradient(to top, #f00 0%, #ff0 17%, #0f0 33%,
#0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
background: -webkit-linear-gradient(to top, #f00 0%, #ff0 17%,
#0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
</style>
<div
style="position: absolute; left: 0%;"
>
<div
style="width: 12px; height: 12px; border-radius: 6px; -webkit-transform: translate(-6px, -1px); transform: translate(-6px, -1px); background-color: rgb(248, 248, 248); box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);"
/>
</div>
</div>
</div>
</div>
<div
style="height: 10px; position: relative; display: none;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; overflow: hidden;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
/>
</div>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; background: linear-gradient(to right, rgba(255,255,255, 0) 0%, rgba(255,255,255, 1) 100%);"
/>
<div
style="position: relative; height: 100%; margin: 0px 3px;"
>
<div
style="position: absolute; left: 100%;"
>
<div
style="width: 12px; height: 12px; border-radius: 6px; -webkit-transform: translate(-6px, -1px); transform: translate(-6px, -1px); background-color: rgb(248, 248, 248); box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="flexbox-fix"
style="padding-top: 16px; display: flex;"
>
<div
class="flexbox-fix"
style="flex: 1 1 0%; display: flex; margin-left: -6px;"
>
<div
style="padding-left: 6px; width: 100%;"
>
<div
style="position: relative;"
>
<input
id="rc-editable-input-3"
spellcheck="false"
style="font-size: 11px; color: rgb(51, 51, 51); width: 100%; border-radius: 2px; box-shadow: inset 0 0 0 1px #dadada; height: 21px; text-align: center;"
value="#FFFFFF"
/>
<label
for="rc-editable-input-3"
style="text-transform: uppercase; font-size: 11px; line-height: 11px; color: rgb(150, 150, 150); text-align: center; display: block; margin-top: 12px;"
>
hex
</label>
</div>
</div>
</div>
<div
style="width: 32px; text-align: right; position: relative;"
>
<div
style="margin-right: -4px; margin-top: 12px; cursor: pointer; position: relative;"
>
<svg
style="fill: #333; width: 24px; height: 24px; border: 1px solid transparent; border-radius: 5px;"
viewBox="0 0 24 24"
>
<path
d="M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/ColorInput should match snapshot, init 1`] = `
<div>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxlength="7"
type="text"
value="#ffffff"
/>
<span
class="input-group-addon color-pad"
id="sidebarBg-squareColorIcon"
>
<i
class="color-icon"
id="sidebarBg-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
</div>
</div>
`;
exports[`components/ColorInput should match snapshot, opened 1`] = `
<div>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxlength="7"
type="text"
value="#ffffff"
/>
<span
class="input-group-addon color-pad"
id="sidebarBg-squareColorIcon"
>
<i
class="color-icon"
id="sidebarBg-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
<div
class="color-popover"
id="sidebarBg-ChromePickerModal"
>
<div
class="chrome-picker "
style="width: 225px; background: rgb(255, 255, 255); border-radius: 2px; box-shadow: 0 0 2px rgba(0,0,0,.3), 0 4px 8px rgba(0,0,0,.3); box-sizing: initial; font-family: Menlo;"
>
<div
style="width: 100%; padding-bottom: 55%; position: relative; border-radius: 2px 2px 0 0; overflow: hidden;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; background: rgb(255, 0, 0);"
>
<style>
.saturation-white {
background: -webkit-linear-gradient(to right, #fff, rgba(255,255,255,0));
background: linear-gradient(to right, #fff, rgba(255,255,255,0));
}
.saturation-black {
background: -webkit-linear-gradient(to top, #000, rgba(0,0,0,0));
background: linear-gradient(to top, #000, rgba(0,0,0,0));
}
</style>
<div
class="saturation-white"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
>
<div
class="saturation-black"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
/>
<div
style="position: absolute; top: 0%; left: 0%; cursor: default;"
>
<div
style="width: 12px; height: 12px; border-radius: 6px; box-shadow: inset 0 0 0 1px #fff; -webkit-transform: translate(-6px, -6px); transform: translate(-6px, -6px);"
/>
</div>
</div>
</div>
</div>
<div
style="padding: 16px 16px 12px;"
>
<div
class="flexbox-fix"
style="display: flex;"
>
<div
style="width: 22px;"
>
<div
style="margin-top: 0px; width: 10px; height: 10px; border-radius: 8px; position: relative; overflow: hidden;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); background: rgb(255, 255, 255); z-index: 2;"
/>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
/>
</div>
</div>
<div
style="flex: 1 1 0%;"
>
<div
style="height: 10px; position: relative; margin-bottom: 0px;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
>
<div
class="hue-horizontal"
style="padding: 0px 2px; position: relative; height: 100%;"
>
<style>
.hue-horizontal {
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0
33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
background: -webkit-linear-gradient(to right, #f00 0%, #ff0
17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.hue-vertical {
background: linear-gradient(to top, #f00 0%, #ff0 17%, #0f0 33%,
#0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
background: -webkit-linear-gradient(to top, #f00 0%, #ff0 17%,
#0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
</style>
<div
style="position: absolute; left: 0%;"
>
<div
style="width: 12px; height: 12px; border-radius: 6px; -webkit-transform: translate(-6px, -1px); transform: translate(-6px, -1px); background-color: rgb(248, 248, 248); box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);"
/>
</div>
</div>
</div>
</div>
<div
style="height: 10px; position: relative; display: none;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; overflow: hidden;"
>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px;"
/>
</div>
<div
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; background: linear-gradient(to right, rgba(255,255,255, 0) 0%, rgba(255,255,255, 1) 100%);"
/>
<div
style="position: relative; height: 100%; margin: 0px 3px;"
>
<div
style="position: absolute; left: 100%;"
>
<div
style="width: 12px; height: 12px; border-radius: 6px; -webkit-transform: translate(-6px, -1px); transform: translate(-6px, -1px); background-color: rgb(248, 248, 248); box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);"
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="flexbox-fix"
style="padding-top: 16px; display: flex;"
>
<div
class="flexbox-fix"
style="flex: 1 1 0%; display: flex; margin-left: -6px;"
>
<div
style="padding-left: 6px; width: 100%;"
>
<div
style="position: relative;"
>
<input
id="rc-editable-input-1"
spellcheck="false"
style="font-size: 11px; color: rgb(51, 51, 51); width: 100%; border-radius: 2px; box-shadow: inset 0 0 0 1px #dadada; height: 21px; text-align: center;"
value="#FFFFFF"
/>
<label
for="rc-editable-input-1"
style="text-transform: uppercase; font-size: 11px; line-height: 11px; color: rgb(150, 150, 150); text-align: center; display: block; margin-top: 12px;"
>
hex
</label>
</div>
</div>
</div>
<div
style="width: 32px; text-align: right; position: relative;"
>
<div
style="margin-right: -4px; margin-top: 12px; cursor: pointer; position: relative;"
>
<svg
style="fill: #333; width: 24px; height: 24px; border: 1px solid transparent; border-radius: 5px;"
viewBox="0 0 24 24"
>
<path
d="M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`components/ColorInput should match snapshot, toggle picker 1`] = `
<div>
<div
class="color-input input-group"
>
<input
class="form-control"
data-testid="color-inputColorValue"
id="sidebarBg-inputColorValue"
maxlength="7"
type="text"
value="#ffffff"
/>
<span
class="input-group-addon color-pad"
id="sidebarBg-squareColorIcon"
>
<i
class="color-icon"
id="sidebarBg-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
</span>
</div>
</div>
`;

View file

@ -28,10 +28,12 @@ exports[`components/ColorSetting should match snapshot, all 1`] = `
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="id-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="id-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -76,10 +78,12 @@ exports[`components/ColorSetting should match snapshot, clicked on color setting
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="id-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="id-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -161,10 +165,12 @@ exports[`components/ColorSetting should match snapshot, no help text 1`] = `
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="id-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="id-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>

View file

@ -1,11 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback, useState} from 'react';
import {render, screen, fireEvent, userEvent} from 'tests/react_testing_utils';
import {renderWithContext, userEvent, screen, act} from 'tests/react_testing_utils';
import ColorInput from './color_input';
import ColorInput, {type ColorInputProps} from './color_input';
function ColorInputWrapper({onChange, value: initialValue, ...otherProps}: ColorInputProps) {
const [value, setValue] = useState(initialValue);
const handleChange = useCallback((value: string) => {
setValue(value);
onChange(value);
}, [onChange]);
return (
<ColorInput
value={value}
onChange={handleChange}
{...otherProps}
/>
);
}
describe('components/ColorInput', () => {
const baseProps = {
@ -14,105 +31,68 @@ describe('components/ColorInput', () => {
value: '#ffffff',
};
test('should match snapshot, init', () => {
const {container} = render(
<ColorInput {...baseProps}/>,
);
test('should hide color picker when first rendered', () => {
const {getByTestId} = renderWithContext(<ColorInputWrapper {...baseProps}/>);
expect(container).toMatchSnapshot();
const inputElement = getByTestId('color-inputColorValue');
expect(inputElement).toBeInTheDocument();
expect(inputElement).toBeVisible();
expect(screen.queryByTestId('color-popover')).not.toBeInTheDocument();
});
test('should match snapshot, opened', async () => {
const {container} = render(
<ColorInput {...baseProps}/>,
);
test('should show color picker when color picker button is clicked', async () => {
const {getByTestId} = renderWithContext(<ColorInputWrapper {...baseProps}/>);
await userEvent.click(container.querySelector('.input-group-addon')!);
const colorPickerToggleButton = getByTestId('color-togglerButton');
expect(container).toMatchSnapshot();
await userEvent.click(colorPickerToggleButton);
const colorPopover = getByTestId('color-popover');
expect(colorPopover).toBeInTheDocument();
expect(colorPopover).toBeVisible();
expect(document.activeElement).toBe(getByTestId('color-inputColorValue'));
await userEvent.click(document.body);
expect(screen.queryByTestId('color-popover')).not.toBeInTheDocument();
});
test('should match snapshot, toggle picker', async () => {
const {container} = render(
<ColorInput {...baseProps}/>,
);
await userEvent.click(container.querySelector('.input-group-addon')!);
await userEvent.click(container.querySelector('.input-group-addon')!);
test('should change color when the color picker is clicked', async () => {
const {getByTestId} = renderWithContext(<ColorInputWrapper {...baseProps}/>);
expect(container).toMatchSnapshot();
});
await userEvent.click(getByTestId('color-togglerButton'));
test('should match snapshot, click on picker', async () => {
const {container} = render(
<ColorInput {...baseProps}/>,
);
const colorPopover = getByTestId('color-popover');
await userEvent.click(container.querySelector('.input-group-addon')!);
await userEvent.click(container.querySelector('.color-popover')!);
await userEvent.click(colorPopover);
expect(container).toMatchSnapshot();
});
test('should have match state on togglePicker', async () => {
const {container} = render(
<ColorInput {...baseProps}/>,
);
// Initially picker should be closed (no color-popover)
expect(container.querySelector('.color-popover')).not.toBeInTheDocument();
// Click to open
await userEvent.click(container.querySelector('.input-group-addon')!);
expect(container.querySelector('.color-popover')).toBeInTheDocument();
// Click to close
await userEvent.click(container.querySelector('.input-group-addon')!);
expect(container.querySelector('.color-popover')).not.toBeInTheDocument();
// Click to open again
await userEvent.click(container.querySelector('.input-group-addon')!);
expect(container.querySelector('.color-popover')).toBeInTheDocument();
expect(colorPopover).toBeInTheDocument();
expect(baseProps.onChange).toHaveBeenCalledTimes(1);
});
test('should keep what the user types in the textbox until blur', async () => {
let currentValue = '#ffffff';
const onChange = jest.fn((value: string) => {
currentValue = value;
const {getByTestId} = renderWithContext(<ColorInputWrapper{...baseProps}/>);
const inputElement = getByTestId('color-inputColorValue');
await userEvent.clear(inputElement);
await userEvent.type(inputElement, '#abc');
expect(inputElement).toHaveValue('#abc');
// The RGB here is the equivalent of '#abc'.
expect(getByTestId('color-icon').style.backgroundColor).toBe('rgb(170, 187, 204)');
await act(() => {
inputElement.blur();
});
const {container, rerender} = render(
<ColorInput
{...baseProps}
value={currentValue}
onChange={onChange}
/>,
);
expect(document.activeElement).not.toBe(inputElement);
expect(inputElement).toHaveValue('#aabbcc');
const input = screen.getByRole('textbox');
const colorIcon = container.querySelector('.color-icon') as HTMLElement;
// Simulate focus on input - fireEvent used because userEvent doesn't have direct focus/blur methods
fireEvent.focus(input);
await userEvent.clear(input);
await userEvent.type(input, '#abc');
expect(onChange).toHaveBeenLastCalledWith('#aabbcc');
expect(input).toHaveValue('#abc');
expect(colorIcon.style.backgroundColor).toBe('rgb(170, 187, 204)');
// Rerender with updated value prop (simulating parent component update)
rerender(
<ColorInput
{...baseProps}
value={currentValue}
onChange={onChange}
/>,
);
fireEvent.blur(input);
// After blur, the input should show the normalized value
expect(input).toHaveValue('#aabbcc');
expect(colorIcon.style.backgroundColor).toBe('rgb(170, 187, 204)');
// The RGB value passed in the assertion is the equivalent of '#aabbcc'.
expect(getByTestId('color-icon').style.backgroundColor).toBe('rgb(170, 187, 204)');
});
});

View file

@ -1,184 +1,159 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {ChromePicker} from 'react-color';
import type {ColorResult} from 'react-color';
import tinycolor from 'tinycolor2';
type Props = {
export interface ColorInputProps {
id: string;
onChange: (color: string) => void;
value: string;
isDisabled?: boolean;
}
type State = {
focused: boolean;
isOpened: boolean;
value: string;
}
const ColorInput = ({
id,
onChange: onChangeFromProps,
value: valueFromProps,
isDisabled,
}: ColorInputProps) => {
const container = useRef<HTMLDivElement>(null);
const colorInput = useRef<HTMLInputElement>(null);
export default class ColorInput extends React.PureComponent<Props, State> {
private colorPicker: React.RefObject<HTMLDivElement>;
private colorInput: React.RefObject<HTMLInputElement>;
const [isFocused, setIsFocused] = useState(false);
const [isOpened, setIsOpened] = useState(false);
const [valueFromState, setValueFromState] = useState(valueFromProps);
public constructor(props: Props) {
super(props);
this.colorPicker = React.createRef();
this.colorInput = React.createRef();
this.state = {
focused: false,
isOpened: false,
value: props.value,
};
if (!isFocused && valueFromProps !== valueFromState) {
setValueFromState(valueFromProps);
}
static getDerivedStateFromProps(props: Props, state: State) {
if (!state.focused && props.value !== state.value) {
return {
value: props.value,
};
useEffect(() => {
if (!isOpened) {
return () => {};
}
return null;
}
public componentDidUpdate(prevProps: Props, prevState: State) {
const {isOpened: prevIsOpened} = prevState;
const {isOpened} = this.state;
if (isOpened !== prevIsOpened) {
if (isOpened) {
document.addEventListener('click', this.checkClick, {capture: true});
} else {
document.removeEventListener('click', this.checkClick);
const checkClick = (e: MouseEvent): void => {
if (!container.current || !container.current.contains(e.target as Element)) {
setIsOpened(false);
}
}
}
};
private checkClick = (e: MouseEvent): void => {
if (!this.colorPicker.current || !this.colorPicker.current.contains(e.target as Element)) {
this.setState({isOpened: false});
}
document.addEventListener('mousedown', checkClick);
return () => {
document.removeEventListener('mousedown', checkClick);
};
}, [isOpened]);
const togglePicker = () => {
colorInput.current?.focus();
setIsOpened(true);
};
private togglePicker = () => {
if (!this.state.isOpened && this.colorInput.current) {
this.colorInput.current.focus();
}
this.setState({isOpened: !this.state.isOpened});
};
const handleColorChange = useCallback((newColorData: ColorResult) => {
setIsFocused(false);
onChangeFromProps(newColorData.hex);
}, [onChangeFromProps]);
public handleColorChange = (newColorData: ColorResult) => {
this.setState({focused: false});
this.props.onChange(newColorData.hex);
};
private onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const color = tinycolor(value);
const normalizedColor = '#' + color.toHex();
if (color.isValid()) {
this.props.onChange(normalizedColor);
onChangeFromProps(normalizedColor);
}
this.setState({value});
setValueFromState(value);
};
private onFocus = (event: React.FocusEvent<HTMLInputElement>): void => {
this.setState({
focused: true,
});
const onFocus = (event: React.FocusEvent<HTMLInputElement>): void => {
setIsFocused(true);
if (event.target) {
event.target.setSelectionRange(1, event.target.value.length);
}
};
private onBlur = () => {
const value = this.state.value;
const onBlur = () => {
const value = valueFromState;
const color = tinycolor(value);
const normalizedColor = '#' + color.toHex();
if (color.isValid()) {
this.props.onChange(normalizedColor);
onChangeFromProps(normalizedColor);
this.setState({
value: normalizedColor,
});
setValueFromState(normalizedColor);
} else {
this.setState({
value: this.props.value,
});
setValueFromState(valueFromProps);
}
this.setState({
focused: false,
});
setIsFocused(false);
};
private onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
// open picker on enter or space
if (event.key === 'Enter' || event.key === ' ') {
this.togglePicker();
togglePicker();
}
};
public render() {
const {id} = this.props;
const {isOpened, value} = this.state;
return (
<div
className='color-input input-group'
ref={container}
>
<input
id={`${id}-inputColorValue`}
ref={colorInput}
className='form-control'
type='text'
value={valueFromState}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
onKeyDown={onKeyDown}
maxLength={7}
disabled={isDisabled}
data-testid='color-inputColorValue'
/>
{!isDisabled &&
<span
id={`${id}-squareColorIcon`}
className='input-group-addon color-pad'
onClick={togglePicker}
data-testid='color-togglerButton'
>
<i
id={`${id}-squareColorIconValue`}
className='color-icon'
data-testid='color-icon'
style={{
backgroundColor: valueFromState,
}}
/>
</span>
}
{isOpened && (
<div
className='color-popover'
id={`${id}-ChromePickerModal`}
data-testid='color-popover'
>
<ChromePicker
color={valueFromState}
onChange={handleColorChange}
disableAlpha={true}
/>
</div>
)}
</div>
);
};
return (
<div className='color-input input-group'>
<input
id={`${id}-inputColorValue`}
ref={this.colorInput}
className='form-control'
type='text'
value={value}
onChange={this.onChange}
onBlur={this.onBlur}
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}
maxLength={7}
disabled={this.props.isDisabled}
data-testid='color-inputColorValue'
/>
{!this.props.isDisabled &&
<span
id={`${id}-squareColorIcon`}
className='input-group-addon color-pad'
onClick={this.togglePicker}
>
<i
id={`${id}-squareColorIconValue`}
className='color-icon'
style={{
backgroundColor: value,
}}
/>
</span>
}
{isOpened && (
<div
ref={this.colorPicker}
className='color-popover'
id={`${id}-ChromePickerModal`}
>
<ChromePicker
color={value}
onChange={this.handleColorChange}
disableAlpha={true}
/>
</div>
)}
</div>
);
}
}
export default React.memo(ColorInput);

View file

@ -21,10 +21,12 @@ exports[`components/user_settings/display/ColorChooser should match, init 1`] =
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="choose-color-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="choose-color-squareColorIconValue"
style="background-color: rgb(255, 238, 192);"
/>

View file

@ -63,10 +63,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarBg-squareColorIconValue"
style="background-color: rgb(30, 50, 92);"
/>
@ -95,10 +97,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarText-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarText-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -127,10 +131,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarHeaderBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarHeaderBg-squareColorIconValue"
style="background-color: rgb(25, 42, 77);"
/>
@ -159,10 +165,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarTeamBarBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarTeamBarBg-squareColorIconValue"
style="background-color: rgb(22, 37, 69);"
/>
@ -191,10 +199,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarHeaderTextColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarHeaderTextColor-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -223,10 +233,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarUnreadText-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarUnreadText-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -255,10 +267,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarTextHoverBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarTextHoverBg-squareColorIconValue"
style="background-color: rgb(40, 66, 123);"
/>
@ -287,10 +301,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarTextActiveBorder-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarTextActiveBorder-squareColorIconValue"
style="background-color: rgb(93, 137, 234);"
/>
@ -319,10 +335,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="sidebarTextActiveColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="sidebarTextActiveColor-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -351,10 +369,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="onlineIndicator-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="onlineIndicator-squareColorIconValue"
style="background-color: rgb(61, 184, 135);"
/>
@ -383,10 +403,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="awayIndicator-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="awayIndicator-squareColorIconValue"
style="background-color: rgb(255, 188, 31);"
/>
@ -415,10 +437,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="dndIndicator-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="dndIndicator-squareColorIconValue"
style="background-color: rgb(210, 75, 78);"
/>
@ -447,10 +471,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="mentionBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="mentionBg-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -479,10 +505,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="mentionColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="mentionColor-squareColorIconValue"
style="background-color: rgb(30, 50, 92);"
/>
@ -547,10 +575,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="centerChannelBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="centerChannelBg-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>
@ -579,10 +609,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="centerChannelColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="centerChannelColor-squareColorIconValue"
style="background-color: rgb(63, 67, 80);"
/>
@ -611,10 +643,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="newMessageSeparator-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="newMessageSeparator-squareColorIconValue"
style="background-color: rgb(204, 143, 0);"
/>
@ -643,10 +677,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="errorTextColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="errorTextColor-squareColorIconValue"
style="background-color: rgb(210, 75, 78);"
/>
@ -675,10 +711,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="mentionHighlightBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="mentionHighlightBg-squareColorIconValue"
style="background-color: rgb(255, 212, 112);"
/>
@ -707,10 +745,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="mentionHighlightLink-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="mentionHighlightLink-squareColorIconValue"
style="background-color: rgb(27, 29, 34);"
/>
@ -824,10 +864,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="linkColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="linkColor-squareColorIconValue"
style="background-color: rgb(56, 111, 229);"
/>
@ -856,10 +898,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="buttonBg-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="buttonBg-squareColorIconValue"
style="background-color: rgb(28, 88, 217);"
/>
@ -888,10 +932,12 @@ exports[`components/user_settings/display/CustomThemeChooser should match, init
/>
<span
class="input-group-addon color-pad"
data-testid="color-togglerButton"
id="buttonColor-squareColorIcon"
>
<i
class="color-icon"
data-testid="color-icon"
id="buttonColor-squareColorIconValue"
style="background-color: rgb(255, 255, 255);"
/>