Skip to content
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

Add Rating Component #339

Merged
merged 3 commits into from
Aug 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Be sure to check out the above migrations before embarking on a new component.
| x Image | | | _ Nag | |
| x Input | | | _ Popup | |
| x Label | | | x Progress | |
| x List | | | _ Rating | |
| x List | | | x Rating | |
| x Loader | | | _ Search | |
| x Rail | | | _ Shape | |
| _ Reveal | | | _ Sidebar | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import { Rating } from 'stardust'

const RatingClearableExample = () => (
<Rating maxRating={5} clearable />
)

export default RatingClearableExample
21 changes: 21 additions & 0 deletions docs/app/Examples/modules/Rating/Types/RatingControlledExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { Component } from 'react'
import { Rating } from 'stardust'

export default class RatingControlledExample extends Component {
state = { rating: 0 }

handleChange = (e) => this.setState({ rating: e.target.value })

render() {
const { rating } = this.state

return (
<div>
<div>Rating: {rating}</div>
<input type='range' min={0} max={5} value={rating} onChange={this.handleChange} />
<br />
<Rating rating={this.state.rating} maxRating={5} />
</div>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import { Rating } from 'stardust'

const RatingDisabledExample = () => (
<Rating defaultRating={3} maxRating={5} disabled />
)

export default RatingDisabledExample
8 changes: 8 additions & 0 deletions docs/app/Examples/modules/Rating/Types/RatingHeartExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import { Rating } from 'stardust'

const RatingHeartExample = () => (
<Rating icon='heart' defaultRating={1} maxRating={3} />
)

export default RatingHeartExample
17 changes: 17 additions & 0 deletions docs/app/Examples/modules/Rating/Types/RatingOnRateExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { Component } from 'react'
import { Rating } from 'stardust'

export default class RatingOnRateExample extends Component {
state = {}

handleRate = (e, { rating, maxRating }) => this.setState({ rating, maxRating })

render() {
return (
<div>
<Rating maxRating={5} onRate={this.handleRate} />
<pre>{JSON.stringify(this.state, null, 2)}</pre>
</div>
)
}
}
8 changes: 8 additions & 0 deletions docs/app/Examples/modules/Rating/Types/RatingRatingExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import { Rating } from 'stardust'

const RatingExample = () => (
<Rating />
)

export default RatingExample
8 changes: 8 additions & 0 deletions docs/app/Examples/modules/Rating/Types/RatingStarExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from 'react'
import { Rating } from 'stardust'

const RatingStarExample = () => (
<Rating icon='star' defaultRating={3} maxRating={4} />
)

export default RatingStarExample
35 changes: 35 additions & 0 deletions docs/app/Examples/modules/Rating/Variations/RatingSizeExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { Rating } from 'stardust'

const RatingSizeExample = () => (
<div>
<Rating maxRating={5} defaultRating={3} icon='star' size='mini' />
<br />
<br />

<Rating maxRating={5} defaultRating={3} icon='star' size='tiny' />
<br />
<br />

<Rating maxRating={5} defaultRating={3} icon='star' size='small' />
<br />
<br />

<Rating maxRating={5} defaultRating={3} icon='star' />
<br />
<br />

<Rating maxRating={5} defaultRating={3} icon='star' size='large' />
<br />
<br />

<Rating maxRating={5} defaultRating={3} icon='star' size='huge' />
<br />
<br />

<Rating maxRating={5} defaultRating={3} icon='star' size='massive' />
<br />
<br />
</div>
)
export default RatingSizeExample
57 changes: 57 additions & 0 deletions docs/app/Examples/modules/Rating/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { Component } from 'react'
import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'

export default class RatingExamples extends Component {
render() {
return (
<div>
<ExampleSection title='Types'>
<ComponentExample
title='Rating'
description='A basic rating'
examplePath='modules/Rating/Types/RatingRatingExample'
/>
<ComponentExample
title='Star'
description='A rating can use a set of star icons'
examplePath='modules/Rating/Types/RatingStarExample'
/>
<ComponentExample
title='Heart'
description='A rating can use a set of heart icons'
examplePath='modules/Rating/Types/RatingHeartExample'
/>
<ComponentExample
title='Clearable'
description='A rating can be cleared by clicking again'
examplePath='modules/Rating/Types/RatingClearableExample'
/>
<ComponentExample
title='Disabled'
description='A rating can be disabled'
examplePath='modules/Rating/Types/RatingDisabledExample'
/>
<ComponentExample
title='Controlled'
description='A rating can be a controlled component'
examplePath='modules/Rating/Types/RatingControlledExample'
/>
<ComponentExample
title='onRate Callback'
description='A rating calls back when the rating changes'
examplePath='modules/Rating/Types/RatingOnRateExample'
/>
</ExampleSection>

<ExampleSection title='Variations'>
<ComponentExample
title='Size'
description='A rating can vary in size'
examplePath='modules/Rating/Variations/RatingSizeExample'
/>
</ExampleSection>
</div>
)
}
}
5 changes: 3 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,16 @@ export Rail from './elements/Rail/Rail'
// ----------------------------------------
export Accordion from './modules/Accordion/Accordion'
export Checkbox from './modules/Checkbox/Checkbox'
export Progress from './modules/Progress/Progress'
export Dropdown from './modules/Dropdown/Dropdown'

import _Modal from './modules/Modal/Modal'
export { _Modal as Modal }
export const ModalContent = deprecateComponent('ModalContent', 'Use "Modal.Content" instead.', _Modal.Content)
export const ModalFooter = deprecateComponent('ModalFooter', 'Use "Modal.Footer" instead.', _Modal.Footer)
export const ModalHeader = deprecateComponent('ModalHeader', 'Use "Modal.Header" instead.', _Modal.Header)

export Dropdown from './modules/Dropdown/Dropdown'
export Progress from './modules/Progress/Progress'
export Rating from './modules/Rating/Rating'

// ----------------------------------------
// Views
Expand Down
156 changes: 156 additions & 0 deletions src/modules/Rating/Rating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import _ from 'lodash'
import cx from 'classnames'
import React, { PropTypes } from 'react'

import AutoControlledComponent from '../../utils/AutoControlledComponent'
import META from '../../utils/Meta'
import { getUnhandledProps } from '../../utils/propUtils'
import * as sui from '../../utils/semanticUtils'

const _meta = {
name: 'Rating',
type: META.type.module,
props: {
clearable: ['auto'],
icon: ['star', 'heart'],
size: _.without(sui.sizes, 'medium', 'big'),
},
}

class Rating extends AutoControlledComponent {
static propTypes = {
/** Additional className. */
className: PropTypes.string,

/**
* You can clear the rating by clicking on the current start rating.
* By default a rating will be only clearable if there is 1 icon.
* Setting to `true`/`false` will allow or disallow a user to clear their rating.
*/
clearable: PropTypes.oneOfType([
PropTypes.oneOf(_meta.props.clearable),
PropTypes.bool,
]),

/** A rating can use a set of star or heart icons. */
icon: PropTypes.oneOf(_meta.props.icon),

/** The total number of icons. */
maxRating: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),

/** The current number of active icons. */
rating: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),

/** The initial rating value. */
defaultRating: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),

/** A progress bar can vary in size. */
size: PropTypes.oneOf(_meta.props.size),

/** You can disable or enable interactive rating. Makes a read-only rating. */
disabled: PropTypes.bool,

/** Called with (event, { rating, maxRating }) after user selects a new rating. */
onRate: PropTypes.func,
}

static defaultProps = {
clearable: 'auto',
maxRating: 1,
}

static _meta = _meta

static autoControlledProps = [
'rating',
]

handleMouseLeave = (...args) => {
_.invoke(this.props, 'onMouseLeave', ...args)

if (this.props.disabled) return

this.setState({ selectedIndex: -1, isSelecting: false })
}

handleIconMouseEnter = (index) => {
if (this.props.disabled) return

this.setState({ selectedIndex: index, isSelecting: true })
}

handleIconClick = (e, index) => {
const { clearable, disabled, maxRating, onRate } = this.props
const { rating } = this.state
if (disabled) return

// default newRating is the clicked icon
// allow toggling a binary rating
// allow clearing ratings
let newRating = index + 1
if (clearable === 'auto' && maxRating === 1) {
newRating = +!rating
} else if (clearable === true && newRating === rating) {
newRating = 0
}

// set rating
this.trySetState({ rating: newRating }, { isSelecting: false })
if (onRate) onRate(e, { rating: newRating, maxRating })
}

renderIcons = () => {
const { maxRating } = this.props
const { rating, selectedIndex, isSelecting } = this.state

return _.times(maxRating, (i) => {
const classes = cx(
selectedIndex >= i && isSelecting && 'selected',
rating >= i + 1 && 'active',
'icon'
)
return (
<i
key={i}
className={classes}
onClick={(e) => this.handleIconClick(e, i)}
onMouseEnter={() => this.handleIconMouseEnter(i)}
/>
)
})
}

render() {
const { className, disabled, icon, size } = this.props
const { selectedIndex, isSelecting } = this.state

const classes = cx(
'ui',
size,
icon,
disabled && 'disabled',
isSelecting && !disabled && selectedIndex >= 0 && 'selected',
'rating',
className,
)

const rest = getUnhandledProps(Rating, this.props)

return (
<div {...rest} className={classes} onMouseLeave={this.handleMouseLeave}>
{this.renderIcons()}
</div>
)
}
}

export default Rating
Loading