Skip to content

Commit

Permalink
Merge pull request #88 from bocoup/form-flow-improvements
Browse files Browse the repository at this point in the history
Re-structure annotation form for better ux flow with fewer errors
  • Loading branch information
kadamwhite committed Mar 20, 2017
2 parents 0037667 + 31a08b7 commit b12ad55
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 95 deletions.
11 changes: 10 additions & 1 deletion frontend/components/App.styl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
html {
font-size: 16px;
}

body {
font-family: sans-serif;
margin: 0;
Expand Down Expand Up @@ -31,11 +35,16 @@ header {
footer {
border-top: 1px solid black;
font-size: 2em;
position: absolute;
// position: absolute;
bottom: 0;
text-align: center;
}

form {
overflow: hidden;
margin-bottom: 1em;
}

label {
display: block;
margin: 5px 0px;
Expand Down
35 changes: 12 additions & 23 deletions frontend/components/PerceivedDemographicQuestion.jsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,34 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';

import strToId from '../utils/str-to-id';

import RadioButtonOption from './RadioButtonOption';

import styles from './PerceivedDemographics.styl';

// ESLint didn't detect all props as being used without this destructuring...
const PerceivedDemographicQuestion = ({
className,
visible,
name,
options,
onChange,
selected,
}) => (
<fieldset
className={`${className} ${styles.fieldset}`}
style={{
display: visible ? 'block' : 'none',
}}
>
<fieldset className={classNames(className, styles.fieldset)}>
<legend>{name}</legend>
{options.map((option) => {
const optionKey = `${strToId(name)}_${strToId(option)}`;
return (
<label
<RadioButtonOption
key={optionKey}
className={styles.radioButton}
htmlFor={optionKey}
>
<input
id={optionKey}
type="radio"
name={name}
value={option}
onChange={onChange}
checked={option === selected}
required
/>
{option}
</label>
id={optionKey}
name={name}
value={option}
checked={option === selected}
onChange={onChange}
required
/>
);
})}
</fieldset>
Expand All @@ -49,7 +39,6 @@ PerceivedDemographicQuestion.propTypes = {
name: PropTypes.string.isRequired,
options: PropTypes.arrayOf(PropTypes.string).isRequired,
selected: PropTypes.string,
visible: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
};

Expand Down
119 changes: 70 additions & 49 deletions frontend/components/PerceivedDemographics.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';

import strToId from '../utils/str-to-id';
import valuesChanged from '../utils/values-changed';

import * as propShapes from '../prop-shapes';

import PerceivedDemographicQuestion from './PerceivedDemographicQuestion';
import ProgressBar from './ProgressBar';
import ProportionalContainer from './ProportionalContainer';

import styles from './PerceivedDemographics.styl';

Expand All @@ -21,7 +24,6 @@ class PerceivedDemographics extends Component {
this.handleInputChange = this.handleInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.prevStep = this.prevStep.bind(this);
this.nextStep = this.nextStep.bind(this);
}

componentDidMount() {
Expand Down Expand Up @@ -59,12 +61,20 @@ class PerceivedDemographics extends Component {
const value = (target.type === 'checkbox') ? target.checked : target.value;
const name = target.name;

this.setState({
[name]: value,
this.setState(({ currentStep }, { questionOrder }) => {
const nextStep = currentStep + 1;
const maxStep = questionOrder.length;
return {
// Persist value
[name]: value,
// Auto-advance to the next step, preventing out-of-bounds values
currentStep: nextStep > maxStep ? maxStep : nextStep,
};
});
}

handleSubmit(event) {
console.log('Submitted!');
event.preventDefault();
this.props.onSubmit(this.prepareAnnotationsObject());
if (this.props.current >= this.props.total) {
Expand All @@ -73,76 +83,87 @@ class PerceivedDemographics extends Component {
}

prevStep() {
const { currentStep } = this.state;
const prevStep = currentStep - 1;
this.setState({
// Prevent out-of-bounds prevStep value
currentStep: prevStep <= 0 ? 0 : prevStep,
});
}

nextStep() {
const { currentStep } = this.state;
const nextStep = currentStep + 1;
this.setState({
// Prevent out-of-bounds nextStep value
currentStep: nextStep >= 2 ? 2 : nextStep,
// pull currentStep out of this.state to compute prev step
this.setState(({ currentStep }) => {
const prevStep = currentStep - 1;
return {
// Prevent out-of-bounds prevStep value
currentStep: prevStep <= 0 ? 0 : prevStep,
};
});
}

render() {
const { currentStep } = this.state;
const { demographicAttributes, questionOrder } = this.props;
window.props = this.props;
const {
demographicAttributes,
questionOrder,
current: currentImage,
total: totalImages,
} = this.props;
return (
<div>
<img
src={this.props.image && this.props.image.url}
alt="A face to label with perceived demographic information"
<ProportionalContainer maxWidth="500px" widthHeightRatio={1}>
<img
src={this.props.image && this.props.image.url}
alt="A face to label with perceived demographic information"
/>
</ProportionalContainer>
<ProgressBar
className={styles.progressBar}
incrementName="Image"
current={currentImage}
total={totalImages}
/>
<ProgressBar
className={styles.progressBar}
incrementName="Step"
current={currentStep + 1}
total={questionOrder.length}
/>
<form onSubmit={this.handleSubmit}>
{questionOrder.map((questionId, idx) => {
const { id, name, options } = demographicAttributes[questionId];
{questionOrder.map((questionName, idx) => {
const { id, name, options } = demographicAttributes[questionName];
return (
<PerceivedDemographicQuestion
key={`question_${strToId(name)}_${id}`}
className={currentStep !== idx ? styles.hidden : ''}
name={name}
options={options}
selected={this.state[name]}
visible={currentStep === idx}
onChange={this.handleInputChange}
/>
);
})}

<div className={styles.carousel}>
{currentStep >= questionOrder.length ? (
<div role="alert">
<p>Review your annotations</p>
<ul>{questionOrder.map((questionName) => {
const { name } = demographicAttributes[questionName];
const value = this.state[name];
return (
<li key={`confirmation_${name}`}>
{name}: <strong>{value}</strong>
</li>
);
})}</ul>
</div>
) : null}

{currentStep !== 0 ? (
<button
className={styles.prev}
type="button"
onClick={this.prevStep}
>Back</button>

<span className={styles.current}>
Step {currentStep + 1} of {questionOrder.length};
{' '}
Image {this.props.current} of {this.props.total}
</span>

{currentStep < 2 ? (
<button
className={styles.next}
type="button"
onClick={this.nextStep}
>Next Step</button>
) : null}

{currentStep >= 2 ? (
<button
className={`${styles.next} ${styles.save}`}
type="submit"
>Submit</button>
) : null}
</div>
) : null}

<button
className={classNames(styles.save, {
[styles.hidden]: currentStep < questionOrder.length,
})}
type="submit"
>Submit Annotations</button>
</form>
</div>
);
Expand Down
32 changes: 10 additions & 22 deletions frontend/components/PerceivedDemographics.styl
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
label {
display: block;
margin: 10px 0px;
}

.radioButton {
text-transform: capitalize;
}
@import './shared/mixins.styl';

.carousel {
.progressBar {
margin: 0.5em 0;
width: 100%;
text-align: center;
}
.prev,
.current,
.next {
display: inline-block;
text-align: center;
}

.prev,
.next {
padding: 5px 10px;
border: 1px solid grey;
background-color: #ececec;
.save {
button();
}
.prev {
float: left;
}
.next {
.save {
float: right;
}

.hidden {
visually-hidden();
}
46 changes: 46 additions & 0 deletions frontend/components/ProgressBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';

import styles from './ProgressBar.styl';

const ProgressBar = ({
className,
incrementName,
current,
total,
}) => {
const complete = current - 1;
const widthPct = (100 / total) * complete;
return (
<div className={classNames(className, styles.barContainer)}>
<span className={styles.label}>
{incrementName} {current} of {total}
</span>
<div
className={classNames(styles.bar, {
// Add a class to apply smooth transitions after the first step
[styles.inProgress]: complete > 0,
})}
style={{
width: `${widthPct}%`,
}}
/>
</div>
);
};

ProgressBar.propTypes = {
className: PropTypes.string,
// e.g. "Step" or "Image"
incrementName: PropTypes.string.isRequired,
// "current" starts at 1
current: PropTypes.number.isRequired,
// Total item count
total: PropTypes.number.isRequired,
};

ProgressBar.defaultProps = {
className: '',
};

export default ProgressBar;
20 changes: 20 additions & 0 deletions frontend/components/ProgressBar.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@import './shared/mixins.styl';

.label {
visually-hidden();
}

.barContainer {
height: 5px;
width: 100%;
border: 1px solid #444;
}

.bar {
height: 5px;
background: #222;
}

.inProgress {
transition: width 200ms;
}

0 comments on commit b12ad55

Please sign in to comment.