Skip to content

Commit

Permalink
feat(composer): add a11y color picker
Browse files Browse the repository at this point in the history
fix: update package-lock.json

fix: update component name
  • Loading branch information
jacksonvogt authored and mumanity committed Sep 28, 2023
1 parent a9011a8 commit e768a88
Show file tree
Hide file tree
Showing 28 changed files with 418 additions and 73 deletions.
31 changes: 23 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/scene-composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"path-browserify": "1.0.1",
"postprocessing": "6.28.5",
"react-color": "^2.19.3",
"react-colorful": "5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-markdown": "^8.0.7",
Expand All @@ -171,6 +172,7 @@
"three": "^0.139.2",
"three-mesh-bvh": "0.5.24",
"three-stdlib": "2.23.9",
"tinycolor2": "^1.6.0",
"ts-custom-error": "^3.3.1",
"tslib": "^2.5.3",
"typescript": "^4.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../../../../assets';
import { Layers } from '../../../../common/constants';
import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext';
import { traverseSvg } from '../../../../components/panels/scene-components/tag-style/ColorPicker/ColorPickerUtils/SvgParserHelper';
import { traverseSvg } from '../../../../components/panels/scene-components/tag-style/ColorSelectorCombo/ColorSelectorComboUtils/SvgParserHelper';
import useRuleResult from '../../../../hooks/useRuleResult';
import useTagSettings from '../../../../hooks/useTagSettings';
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@use '@awsui/design-tokens/index.scss' as awsui;

.color-picker .react-colorful {
width: auto
}

.color-picker .react-colorful__saturation {
border-radius: 5px;
border-bottom: none;
}

.color-picker .react-colorful__hue {
margin-top: 1.5em;
margin-bottom: 1.4em;
height: 1.5em;
border-radius: .75em;
}

.color-picker .react-colorful__hue-pointer,
.react-colorful__saturation-pointer {
width: 1.8em;
height: 1.8em;
}

.color-picker-wrapper {
width: auto;
padding: 1em;
border-radius: 12px;
background: awsui.$color-background-item-selected;
box-shadow: 0 4px 8px black;
}

.flexible-div {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: .75em 2em;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useState } from 'react';
import { debounce } from 'lodash';
import { SpaceBetween } from '@awsui/components-react';
import { useIntl } from 'react-intl';
import { ColorRepresentation } from 'three';

import {
ColorPickerWrapper,
FlexibleDiv,
validateHex,
validateRgbValue,
hexString,
isValidColor,
DEFAULT_COLOR,
updateColorWithRgb,
getRgb,
ReactColorfulPicker,
HexInputField,
RgbInputField,
} from './ColorPickerHelpers';

interface ColorPickerProps {
color: ColorRepresentation | undefined;
onChange: (newColor: string) => void;
}

