Skip to content

Commit

Permalink
PLT-6739 Convert theme color pickers to use react-color (mattermost#108)
Browse files Browse the repository at this point in the history
* Add color input component

* Add ColorChooser component

Add component for choosing color in custom_theme_chooser.jsx

* Use custom chooser isntead of bootstrap color picker

* Configure enzyme to work with react 15

After updating enzyme to 3(commit:
mattermost@561b93b)

Need to add Adapter for
enzyme(http:https://airbnb.io/enzyme/docs/guides/migration-from-2-to-3.html)

* Add snapshot tests for ColorInput component

* Add test for ColorChooser component

* Fix test for ColorChooser

* Update dependencies for enzyme-adapter-react-15
  • Loading branch information
Zapix authored and jwilander committed Oct 5, 2017
1 parent 2f47bb1 commit 38d0d3e
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 72 deletions.
107 changes: 107 additions & 0 deletions components/color_input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

import React from 'react';
import ReactDom from 'react-dom';
import PropTypes from 'prop-types';
import {ChromePicker} from 'react-color';

class ColorInput extends React.Component {
static propTypes = {

/*
* Selected color
*/
color: PropTypes.string.isRequired,

/*
* Function called when color changed. Takes hex format of color Ex: #ffeec0
*/
onChange: PropTypes.func
};

constructor(props) {
super(props);
this.state = {
idOpened: false
};
}

componentDidUpdate(prevProps, prevState) {
const {isOpened: prevIsOpened} = prevState;
const {isOpened} = this.state;

if (isOpened !== prevIsOpened) {
if (isOpened) {
document.addEventListener('click', this.checkClick);
} else {
document.removeEventListener('click', this.checkClick);
}
}
}

checkClick = (e) => {
const colorPickerDOMNode = ReactDom.findDOMNode(this.colorPicker);
if (!colorPickerDOMNode.contains(e.target)) {
this.setState({isOpened: false});
}
};

togglePicker = () => {
this.setState({isOpened: !this.state.isOpened});
};

handleChange = (newColorData) => {
const {hex} = newColorData;
const {onChange: handleChange} = this.props;

if (handleChange) {
handleChange(hex);
}
};

render() {
const {color} = this.props;
const {isOpened} = this.state;

return (
<div className='color-input input-group'>
<input
className='form-control'
type='text'
value={color}
style={{
background: '#fff'
}}
readOnly={true}
/>
<span
className='input-group-addon'
onClick={this.togglePicker}
>
<i
className='color-icon'
style={{
backgroundColor: color
}}
/>
</span>
{isOpened && (
<div
ref={(item) => {
this.colorPicker = item;
}}
className='color-popover'
>
<ChromePicker
color={color}
onChange={this.handleChange}
/>
</div>
)}
</div>
);
}
}

export default ColorInput;
54 changes: 54 additions & 0 deletions components/user_settings/color_chooser.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

import React from 'react';
import PropTypes from 'prop-types';

import ColorInput from 'components/color_input';

class ColorChooser extends React.Component {
static propTypes = {

/*
* The id of setting that we will change
*/
id: PropTypes.string.isRequired,

/*
* The label of setting that we will choose
*/
label: PropTypes.string.isRequired,

/*
* Selected color
*/
color: PropTypes.string.isRequired,

/*
* Function called when color changed takes 2 arguments: Id of changing setting and new color
*/
onChange: PropTypes.func
}

handleChange = (newColor) => {
const {id, onChange: handleChange} = this.props;
if (handleChange) {
handleChange(id, newColor);
}
}

render() {
const {label, color} = this.props;
return (
<div>
<label className='custom-label'>{label}</label>
<ColorInput
color={color}
onChange={this.handleChange}
/>
</div>
);
}
}

export default ColorChooser;
95 changes: 24 additions & 71 deletions components/user_settings/custom_theme_chooser.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

import 'bootstrap-colorpicker';
import $ from 'jquery';
import PropTypes from 'prop-types';
import React from 'react';
Expand All @@ -10,7 +9,7 @@ import {defineMessages, FormattedMessage, intlShape, injectIntl} from 'react-int

import Constants from 'utils/constants.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx';
import ColorChooser from './color_chooser.jsx';

const messages = defineMessages({
sidebarBg: {
Expand Down Expand Up @@ -103,8 +102,6 @@ const messages = defineMessages({
}
});

const HEX_CODE_LENGTH = 7;

class CustomThemeChooser extends React.Component {
constructor(props) {
super(props);
Expand All @@ -120,47 +117,21 @@ class CustomThemeChooser extends React.Component {
}

componentDidMount() {
$('.color-picker').colorpicker({
format: 'hex'
});
$('.color-picker').on('changeColor', this.onPickerChange);
$('.group--code').on('change', this.onCodeThemeChange);
document.addEventListener('click', this.closeColorpicker);
}

componentWillUnmount() {
$('.color-picker').off('changeColor', this.onPickerChange);
$('.group--code').off('change', this.onCodeThemeChange);
document.removeEventListener('click', this.closeColorpicker);
}

componentDidUpdate() {
const theme = this.props.theme;
Constants.THEME_ELEMENTS.forEach((element) => {
if (theme.hasOwnProperty(element.id) && element.id !== 'codeTheme') {
$('#' + element.id).data('colorpicker').color.setColor(theme[element.id]);
$('#' + element.id).colorpicker('update');
}
});
}

closeColorpicker(e) {
if (!$(e.target).closest('.color-picker').length && Utils.isMobile()) {
$('.color-picker').colorpicker('hide');
}
}

onPickerChange = (e) => {
const inputBox = e.target.childNodes[0];
if (document.activeElement === inputBox && inputBox.value.length !== HEX_CODE_LENGTH) {
return;
}

const theme = this.props.theme;
if (theme[e.target.id] !== e.color.toHex()) {
theme[e.target.id] = e.color.toHex();
theme.type = 'custom';
this.props.updateTheme(theme);
handleColorChange = (settingId, color) => {
const {updateTheme, theme} = this.props;
if (theme[settingId] !== color) {
updateTheme({
...theme,
type: 'custom',
[settingId]: color
});
}
}

Expand Down Expand Up @@ -315,18 +286,12 @@ class CustomThemeChooser extends React.Component {
className='col-sm-6 form-group element'
key={'custom-theme-key' + index}
>
<label className='custom-label'>{formatMessage(messages[element.id])}</label>
<div
className='input-group color-picker'
<ColorChooser
id={element.id}
>
<input
className='form-control'
type='text'
defaultValue={theme[element.id]}
/>
<span className='input-group-addon'><i/></span>
</div>
label={formatMessage(messages[element.id])}
color={theme[element.id]}
onChange={this.handleColorChange}
/>
</div>
);
} else if (element.group === 'sidebarElements') {
Expand All @@ -335,18 +300,12 @@ class CustomThemeChooser extends React.Component {
className='col-sm-6 form-group element'
key={'custom-theme-key' + index}
>
<label className='custom-label'>{formatMessage(messages[element.id])}</label>
<div
className='input-group color-picker'
<ColorChooser
id={element.id}
>
<input
className='form-control'
type='text'
defaultValue={theme[element.id]}
/>
<span className='input-group-addon'><i/></span>
</div>
label={formatMessage(messages[element.id])}
color={theme[element.id]}
onChange={this.handleColorChange}
/>
</div>
);
} else {
Expand All @@ -355,18 +314,12 @@ class CustomThemeChooser extends React.Component {
className='col-sm-6 form-group element'
key={'custom-theme-key' + index}
>
<label className='custom-label'>{formatMessage(messages[element.id])}</label>
<div
className='input-group color-picker'
<ColorChooser
id={element.id}
>
<input
className='form-control'
type='text'
defaultValue={theme[element.id]}
/>
<span className='input-group-addon'><i/></span>
</div>
label={formatMessage(messages[element.id])}
color={theme[element.id]}
onChange={this.handleColorChange}
/>
</div>
);
}
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"bootstrap-colorpicker": "2.5.2",
"chart.js": "2.7.0",
"compass-mixins": "0.12.10",
"enzyme-adapter-react-15": "1.0.1",
"exif2css": "1.2.0",
"fastclick": "1.0.6",
"flux": "3.1.3",
Expand Down Expand Up @@ -40,6 +41,7 @@
"react-redux": "5.0.6",
"react-router": "2.8.1",
"react-select": "1.0.0-rc.10",
"react-test-renderer": "15",
"redux": "3.7.2",
"redux-batched-actions": "0.2.0",
"redux-persist": "4.10.1",
Expand Down Expand Up @@ -132,7 +134,8 @@
],
"transformIgnorePatterns": [
"node_modules/(?!react-native|react-router)"
]
],
"setupTestFrameworkScriptFile": "<rootDir>/tests/setup.js"
},
"scripts": {
"check": "eslint --ext \".jsx\" --ignore-pattern node_modules --quiet .",
Expand Down
19 changes: 19 additions & 0 deletions sass/components/_color-input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@charset 'UTF-8';

.color-input {
position: relative;
}

.color-icon {
display: inline-block;
width: 16px;
height: 16px;
}

.color-popover {
position: absolute;
top: 100%;
right: 0;
padding-top: 8px;
z-index: 12;
}
1 change: 1 addition & 0 deletions sass/components/_module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
@import 'tutorial';
@import 'videos';
@import 'webrtc';
@import 'color-input';
Loading

0 comments on commit 38d0d3e

Please sign in to comment.