-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Polish Label Color Picker #12464
Polish Label Color Picker #12464
Changes from 9 commits
c1175d3
475bdb5
8b2bd4d
1a8374f
61ea922
543e284
340f4ec
7a48f76
305242d
7950631
e4cd8c7
3d1ba1f
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 |
---|---|---|
@@ -0,0 +1,100 @@ | ||
@import 'src/style/modules'; | ||
|
||
/* | ||
Color Picker Widget | ||
------------------------------------------------------------------------------ | ||
*/ | ||
|
||
$color-picker--margin: 0; | ||
|
||
.color-picker { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: stretch; | ||
width: 100%; | ||
min-width: 160px; | ||
} | ||
|
||
.color-picker--swatches { | ||
margin-bottom: $ix-marg-b; | ||
position: relative; | ||
padding: $color-picker--margin; | ||
border-radius: $radius; | ||
overflow: hidden; | ||
|
||
&:hover { | ||
cursor: pointer; | ||
} | ||
} | ||
|
||
.color-picker--swatch { | ||
width: 10%; | ||
padding-bottom: 10%; | ||
position: relative; | ||
float: left; | ||
opacity: 1; | ||
transition: opacity 0.25s ease; | ||
|
||
> span { | ||
position: absolute; | ||
top: $color-picker--margin; | ||
left: $color-picker--margin; | ||
right: $color-picker--margin; | ||
bottom: $color-picker--margin; | ||
} | ||
|
||
&:after { | ||
content: ''; | ||
position: absolute; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%) scale(0, 0); | ||
width: 8px; | ||
height: 8px; | ||
border-radius: 50%; | ||
background-color: $g20-white; | ||
opacity: 0; | ||
transition: opacity 0.25s ease, transform 0.25s ease; | ||
box-shadow: 0 0 4px 1px rgba($g0-obsidian, 0.25); | ||
} | ||
|
||
&:hover { | ||
&:after { | ||
opacity: 1; | ||
transform: translate(-50%, -50%) scale(1, 1); | ||
} | ||
} | ||
} | ||
|
||
.color-picker--form { | ||
display: flex; | ||
align-items: center; | ||
position: relative; | ||
} | ||
|
||
.input.color-picker--input { | ||
flex: 1 0 0; | ||
margin-right: $ix-marg-a; | ||
|
||
> input { | ||
padding-left: $ix-marg-d; | ||
} | ||
} | ||
|
||
.color-picker--selected { | ||
pointer-events: none; | ||
z-index: 2; | ||
position: absolute; | ||
top: 50%; | ||
left: $ix-marg-c; | ||
transform: translate(-50%, -50%); | ||
width: 18px; | ||
height: 18px; | ||
border-radius: 50%; | ||
border: $ix-border solid $g5-pepper; | ||
transition: background-color 0.25s ease, border-color 0.25s ease; | ||
|
||
.input:hover + & { | ||
border-color: $g7-graphite; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
// Libraries | ||
import React, {Component, ChangeEvent} from 'react' | ||
import _ from 'lodash' | ||
|
||
// Components | ||
import { | ||
Button, | ||
IconFont, | ||
ButtonShape, | ||
ComponentStatus, | ||
} from '@influxdata/clockface' | ||
import {Input} from 'src/clockface' | ||
import Swatch from 'src/clockface/components/color_picker/ColorPickerSwatch' | ||
import Error from 'src/clockface/components/form_layout/FormElementError' | ||
|
||
// Constants | ||
import {colors} from 'src/clockface/constants/colors' | ||
|
||
// Utils | ||
import {validateHexCode} from 'src/configuration/utils/labels' | ||
|
||
// Styles | ||
import 'src/clockface/components/color_picker/ColorPicker.scss' | ||
|
||
interface PassedProps { | ||
selectedHex: string | ||
onSelect: (hex: string, status?: ComponentStatus) => void | ||
} | ||
|
||
interface DefaultProps { | ||
maintainInputFocus?: boolean | ||
testID?: string | ||
} | ||
|
||
type Props = PassedProps & DefaultProps | ||
|
||
interface State { | ||
inputValue: string | ||
status: string | ||
} | ||
|
||
export default class ColorPicker extends Component<Props, State> { | ||
public static defaultProps: DefaultProps = { | ||
maintainInputFocus: false, | ||
testID: 'color-picker', | ||
} | ||
|
||
constructor(props: Props) { | ||
super(props) | ||
|
||
this.state = { | ||
inputValue: this.props.selectedHex || '', | ||
status: null, | ||
} | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
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 prevProps be typed? 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. YES |
||
if (prevProps.selectedHex !== this.props.selectedHex) { | ||
this.setState({inputValue: this.props.selectedHex}) | ||
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. is it necessary to keep track of the hex value in state? it seems like this will be creating two sources of truth? could all the places that state instead call the onSelect() prop? |
||
} | ||
} | ||
|
||
render() { | ||
const {maintainInputFocus, testID} = this.props | ||
const {inputValue} = this.state | ||
|
||
return ( | ||
<div className="color-picker" data-testid={testID}> | ||
<div className="color-picker--swatches"> | ||
{colors.map(color => ( | ||
<Swatch | ||
key={color.name} | ||
hex={color.hex} | ||
name={color.name} | ||
onClick={this.handleSwatchClick} | ||
testID={testID} | ||
/> | ||
))} | ||
</div> | ||
<div className="color-picker--form"> | ||
<Input | ||
customClass="color-picker--input" | ||
placeholder="#000000" | ||
value={inputValue} | ||
onChange={this.handleInputChange} | ||
maxLength={7} | ||
onBlur={this.handleInputBlur} | ||
autoFocus={maintainInputFocus} | ||
status={this.inputStatus} | ||
testID={`${testID}--input`} | ||
/> | ||
{this.selectedColor} | ||
<Button | ||
icon={IconFont.Refresh} | ||
shape={ButtonShape.Square} | ||
onClick={this.handleRandomizeColor} | ||
titleText="I'm feeling lucky" | ||
testID={`${testID}--randomize`} | ||
/> | ||
</div> | ||
{this.errorMessage} | ||
</div> | ||
) | ||
} | ||
|
||
private handleSwatchClick = (hex: string): void => { | ||
const {onSelect} = this.props | ||
|
||
this.setState({inputValue: hex}) | ||
onSelect(hex, ComponentStatus.Valid) | ||
} | ||
|
||
private get inputStatus(): ComponentStatus { | ||
return this.state.status ? ComponentStatus.Error : ComponentStatus.Valid | ||
} | ||
|
||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { | ||
const {onSelect} = this.props | ||
const acceptedChars = [ | ||
'#', | ||
'a', | ||
'b', | ||
'c', | ||
'd', | ||
'e', | ||
'f', | ||
'0', | ||
'1', | ||
'2', | ||
'3', | ||
'4', | ||
'5', | ||
'6', | ||
'7', | ||
'8', | ||
'9', | ||
] | ||
|
||
const trimmedValue = e.target.value.trim() | ||
const inputValue = trimmedValue | ||
.split('') | ||
.filter(char => acceptedChars.includes(char.toLowerCase())) | ||
.join('') | ||
|
||
const status = validateHexCode(inputValue) | ||
const validity = | ||
status === null ? ComponentStatus.Valid : ComponentStatus.Error | ||
|
||
onSelect(inputValue, validity) | ||
this.setState({inputValue, status}) | ||
} | ||
|
||
private handleInputBlur = (e: ChangeEvent<HTMLInputElement>) => { | ||
const {maintainInputFocus} = this.props | ||
|
||
if (maintainInputFocus) { | ||
e.target.focus() | ||
} | ||
} | ||
|
||
private handleRandomizeColor = (): void => { | ||
const {onSelect} = this.props | ||
const {hex} = _.sample(colors) | ||
|
||
this.setState({inputValue: hex}) | ||
onSelect(hex, ComponentStatus.Valid) | ||
} | ||
|
||
private get selectedColor(): JSX.Element { | ||
const {inputValue} = this.state | ||
|
||
return ( | ||
<div | ||
className="color-picker--selected" | ||
style={{backgroundColor: inputValue}} | ||
/> | ||
) | ||
} | ||
|
||
private get errorMessage(): JSX.Element { | ||
const {testID} = this.props | ||
const {status} = this.state | ||
|
||
if (status) { | ||
return ( | ||
<div className="color-picker--error" data-testid={`${testID}--error`}> | ||
<Error message={status} /> | ||
</div> | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Libraries | ||
import React, {Component} from 'react' | ||
|
||
interface Props { | ||
name: string | ||
hex: string | ||
onClick: (hex: string) => void | ||
testID: string | ||
} | ||
|
||
export default class ColorPickerSwatch extends Component<Props> { | ||
render() { | ||
const {name, hex, testID} = this.props | ||
return ( | ||
<div | ||
className="color-picker--swatch" | ||
title={name} | ||
onClick={this.handleClick} | ||
data-testid={`${testID}--swatch`} | ||
> | ||
<span style={{backgroundColor: hex}} /> | ||
</div> | ||
) | ||
} | ||
|
||
private handleClick = (): void => { | ||
this.props.onClick(this.props.hex) | ||
} | ||
} |
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.
noice!