Skip to content

Commit

Permalink
chore(Rating|Reveal): use React.forwardRef() (#4258)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Apr 22, 2022
1 parent 0275fd4 commit dcc7343
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 99 deletions.
7 changes: 4 additions & 3 deletions src/elements/Reveal/Reveal.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import RevealContent from './RevealContent'
/**
* A reveal displays additional content in place of previous content when activated.
*/
function Reveal(props) {
const Reveal = React.forwardRef(function (props, ref) {
const { active, animated, children, className, content, disabled, instant } = props

const classes = cx(
Expand All @@ -30,12 +30,13 @@ function Reveal(props) {
const ElementType = getElementType(Reveal, props)

return (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{childrenUtils.isNil(children) ? content : children}
</ElementType>
)
}
})

Reveal.displayName = 'Reveal'
Reveal.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
7 changes: 4 additions & 3 deletions src/elements/Reveal/RevealContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
/**
* A content sub-component for the Reveal.
*/
function RevealContent(props) {
const RevealContent = React.forwardRef(function (props, ref) {
const { children, className, content, hidden, visible } = props

const classes = cx(
Expand All @@ -27,12 +27,13 @@ function RevealContent(props) {
const ElementType = getElementType(RevealContent, props)

return (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{childrenUtils.isNil(children) ? content : children}
</ElementType>
)
}
})

RevealContent.displayName = 'RevealContent'
RevealContent.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
137 changes: 77 additions & 60 deletions src/modules/Rating/Rating.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,95 +4,112 @@ import PropTypes from 'prop-types'
import React from 'react'

import {
ModernAutoControlledComponent as Component,
getElementType,
getUnhandledProps,
SUI,
useKeyOnly,
useAutoControlledValue,
} from '../../lib'
import RatingIcon from './RatingIcon'

/**
* A rating indicates user interest in content.
*/
export default class Rating extends Component {
handleIconClick = (e, { index }) => {
const { clearable, disabled, maxRating, onRate } = this.props
const { rating } = this.state
if (disabled) return
const Rating = React.forwardRef(function (props, ref) {
const { className, clearable, disabled, icon, maxRating, size } = props

const [rating, setRating] = useAutoControlledValue({
state: props.rating,
defaultState: props.defaultRating,
initialState: 0,
})
const [selectedIndex, setSelectedIndex] = React.useState(-1)
const [isSelecting, setIsSelecting] = React.useState(false)

const classes = cx(
'ui',
icon,
size,
useKeyOnly(disabled, 'disabled'),
useKeyOnly(isSelecting && !disabled && selectedIndex >= 0, 'selected'),
'rating',
className,
)
const rest = getUnhandledProps(Rating, props)
const ElementType = getElementType(Rating, props)

const handleIconClick = (e, { index }) => {
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.setState({ rating: newRating, isSelecting: false })
if (onRate) onRate(e, { ...this.props, rating: newRating })
setRating(newRating)
setIsSelecting(false)

_.invoke(props, 'onRate', e, { ...props, rating: newRating })
}

handleIconMouseEnter = (e, { index }) => {
if (this.props.disabled) return
const handleIconMouseEnter = (e, { index }) => {
if (disabled) {
return
}

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

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

if (this.props.disabled) return
const handleMouseLeave = (...args) => {
_.invoke(props, 'onMouseLeave', ...args)

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

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

const classes = cx(
'ui',
icon,
size,
useKeyOnly(disabled, 'disabled'),
useKeyOnly(isSelecting && !disabled && selectedIndex >= 0, 'selected'),
'rating',
className,
)
const rest = getUnhandledProps(Rating, this.props)
const ElementType = getElementType(Rating, this.props)

return (
<ElementType
{...rest}
className={classes}
role='radiogroup'
onMouseLeave={this.handleMouseLeave}
tabIndex={disabled ? 0 : -1}
>
{_.times(maxRating, (i) => (
<RatingIcon
tabIndex={disabled ? -1 : 0}
active={rating >= i + 1}
aria-checked={rating === i + 1}
aria-posinset={i + 1}
aria-setsize={maxRating}
index={i}
key={i}
onClick={this.handleIconClick}
onMouseEnter={this.handleIconMouseEnter}
selected={selectedIndex >= i && isSelecting}
/>
))}
</ElementType>
)
setSelectedIndex(-1)
setIsSelecting(false)
}
}

return (
<ElementType
role='radiogroup'
{...rest}
className={classes}
onMouseLeave={handleMouseLeave}
ref={ref}
tabIndex={disabled ? 0 : -1}
>
{_.times(maxRating, (i) => (
/* TODO: use .create() factory */
<RatingIcon
tabIndex={disabled ? -1 : 0}
active={rating >= i + 1}
aria-checked={rating === i + 1}
aria-posinset={i + 1}
aria-setsize={maxRating}
index={i}
key={i}
onClick={handleIconClick}
onMouseEnter={handleIconMouseEnter}
selected={selectedIndex >= i && isSelecting}
/>
))}
</ElementType>
)
})

Rating.displayName = 'Rating'
Rating.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down Expand Up @@ -134,11 +151,11 @@ Rating.propTypes = {
size: PropTypes.oneOf(_.without(SUI.SIZES, 'medium', 'big')),
}

Rating.autoControlledProps = ['rating']

Rating.defaultProps = {
clearable: 'auto',
maxRating: 1,
}

Rating.Icon = RatingIcon

export default Rating
69 changes: 36 additions & 33 deletions src/modules/Rating/RatingIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,60 @@ import cx from 'clsx'
import keyboardKey from 'keyboard-key'
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import React from 'react'

import { getElementType, getUnhandledProps, useKeyOnly } from '../../lib'

/**
* An internal icon sub-component for Rating component
*/
export default class RatingIcon extends Component {
handleClick = (e) => {
_.invoke(this.props, 'onClick', e, this.props)
const RatingIcon = React.forwardRef(function (props, ref) {
const { active, className, selected } = props

const classes = cx(
useKeyOnly(active, 'active'),
useKeyOnly(selected, 'selected'),
'icon',
className,
)
const rest = getUnhandledProps(RatingIcon, props)
const ElementType = getElementType(RatingIcon, props)

const handleClick = (e) => {
_.invoke(props, 'onClick', e, props)
}

handleKeyUp = (e) => {
_.invoke(this.props, 'onKeyUp', e, this.props)
const handleKeyUp = (e) => {
_.invoke(props, 'onKeyUp', e, props)

switch (keyboardKey.getCode(e)) {
case keyboardKey.Enter:
case keyboardKey.Spacebar:
e.preventDefault()
_.invoke(this.props, 'onClick', e, this.props)
_.invoke(props, 'onClick', e, props)
break
default:
}
}

handleMouseEnter = (e) => {
_.invoke(this.props, 'onMouseEnter', e, this.props)
const handleMouseEnter = (e) => {
_.invoke(props, 'onMouseEnter', e, props)
}

render() {
const { active, className, selected } = this.props
const classes = cx(
useKeyOnly(active, 'active'),
useKeyOnly(selected, 'selected'),
'icon',
className,
)
const rest = getUnhandledProps(RatingIcon, this.props)
const ElementType = getElementType(RatingIcon, this.props)

return (
<ElementType
{...rest}
className={classes}
onClick={this.handleClick}
onKeyUp={this.handleKeyUp}
onMouseEnter={this.handleMouseEnter}
role='radio'
/>
)
}
}

return (
<ElementType
role='radio'
{...rest}
className={classes}
onClick={handleClick}
onKeyUp={handleKeyUp}
onMouseEnter={handleMouseEnter}
ref={ref}
/>
)
})

RatingIcon.displayName = 'RatingIcon'
RatingIcon.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down Expand Up @@ -99,3 +100,5 @@ RatingIcon.propTypes = {
RatingIcon.defaultProps = {
as: 'i',
}

export default RatingIcon
1 change: 1 addition & 0 deletions test/specs/elements/Reveal/Reveal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as common from 'test/specs/commonTests'

describe('Reveal', () => {
common.isConformant(Reveal)
common.forwardsRef(Reveal)
common.hasSubcomponents(Reveal, [RevealContent])
common.hasUIClassName(Reveal)
common.rendersChildren(Reveal)
Expand Down
1 change: 1 addition & 0 deletions test/specs/elements/Reveal/RevealContent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import RevealContent from 'src/elements/Reveal/RevealContent'

describe('RevealContent', () => {
common.isConformant(RevealContent)
common.forwardsRef(RevealContent)
common.rendersChildren(RevealContent)

common.propKeyOnlyToClassName(RevealContent, 'hidden')
Expand Down
1 change: 1 addition & 0 deletions test/specs/modules/Rating/Rating-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { sandbox } from 'test/utils'

describe('Rating', () => {
common.isConformant(Rating)
common.forwardsRef(Rating)
common.hasUIClassName(Rating)

common.propKeyOnlyToClassName(Rating, 'disabled')
Expand Down
1 change: 1 addition & 0 deletions test/specs/modules/Rating/RatingIcon-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { sandbox } from 'test/utils'

describe('RatingIcon', () => {
common.isConformant(RatingIcon)
common.forwardsRef(RatingIcon, { tagName: 'i' })

common.propKeyOnlyToClassName(RatingIcon, 'active')
common.propKeyOnlyToClassName(RatingIcon, 'selected')
Expand Down

0 comments on commit dcc7343

Please sign in to comment.