export const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }: ColorPickerProps) => {
const { formatMessage } = useIntl();
const [internalColor, setInternalColor] = useState<ColorRepresentation>(color ? color : DEFAULT_COLOR);
const [hexInputValue, setHexInputValue] = useState<string>(color ? hexString(color) : hexString(DEFAULT_COLOR));

const handleColorfulChange = (newColor: ColorRepresentation) => {
setInternalColor(newColor);
setHexInputValue(hexString(newColor));
onChange(hexString(newColor));
};

const handleHexChange = (newColor: string) => {
setHexInputValue(validateHex(newColor));
if (isValidColor(newColor)) {
setInternalColor(newColor);
onChange(newColor);
}
};

const handleRGBChange = (newValue) => {
const updatedColor = updateColorWithRgb(internalColor, newValue);
setInternalColor(updatedColor);
setHexInputValue(updatedColor);
onChange(updatedColor);
};

return (
<ColorPickerWrapper>
<form onSubmit={(e) => e.preventDefault()}>
<ReactColorfulPicker color={hexString(internalColor)} onChange={debounce(handleColorfulChange, 100)} />
<FlexibleDiv>
<HexInputField
color={`#${hexInputValue.replace('#', '')}`}
onChange={(e) => handleHexChange(e.detail.value)}
label={formatMessage({ defaultMessage: 'Hex', description: 'Abbreviation for hexidecimal' })}
errorText={
!isValidColor(hexInputValue) &&
formatMessage({ defaultMessage: 'Invalid Hex', description: 'Form field error message' })
}
/>
<SpaceBetween size='xs' direction='horizontal'>
<RgbInputField
value={getRgb(internalColor).r.toString()}
label={formatMessage({ defaultMessage: 'R', description: 'Abbreviation for red' })}
onChange={(e) => handleRGBChange({ r: validateRgbValue(e.detail.value) })}
testId='rule-color-selector-red'
/>
<RgbInputField
value={getRgb(internalColor).g.toString()}
label={formatMessage({ defaultMessage: 'G', description: 'Abbreviation for green' })}
onChange={(e) => handleRGBChange({ g: validateRgbValue(e.detail.value) })}
testId='rule-color-selector-green'
/>
<RgbInputField
value={getRgb(internalColor).b.toString()}
label={formatMessage({ defaultMessage: 'B', description: 'Abbreviation for blue' })}
onChange={(e) => handleRGBChange({ b: validateRgbValue(e.detail.value) })}
testId='rule-color-selector-blue'
/>
</SpaceBetween>
</FlexibleDiv>
</form>
</ColorPickerWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react';
import { FormField, Input, InputProps } from '@awsui/components-react';
import { ColorRepresentation } from 'three';
import tinycolor from 'tinycolor2';
import './ColorPicker.scss';
import { HexColorPicker } from 'react-colorful';
import { NonCancelableEventHandler } from '@awsui/components-react/internal/events';
import { BaseChangeDetail } from '@awsui/components-react/input/interfaces';

export const DEFAULT_COLOR = new tinycolor().toHexString();

const toTinyColor = (color: ColorRepresentation) => {
return typeof color === 'number' ? new tinycolor(color.toString(16)) : new tinycolor(color);
};

export const hexString = (color: ColorRepresentation): string => {
return toTinyColor(color).toHexString();
};

export const updateColorWithRgb = (oldColor: ColorRepresentation, newValue): string => {
return new tinycolor({ ...toTinyColor(oldColor).toRgb(), ...newValue }).toHexString();
};

export const isValidColor = (color: ColorRepresentation): boolean => {
return toTinyColor(color).isValid() && toTinyColor(color).getFormat() === 'hex';
};

export const validateHex = (value: string): string => {
return `#${value.replace(/[^a-fA-F0-9]/g, '').substring(0, 6)}`;
};

export const getRgb = (color: ColorRepresentation) => {
return toTinyColor(color).toRgb();
};

export const validateRgbValue = (value: string): string => {
const maxRgbValue = 0xff; //Maximum possible value representable by 2 bytes
const onlyDigits = value.replace(/\D/g, '');
if (value === '-1') return '0'; //manually handle decreasing from 0
return parseInt(onlyDigits) > maxRgbValue ? maxRgbValue.toString() : parseInt(onlyDigits.padStart(1, '0')).toString();
};

export const ColorPickerWrapper: React.FC<React.PropsWithChildren> = ({ children }: React.PropsWithChildren) => {
return <div className='color-picker-wrapper'>{children}</div>;
};

export const FlexibleDiv: React.FC<React.PropsWithChildren> = ({ children }: React.PropsWithChildren) => {
return <div className='flexible-div'>{children}</div>;
};

interface ReactColorfulPickerProps {
color: string;
onChange: ((newColor: string) => void) | undefined;
}

export const ReactColorfulPicker: React.FC<ReactColorfulPickerProps> = ({
color,
onChange,
}: ReactColorfulPickerProps) => {
return (
<div className='color-picker'>
<HexColorPicker color={color} onChange={onChange} />
</div>
);
};

interface FixedWidthInputProps extends InputProps {
width: string;
}

const FixedWidthInput: React.FC<FixedWidthInputProps> = (props) => {
return (
<div style={{ width: props.width }} role='status'>
<Input {...props} />
</div>
);
};

interface HexInputFieldProps {
color: string;
onChange: NonCancelableEventHandler<BaseChangeDetail>;
label: string;
errorText: string | boolean;
}

export const HexInputField: React.FC<HexInputFieldProps> = ({
color,
onChange,
label,
errorText,
}: HexInputFieldProps) => {
return (
<FormField label={label} errorText={errorText}>
<FixedWidthInput width='6em' value={color} onChange={onChange} data-testid='rule-color-selector-hex' />
</FormField>
);
};

interface RgbInputFieldProps {
value: string;
onChange: NonCancelableEventHandler<BaseChangeDetail>;
label: string;
testId: string;
}

export const RgbInputField: React.FC<RgbInputFieldProps> = ({ value, onChange, label, testId }: RgbInputFieldProps) => {
return (
<FormField label={label}>
<FixedWidthInput
width='4.3em'
inputMode='numeric'
type='number'
value={value}
onChange={onChange}
data-testid={testId}
/>
</FormField>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { TextInput } from '../CommonPanelComponents';
import useDynamicScene from '../../../hooks/useDynamicScene';

import { ValueDataBindingBuilder } from './common/ValueDataBindingBuilder';
import { ColorPicker } from './tag-style/ColorPicker/ColorPicker';
import { DecodeSvgString } from './tag-style/ColorPicker/ColorPickerUtils/DecodeSvgString';
import { ColorSelectorCombo } from './tag-style/ColorSelectorCombo/ColorSelectorCombo';
import { DecodeSvgString } from './tag-style/ColorSelectorCombo/ColorSelectorComboUtils/DecodeSvgString';
import { IconPicker } from './tag-style/IconPicker/IconPicker';

export const convertParamsToKeyValuePairs = (params: Record<string, string>) => {
Expand Down Expand Up @@ -238,7 +238,7 @@ export const AnchorComponentEditor: React.FC<IAnchorComponentEditorProps> = ({
{isCustomStyle && (
<FormField>
<SpaceBetween size='m'>
<ColorPicker
<ColorSelectorCombo
color={anchorComponent.chosenColor ?? colors.customBlue}
onSelectColor={(pickedColor) => {
onUpdateCallbackForChosenColor({
Expand Down

0 comments on commit e768a88

Please sign in to comment.