Skip to content

Commit

Permalink
Use input widget state to style task inputs (#5091)
Browse files Browse the repository at this point in the history
* Use input widget state to style task inputs
Use checkbox/radio button state (active, checked or focussed) to control the style of adjacent task labels.
Remove onFocus(), onBlur(), unfocus() and associated tests from TaskInputField.

* Remove unused props
Remove a ref that's no longer used, and a label prop that didn't seem to be referenced anywhere.
Move classname up to the container label.

* Convert TaskInputField to a function
Convert task input field to a functional component and add React.memo to speed up rendering.
Remove onChange() method and update tests.

* Fix multiple choice task tests
Rewrite tests to use shallow rendering and test props of the generic task component, not nested children.

* Fix single answer task tests
Rewrite tests to use shallow rendering and test props of the generic task component, not nested children.

* Add checked prop
Remove shouldInputBeChecked and move responsibility for checked logic up to parent task. Add boolean checked prop.
Use defaultChecked instead of checked, so that checkbox state controls the annotation, rather than vice-versa.

* Add autoFocus prop
Remove shouldInputBeAutoFocussed. Add boolean autoFocus prop instead, and move autofocus logic up to parent tasks.

* Fix text highlighter buttons
Use the as prop to changed the rendered HTML element.
Apply hover styles directly to task input labels.

* Restore missing active and focus hover styles
Restore styles for active state (input:checked), input checked and focussed, and label hover on checked imputs.

* Define common hover styles
Move common hover styles into a shared object.

* Define constants for styles
Define constants for the default, hover and checked themed styles.

* Fix linter warnings

* Update sinon spy tests
Use the BDD syntax from sinon-chai.
  • Loading branch information
eatyourgreens authored and srallen committed Nov 27, 2018
1 parent 74a3e43 commit f2ecaf2
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 323 deletions.
259 changes: 105 additions & 154 deletions app/classifier/tasks/components/TaskInputField/TaskInputField.jsx
Expand Up @@ -9,185 +9,144 @@ import { pxToRem, zooTheme } from '../../../../theme';
import TaskInputLabel from './components/TaskInputLabel';
import { doesTheLabelHaveAnImage } from './helpers';

export const StyledTaskInputField = styled.label`
align-items: baseline;
background-color: ${theme('mode', {
const DEFAULT = {
backgroundColor: theme('mode', {
dark: zooTheme.colors.darkTheme.background.default,
light: zooTheme.colors.lightTheme.background.default
})};
border: ${theme('mode', {
}),
border: theme('mode', {
dark: `2px solid ${zooTheme.colors.darkTheme.font}`,
light: '2px solid transparent'
})};
box-shadow: 1px 1px 2px 0 rgba(0,0,0,0.5);
color: ${theme('mode', {
}),
color: theme('mode', {
dark: zooTheme.colors.darkTheme.font,
light: zooTheme.colors.lightTheme.font
})};
})
};

const HOVER = {
gradientTop: theme('mode', {
dark: zooTheme.colors.darkTheme.button.answer.gradient.top,
light: zooTheme.colors.lightTheme.button.answer.gradient.top
}),
gradientBottom: theme('mode', {
dark: zooTheme.colors.darkTheme.button.answer.gradient.bottom,
light: zooTheme.colors.lightTheme.button.answer.gradient.bottom
}),
color: theme('mode', {
dark: zooTheme.colors.darkTheme.font,
light: 'black'
})
};

const CHECKED = {
background: theme('mode', {
dark: zooTheme.colors.teal.mid,
light: zooTheme.colors.teal.mid
}),
border: theme('mode', {
dark: `2px solid ${zooTheme.colors.teal.mid}`,
light: '2px solid transparent'
}),
color: theme('mode', {
dark: zooTheme.colors.darkTheme.font,
light: 'white'
})
};

export const StyledTaskLabel = styled.span`
align-items: baseline;
background-color: ${DEFAULT.backgroundColor};
border: ${DEFAULT.border};
box-shadow: 1px 1px 2px 0 rgba(0,0,0,0.5);
color: ${DEFAULT.color};
cursor: pointer;
display: flex;
margin: ${pxToRem(10)} 0;
padding: ${(props) => { return doesTheLabelHaveAnImage(props.label) ? '0' : '1ch 2ch'; }};
padding: ${props => (doesTheLabelHaveAnImage(props.label) ? '0' : '1ch 2ch')};
&:hover {
background: linear-gradient(${HOVER.gradientTop}, ${HOVER.gradientBottom});
border-width: 2px;
border-style: solid;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: ${HOVER.gradientTop};
border-bottom-color: ${HOVER.gradientBottom};
color: ${HOVER.color};
}
`;

export const StyledTaskInputField = styled.label`
position: relative;
&:hover, &:focus, &[data-focus=true] {
background: ${theme('mode', {
dark: `linear-gradient(
${zooTheme.colors.darkTheme.button.answer.gradient.top},
${zooTheme.colors.darkTheme.button.answer.gradient.bottom}
)`,
light: `linear-gradient(
${zooTheme.colors.lightTheme.button.answer.gradient.top},
${zooTheme.colors.lightTheme.button.answer.gradient.bottom}
)`
})};
input {
opacity: 0.01;
position: absolute;
}
input:focus + ${StyledTaskLabel} {
background: linear-gradient(${HOVER.gradientTop}, ${HOVER.gradientBottom});
border-width: 2px;
border-style: solid;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: ${theme('mode', {
dark: zooTheme.colors.darkTheme.button.answer.gradient.top,
light: zooTheme.colors.lightTheme.button.answer.gradient.top
})};
border-bottom-color: ${theme('mode', {
dark: zooTheme.colors.darkTheme.button.answer.gradient.bottom,
light: zooTheme.colors.lightTheme.button.answer.gradient.bottom
})};
color: ${theme('mode', {
dark: zooTheme.colors.darkTheme.font,
light: 'black'
})};
border-top-color: ${HOVER.gradientTop};
border-bottom-color: ${HOVER.gradientBottom};
color: ${HOVER.color};
}
&:active {
background: ${theme('mode', {
dark: `linear-gradient(
${zooTheme.colors.darkTheme.button.answer.gradient.top},
${zooTheme.colors.darkTheme.button.answer.gradient.bottom}
)`,
light: `linear-gradient(
${zooTheme.colors.lightTheme.button.answer.gradient.top},
${zooTheme.colors.lightTheme.button.answer.gradient.bottom}
)`
})};
input:active + ${StyledTaskLabel} {
background: linear-gradient(${HOVER.gradientTop}, ${HOVER.gradientBottom});
border-width: 2px;
border-style: solid;
border-color: ${theme('mode', {
dark: zooTheme.colors.teal.dark,
light: zooTheme.colors.teal.mid
})};
color: ${theme('mode', {
dark: zooTheme.colors.darkTheme.font,
light: 'black'
})};
color: ${HOVER.color};
}
&.active {
background: ${theme('mode', {
dark: zooTheme.colors.teal.mid,
light: zooTheme.colors.teal.mid
})};
border: ${theme('mode', {
dark: `2px solid ${zooTheme.colors.teal.mid}`,
light: '2px solid transparent'
})};
color: ${theme('mode', {
dark: zooTheme.colors.darkTheme.font,
light: 'white'
})}
input:checked + ${StyledTaskLabel} {
background: ${CHECKED.background};
border: ${CHECKED.border};
color: ${CHECKED.color}
}
&.active:hover, &.active:focus, &.active[data-focus=true] {
background: ${theme('mode', {
dark: zooTheme.colors.teal.mid,
light: zooTheme.colors.teal.mid
})};
input:focus:checked + ${StyledTaskLabel},
input:checked + ${StyledTaskLabel}:hover {
border: ${theme('mode', {
dark: `2px solid ${zooTheme.colors.teal.dark}`,
light: `2px solid ${zooTheme.colors.teal.dark}`
})};
}
input {
opacity: 0.01;
position: absolute;
}
`;

function shouldInputBeChecked(annotation, index, type) {
if (type === 'radio') {
const toolIndex = annotation._toolIndex || 0;
if (toolIndex) {
return index === toolIndex;
}
return index === annotation.value;
}

if (type === 'checkbox') {
return (annotation.value && annotation.value.length > 0) ? annotation.value.includes(index) : false;
}

return false;
}

function shouldInputBeAutoFocused(annotation, index, name, type) {
if (type === 'radio' && name === 'drawing-tool') {
return index === 0;
}

return index === annotation.value;
}

export class TaskInputField extends React.Component {
constructor() {
super();
this.unFocus = this.unFocus.bind(this);
}

onChange(e) {
this.unFocus();
this.props.onChange(e);
}

onFocus() {
if (this.field) this.field.dataset.focus = true;
}

onBlur() {
this.unFocus();
}

unFocus() {
if (this.field) this.field.dataset.focus = false;
}

render() {
return (
<ThemeProvider theme={{ mode: this.props.theme }}>
<StyledTaskInputField
ref={(node) => { this.field = node; }}
className={this.props.className}
label={this.props.label}
data-focus={false}
>
<input
autoFocus={shouldInputBeAutoFocused(this.props.annotation, this.props.index, this.props.name, this.props.type)}
checked={shouldInputBeChecked(this.props.annotation, this.props.index, this.props.type)}
name={this.props.name}
onBlur={this.onBlur.bind(this)}
onChange={this.onChange.bind(this)}
onFocus={this.onFocus.bind(this)}
type={this.props.type}
value={this.props.index}
/>
<TaskInputLabel label={this.props.label} labelIcon={this.props.labelIcon} labelStatus={this.props.labelStatus} />
</StyledTaskInputField>
</ThemeProvider>
);
}
export function TaskInputField(props) {
return (
<ThemeProvider theme={{ mode: props.theme }}>
<StyledTaskInputField
className={props.className}
>
<input
autoFocus={props.autoFocus}
defaultChecked={props.checked}
name={props.name}
onChange={props.onChange}
type={props.type}
value={props.index}
/>
<StyledTaskLabel>
<TaskInputLabel label={props.label} labelIcon={props.labelIcon} labelStatus={props.labelStatus} />
</StyledTaskLabel>
</StyledTaskInputField>
</ThemeProvider>
);
}

TaskInputField.defaultProps = {
autoFocus: false,
checked: false,
className: '',
label: '',
labelIcon: null,
Expand All @@ -198,21 +157,13 @@ TaskInputField.defaultProps = {
};

TaskInputField.propTypes = {
annotation: PropTypes.shape({
_key: PropTypes.number,
task: PropTypes.string,
value: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.number), // mulitple choice
PropTypes.number, // single choice
PropTypes.arrayOf(PropTypes.object), // drawing task
PropTypes.object // null
])
}).isRequired,
autoFocus: PropTypes.bool,
checked: PropTypes.bool,
className: PropTypes.string,
index: PropTypes.number.isRequired,
label: PropTypes.string,
labelIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
labelStatus: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
labelStatus: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
name: PropTypes.string,
onChange: PropTypes.func,
theme: PropTypes.string,
Expand All @@ -223,4 +174,4 @@ const mapStateToProps = state => ({
theme: state.userInterface.theme
});

export default connect(mapStateToProps)(TaskInputField);
export default connect(mapStateToProps)(React.memo(TaskInputField));

0 comments on commit f2ecaf2

Please sign in to comment.