-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ColorPicker] Add hex/alpha text fields #2186
Changes from all commits
713f088
7982779
0661d40
7d9c8e4
b03edac
007337e
236e296
7c90dd3
4208728
42c0ab6
ec02661
2f5a095
2d60fc2
eee15f8
b8703b0
22b31da
3460002
2f9c917
87dc89c
e4d1eaa
285aaf8
cc610d4
c4899f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,12 @@ | ||
@import '../../styles/common'; | ||
|
||
$picker-size: rem(160px); | ||
$picker-width: rem(160px); | ||
$picker-with-alpha-width: rem(194px); | ||
$picker-height: $picker-width; | ||
$dragger-size: rem(18px); | ||
$text-picker-width: rem(192px); | ||
$text-picker-with-alpha-width: $picker-width; | ||
$alpha-size: rem(90px); | ||
$inner-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.5); | ||
|
||
$stacking-order: ( | ||
|
@@ -10,6 +15,10 @@ $stacking-order: ( | |
dragger: 30, | ||
); | ||
|
||
$swatch-size: rem(20px); | ||
$swatch-shadow: inset rgba(0, 0, 0, 0.07) 0 0 0 1px, | ||
inset rgba(0, 0, 0, 0.15) 0 1px 3px 0; | ||
|
||
@mixin checkers { | ||
background-image: linear-gradient( | ||
45deg, | ||
|
@@ -36,8 +45,12 @@ $stacking-order: ( | |
@include checkers; | ||
position: relative; | ||
overflow: hidden; | ||
height: $picker-size; | ||
width: $picker-size; | ||
height: $picker-height; | ||
width: $picker-width; | ||
|
||
&.AlphaAllowed { | ||
width: $picker-with-alpha-width; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I just saw the earlier discussions about that too... I'm not sure increasing the width is really the best approach tho. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mirualves What do you think about this approach? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As long as the component width adapts to accept RGB colors comfortably, I don't see a problem. |
||
} | ||
|
||
// Need an extra pixel to avoid a small color bleed from happening | ||
border-radius: var(--p-border-radius-base, border-radius() + 1px); | ||
|
@@ -98,17 +111,17 @@ $stacking-order: ( | |
$green: rgb(0, 255, 0); | ||
$purple: rgb(255, 0, 255); | ||
$huepicker-padding: $dragger-size; | ||
$huepicker-bottom-padding-start: $picker-size - $dragger-size; | ||
$huepicker-bottom-padding-start: $picker-width - $dragger-size; | ||
|
||
.HuePicker, | ||
.AlphaPicker { | ||
position: relative; | ||
overflow: hidden; | ||
height: $picker-size; | ||
height: $picker-height; | ||
width: rem(24px); | ||
margin-left: spacing(tight); | ||
border-width: var(--p-border-radius-base, border-radius()); | ||
border-radius: $picker-size * 0.5; | ||
border-radius: $picker-height * 0.5; | ||
} | ||
|
||
.HuePicker { | ||
|
@@ -129,7 +142,7 @@ $huepicker-bottom-padding-start: $picker-size - $dragger-size; | |
@include checkers; | ||
|
||
.ColorLayer { | ||
border-radius: var(--p-override-none, $picker-size * 0.5); | ||
border-radius: var(--p-override-none, $picker-height * 0.5); | ||
} | ||
} | ||
|
||
|
@@ -149,3 +162,40 @@ $huepicker-bottom-padding-start: $picker-size - $dragger-size; | |
width: 100%; | ||
cursor: pointer; | ||
} | ||
|
||
.TextFields { | ||
display: flex; | ||
} | ||
|
||
.TextPicker { | ||
width: $text-picker-width; | ||
margin-top: spacing(tight); | ||
|
||
&.AlphaAllowed { | ||
width: $text-picker-with-alpha-width; | ||
} | ||
} | ||
|
||
.TextFieldSwatch { | ||
width: $swatch-size; | ||
height: $swatch-size; | ||
margin-left: -(spacing(extra-tight)); | ||
border-radius: 50%; | ||
box-shadow: $swatch-shadow; | ||
|
||
&.AlphaAllowed { | ||
@include checkers; | ||
} | ||
} | ||
|
||
.SwatchBackground { | ||
width: 100%; | ||
height: 100%; | ||
border-radius: inherit; | ||
box-shadow: inherit; | ||
} | ||
|
||
.AlphaField { | ||
width: $alpha-size; | ||
margin: spacing(tight) 0 0 spacing(tight); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,29 @@ | ||
import React from 'react'; | ||
import debounce from 'lodash/debounce'; | ||
import {clamp} from '@shopify/javascript-utilities/math'; | ||
|
||
import {hsbToRgb} from '../../utilities/color-transformers'; | ||
import {classNames} from '../../utilities/css'; | ||
import {hsbToRgb, hexToHsb} from '../../utilities/color-transformers'; | ||
import type {HSBColor, HSBAColor} from '../../utilities/color-types'; | ||
|
||
import {AlphaPicker, HuePicker, Slidable, SlidableProps} from './components'; | ||
import {EventListener} from '../EventListener'; | ||
|
||
import { | ||
AlphaField, | ||
AlphaPicker, | ||
HuePicker, | ||
Slidable, | ||
TextPicker, | ||
} from './components'; | ||
import styles from './ColorPicker.scss'; | ||
|
||
interface State { | ||
pickerSize: number; | ||
pickerWidth: number; | ||
pickerHeight: number; | ||
} | ||
|
||
interface Position { | ||
x: number; | ||
y: number; | ||
} | ||
|
||
interface Color extends HSBColor { | ||
|
@@ -29,36 +44,64 @@ export interface ColorPickerProps { | |
|
||
export class ColorPicker extends React.PureComponent<ColorPickerProps, State> { | ||
state: State = { | ||
pickerSize: 0, | ||
pickerWidth: 0, | ||
pickerHeight: 0, | ||
}; | ||
|
||
private colorNode: HTMLElement | null = null; | ||
|
||
private handleResize = debounce( | ||
() => { | ||
if (this.colorNode == null) return; | ||
this.setState({ | ||
pickerWidth: this.colorNode.clientWidth, | ||
pickerHeight: this.colorNode.clientHeight, | ||
}); | ||
}, | ||
50, | ||
{trailing: true}, | ||
); | ||
|
||
componentDidMount() { | ||
const {colorNode} = this; | ||
if (colorNode == null) { | ||
return; | ||
} | ||
|
||
this.setState({pickerSize: colorNode.clientWidth}); | ||
this.setState({ | ||
pickerWidth: colorNode.clientWidth, | ||
pickerHeight: colorNode.clientHeight, | ||
}); | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
setTimeout(() => { | ||
this.setState({pickerSize: colorNode.clientWidth}); | ||
this.setState({ | ||
pickerWidth: colorNode.clientWidth, | ||
pickerHeight: colorNode.clientHeight, | ||
}); | ||
}, 0); | ||
} | ||
} | ||
|
||
render() { | ||
const {id, color, allowAlpha} = this.props; | ||
const {hue, saturation, brightness, alpha: providedAlpha} = color; | ||
const {pickerSize} = this.state; | ||
const {pickerWidth, pickerHeight} = this.state; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious, do we need to update these values on resize? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, they should be updated 👍 |
||
|
||
const alpha = providedAlpha != null && allowAlpha ? providedAlpha : 1; | ||
const {red, green, blue} = hsbToRgb({hue, saturation: 1, brightness: 1}); | ||
const colorString = `rgba(${red}, ${green}, ${blue}, ${alpha})`; | ||
const draggerX = clamp(saturation * pickerSize, 0, pickerSize); | ||
const draggerY = clamp(pickerSize - brightness * pickerSize, 0, pickerSize); | ||
const draggerX = clamp(saturation * pickerWidth, 0, pickerWidth); | ||
const draggerY = clamp( | ||
pickerHeight - brightness * pickerHeight, | ||
0, | ||
pickerHeight, | ||
); | ||
|
||
const className = classNames( | ||
styles.MainColor, | ||
allowAlpha && styles.AlphaAllowed, | ||
); | ||
|
||
const alphaSliderMarkup = allowAlpha ? ( | ||
<AlphaPicker | ||
|
@@ -68,25 +111,44 @@ export class ColorPicker extends React.PureComponent<ColorPickerProps, State> { | |
/> | ||
) : null; | ||
|
||
const hexPickerMarkup = ( | ||
<TextPicker | ||
color={color} | ||
allowAlpha={allowAlpha} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of having to pass an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One issue with not passing it is the use of |
||
onChange={this.handleHexChange} | ||
/> | ||
); | ||
|
||
const alphaFieldMarkup = allowAlpha ? ( | ||
<AlphaField alpha={alpha} onChange={this.handleAlphaChange} /> | ||
) : null; | ||
|
||
return ( | ||
<div | ||
className={styles.ColorPicker} | ||
id={id} | ||
onMouseDown={this.handlePickerDrag} | ||
> | ||
<div ref={this.setColorNode} className={styles.MainColor}> | ||
<div | ||
className={styles.ColorLayer} | ||
style={{backgroundColor: colorString}} | ||
/> | ||
<Slidable | ||
onChange={this.handleDraggerMove} | ||
draggerX={draggerX} | ||
draggerY={draggerY} | ||
/> | ||
<div> | ||
<div | ||
className={styles.ColorPicker} | ||
id={id} | ||
onMouseDown={this.handlePickerDrag} | ||
> | ||
<div ref={this.setColorNode} className={className}> | ||
<div | ||
className={styles.ColorLayer} | ||
style={{backgroundColor: colorString}} | ||
/> | ||
<Slidable | ||
onChange={this.handleDraggerMove} | ||
draggerX={draggerX} | ||
draggerY={draggerY} | ||
/> | ||
</div> | ||
<HuePicker hue={hue} onChange={this.handleHueChange} /> | ||
{alphaSliderMarkup} | ||
</div> | ||
<HuePicker hue={hue} onChange={this.handleHueChange} /> | ||
{alphaSliderMarkup} | ||
<div className={styles.TextFields}> | ||
{hexPickerMarkup} | ||
{alphaFieldMarkup} | ||
</div> | ||
<EventListener event="resize" handler={this.handleResize} /> | ||
</div> | ||
); | ||
} | ||
|
@@ -111,15 +173,24 @@ export class ColorPicker extends React.PureComponent<ColorPickerProps, State> { | |
onChange({hue, brightness, saturation, alpha}); | ||
}; | ||
|
||
private handleDraggerMove: SlidableProps['onChange'] = ({x, y}) => { | ||
const {pickerSize} = this.state; | ||
private handleHexChange = (hex: string) => { | ||
const { | ||
color: {alpha = 1}, | ||
onChange, | ||
} = this.props; | ||
const newColor = hexToHsb(hex); | ||
onChange({...newColor, alpha}); | ||
}; | ||
Comment on lines
+176
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we prevent invalid hex codes from being added, or maybe our error message could include why it's invalid? What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was discussed over in the Figma designs. We had error messages implemented first, but then decided to change to prevent invalid hex values being entered. The check is made in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, so if a user enters There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is a good point @AndrewMusgrave any thoughts on this scenario @mirualves? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll leave this up to Miru's best judgment 😄 |
||
|
||
private handleDraggerMove = ({x, y}: Position) => { | ||
const {pickerWidth, pickerHeight} = this.state; | ||
const { | ||
color: {hue, alpha = 1}, | ||
onChange, | ||
} = this.props; | ||
|
||
const saturation = clamp(x / pickerSize, 0, 1); | ||
const brightness = clamp(1 - y / pickerSize, 0, 1); | ||
const saturation = clamp(x / pickerWidth, 0, 1); | ||
const brightness = clamp(1 - y / pickerHeight, 0, 1); | ||
|
||
onChange({hue, saturation, brightness, alpha}); | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React, {useCallback, useEffect, useState} from 'react'; | ||
import {clamp} from '@shopify/javascript-utilities/math'; | ||
|
||
import {useI18n} from '../../../../utilities/i18n'; | ||
import {TextField} from '../../../TextField'; | ||
import styles from '../../ColorPicker.scss'; | ||
|
||
export interface AlphaFieldProps { | ||
alpha: number; | ||
onChange(alpha: number): void; | ||
} | ||
|
||
export function AlphaField({alpha, onChange}: AlphaFieldProps) { | ||
const i18n = useI18n(); | ||
|
||
const [percentage, setPercentage] = useState( | ||
clamp(Math.round(alpha * 100) || 0, 0, 100), | ||
); | ||
|
||
const label = i18n.translate( | ||
'Polaris.ColorPicker.alphaFieldAccessibilityLabel', | ||
); | ||
|
||
useEffect(() => { | ||
setPercentage(Math.round(alpha * 100)); | ||
}, [alpha]); | ||
|
||
const handleTextChange = useCallback((value) => { | ||
setPercentage(value); | ||
}, []); | ||
|
||
const handleBlur = useCallback(() => { | ||
const normalizedPercentage = clamp(percentage, 0, 100); | ||
|
||
if (normalizedPercentage !== null) { | ||
setPercentage(normalizedPercentage); | ||
|
||
const alphaHasChanged = normalizedPercentage !== alpha * 100; | ||
|
||
if (alphaHasChanged) { | ||
onChange(normalizedPercentage / 100); | ||
} | ||
} | ||
}, [alpha, onChange, percentage]); | ||
|
||
return ( | ||
<div className={styles.AlphaField}> | ||
<TextField | ||
suffix="%" | ||
value={percentage.toString()} | ||
label={label} | ||
labelHidden | ||
type="number" | ||
autoComplete={false} | ||
max={100} | ||
min={0} | ||
onChange={handleTextChange} | ||
onBlur={handleBlur} | ||
/> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export {AlphaField} from './AlphaField'; | ||
export type {AlphaFieldProps} from './AlphaField'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dpersing Any suggestions on what a11y labels should we use here?
The first is used for the hex/colour name/rgb input field and the second is used for the alpha percentage field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added these as placeholders for now.