New form field: star rating

This commit is contained in:
Vesa Luusua 2017-11-28 18:27:34 +02:00
parent c8f22319f8
commit 993c06f162
7 changed files with 244 additions and 0 deletions

View file

@ -0,0 +1,51 @@
@import '../../marketplace.css';
.ratingFieldSet {
border: none;
padding: 0;
margin: 0;
}
/* fieldset can't use flexbox hence this wrapper exists */
.rating {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
margin-top: 16px;
}
.rateInput {
display: none;
}
.label {
width: 30px;
}
.star {
fill: var(--matterColorNegative);
}
/***** CSS Magic to Highlight Stars on Hover *****/
/* Star order: reverse expected (5 -> 1) and also input before label */
/* show actived star when checked */
/* and actived star when hovering over a star */
/* and show previous stars also as activated */
.rating > .rateInput:checked ~ .label > .star,
.rating > .label:hover > .star,
.rating > .label:hover ~ .label > .star {
fill: var(--marketplaceColor);
}
/* Darken hovered star when changing rating i.e it already is active */
/* and darken hovered star too when changing rating (hovering inside current selection) */
/* and darken current selection inside hovered selection */
/* and darken hovered selection inside current selection */
.rating > .rateInput:checked + .label:hover > .star,
.rating > .rateInput:checked ~ .label:hover > .star,
.rating > .label:hover ~ .rateInput:checked ~ .label > .star,
.rating > .rateInput:checked ~ .label:hover ~ .label > .star {
fill: var(--marketplaceColorDark);
}

View file

@ -0,0 +1,44 @@
/* eslint-disable no-console */
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldStarRating from './FieldStarRating';
const formName = 'Styleguide.FieldStarRating.Form';
const FormComponent = props => {
const { form, handleSubmit, invalid, pristine, submitting } = props;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldStarRating
id={`${form}.rate1`}
name="rating"
label="Rate your experience"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
};
FormComponent.propTypes = formPropTypes;
const Form = reduxForm({
form: formName,
})(FormComponent);
export const StarRating = {
component: Form,
props: {
onSubmit: values => {
console.log('submit values:', values);
},
},
group: 'inputs',
};

View file

@ -0,0 +1,140 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import { Field } from 'redux-form';
import classNames from 'classnames';
import { IconReviewStar, ValidationError } from '../../components';
import css from './FieldStarRating.css';
class FieldStarRatingComponent extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
componentWillUnmount() {
if (this.props.clearOnUnmount) {
this.props.input.onChange('');
}
}
handleChange(event) {
this.props.input.onChange(event.target.value);
}
render() {
/* eslint-disable no-unused-vars */
const {
rootClassName,
className,
inputRootClass,
clearOnUnmount,
customErrorText,
id,
intl,
label,
input,
meta,
...rest
} = this.props;
/* eslint-enable no-unused-vars */
const { touched, error } = meta;
const errorText = customErrorText || error;
const fieldMeta = { touched, error: errorText };
const { value, ...restInputProps } = input;
const inputProps = { ...restInputProps, type: 'radio', name: 'rating', ...rest };
const classes = classNames(rootClassName || css.root, className);
const createStarRating = starCount => {
let inputsAndLabels = [];
// Star inpu order: reverse order expected (5 -> 1) and also input before label
// This is due to CSS selectors.
// Sibling combinator (~) selects following siblings, but we want to select previous siblings
for (let i = starCount; i > 0; i--) {
const inputValue = `${i}`;
const starId = `star${i}`;
const inputId = `${id}.${starId}`;
inputsAndLabels.push(
<input
key={inputId}
id={inputId}
className={css.rateInput}
value={inputValue}
checked={value === inputValue}
{...inputProps}
/>
);
inputsAndLabels.push(
<label
key={`label.${inputId}`}
className={css.label}
htmlFor={inputId}
title={intl.formatMessage({ id: `FieldStarRating.${starId}` })}
>
<IconReviewStar rootClassName={css.star} />
</label>
);
}
return inputsAndLabels;
};
return (
<div className={classes}>
<fieldset
className={css.ratingFieldSet}
ref={c => {
this.ratingFieldSet = c;
}}
>
{label ? <legend>{label}</legend> : null}
<div className={css.rating}>{createStarRating(5)}</div>
</fieldset>
<ValidationError fieldMeta={fieldMeta} />
</div>
);
}
}
FieldStarRatingComponent.defaultProps = {
rootClassName: null,
className: null,
clearOnUnmount: false,
customErrorText: null,
label: null,
};
const { string, bool, shape, func, object } = PropTypes;
FieldStarRatingComponent.propTypes = {
rootClassName: string,
className: string,
clearOnUnmount: bool,
id: string.isRequired,
label: string,
// Error message that can be manually passed to input field,
// overrides default validation message
customErrorText: string,
// Generated by redux-form's Field component
input: shape({
onChange: func.isRequired,
}).isRequired,
meta: object.isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const FieldStarRating = props => {
return <Field component={FieldStarRatingComponent} {...props} />;
};
export default injectIntl(FieldStarRating);

View file

@ -27,6 +27,7 @@ export { default as EditListingWizard } from './EditListingWizard/EditListingWiz
export { default as ExpandingTextarea } from './ExpandingTextarea/ExpandingTextarea';
export { default as ExternalLink } from './ExternalLink/ExternalLink';
export { default as FilterPanel } from './FilterPanel/FilterPanel';
export { default as FieldStarRating } from './FieldStarRating/FieldStarRating';
export { default as Footer } from './Footer/Footer';
export { default as Form } from './Form/Form';
export { default as IconBannedUser } from './IconBannedUser/IconBannedUser';

View file

@ -10,6 +10,7 @@ import * as DateInputField from './components/DateInputField/DateInputField.exam
import * as DateRangeInputField from './components/DateRangeInputField/DateRangeInputField.example';
import * as EditListingWizard from './components/EditListingWizard/EditListingWizard.example';
import * as ExpandingTextarea from './components/ExpandingTextarea/ExpandingTextarea.example';
import * as FieldStarRating from './components/FieldStarRating/FieldStarRating.example';
import * as Footer from './components/Footer/Footer.example';
import * as IconBannedUser from './components/IconBannedUser/IconBannedUser.example';
import * as IconCheckmark from './components/IconCheckmark/IconCheckmark.example';
@ -82,6 +83,7 @@ export {
EditListingWizard,
EmailVerificationForm,
ExpandingTextarea,
FieldStarRating,
Footer,
IconBannedUser,
IconCheckmark,

View file

@ -71,6 +71,7 @@ ul {
padding: 0;
}
legend,
label {
@apply --marketplaceH4FontStyles;
font-weight: var(--fontWeightSemiBold);

View file

@ -169,6 +169,11 @@
"EmailVerificationForm.verifying": "Verifying…",
"EmailVerificationPage.loadingUserInformation": "Loading user information…",
"EmailVerificationPage.title": "Verify your email address",
"FieldStarRating.star1": "Bad experience - 1 star",
"FieldStarRating.star2": "Not so nice - 2 stars",
"FieldStarRating.star3": "OK - 3 stars",
"FieldStarRating.star4": "Good - 4 stars",
"FieldStarRating.star5": "Awesome - 5 stars",
"Footer.copyright": "© Sharetribe 2017",
"Footer.goToFacebook": "Go to Facebook page",
"Footer.goToInstagram": "Go to Instagram page",