-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(composer): add a11y color picker
fix: update package-lock.json fix: update component name
- Loading branch information
1 parent
a9011a8
commit e768a88
Showing
28 changed files
with
418 additions
and
73 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
packages/scene-composer/src/components/panels/ColorPicker/ColorPicker.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
91 changes: 91 additions & 0 deletions
91
packages/scene-composer/src/components/panels/ColorPicker/ColorPicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
119 changes: 119 additions & 0 deletions
119
packages/scene-composer/src/components/panels/ColorPicker/ColorPickerHelpers.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.