Skip to content

Commit

Permalink
chore(Button): use React.forwardRef() (#4256)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Aug 4, 2021
1 parent 2142ba4 commit eef72ce
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 167 deletions.
275 changes: 143 additions & 132 deletions src/elements/Button/Button.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Ref } from '@fluentui/react-component-ref'
import cx from 'clsx'
import _ from 'lodash'
import PropTypes from 'prop-types'
import React, { Component, createRef } from 'react'
import React from 'react'

import {
childrenUtils,
Expand All @@ -14,6 +13,7 @@ import {
useKeyOnly,
useKeyOrValueAndKey,
useValueAndKey,
useMergedRefs,
} from '../../lib'
import Icon from '../Icon/Icon'
import Label from '../Label/Label'
Expand All @@ -22,162 +22,173 @@ import ButtonGroup from './ButtonGroup'
import ButtonOr from './ButtonOr'

/**
* A Button indicates a possible user action.
* @see Form
* @see Icon
* @see Label
* @param {React.ElementType} ElementType
* @param {String} role
*/
class Button extends Component {
ref = createRef()

computeButtonAriaRole(ElementType) {
const { role } = this.props

if (!_.isNil(role)) return role
if (ElementType !== 'button') return 'button'
function computeButtonAriaRole(ElementType, role) {
if (!_.isNil(role)) {
return role
}

computeElementType = () => {
const { attached, label } = this.props
if (ElementType !== 'button') {
return 'button'
}
}

if (!_.isNil(attached) || !_.isNil(label)) return 'div'
/**
* @param {React.ElementType} ElementType
* @param {Boolean} disabled
* @param {Number} tabIndex
*/
function computeTabIndex(ElementType, disabled, tabIndex) {
if (!_.isNil(tabIndex)) {
return tabIndex
}
if (disabled) {
return -1
}
if (ElementType === 'div') {
return 0
}
}

computeTabIndex = (ElementType) => {
const { disabled, tabIndex } = this.props
function hasIconClass(props) {
const { children, content, icon, labelPosition } = props

if (!_.isNil(tabIndex)) return tabIndex
if (disabled) return -1
if (ElementType === 'div') return 0
if (icon === true) {
return true
}

focus = () => _.invoke(this.ref.current, 'focus')
if (icon) {
return labelPosition || (childrenUtils.isNil(children) && _.isNil(content))
}
}

handleClick = (e) => {
const { disabled } = this.props
/**
* A Button indicates a possible user action.
* @see Form
* @see Icon
* @see Label
*/
const Button = React.forwardRef(function (props, ref) {
const {
active,
animated,
attached,
basic,
children,
circular,
className,
color,
compact,
content,
disabled,
floated,
fluid,
icon,
inverted,
label,
labelPosition,
loading,
negative,
positive,
primary,
secondary,
size,
toggle,
} = props
const elementRef = useMergedRefs(ref, React.useRef())

const baseClasses = cx(
color,
size,
useKeyOnly(active, 'active'),
useKeyOnly(basic, 'basic'),
useKeyOnly(circular, 'circular'),
useKeyOnly(compact, 'compact'),
useKeyOnly(fluid, 'fluid'),
useKeyOnly(hasIconClass(props), 'icon'),
useKeyOnly(inverted, 'inverted'),
useKeyOnly(loading, 'loading'),
useKeyOnly(negative, 'negative'),
useKeyOnly(positive, 'positive'),
useKeyOnly(primary, 'primary'),
useKeyOnly(secondary, 'secondary'),
useKeyOnly(toggle, 'toggle'),
useKeyOrValueAndKey(animated, 'animated'),
useKeyOrValueAndKey(attached, 'attached'),
)
const labeledClasses = cx(useKeyOrValueAndKey(labelPosition || !!label, 'labeled'))
const wrapperClasses = cx(useKeyOnly(disabled, 'disabled'), useValueAndKey(floated, 'floated'))

const rest = getUnhandledProps(Button, props)
const ElementType = getElementType(Button, props, () => {
if (!_.isNil(attached) || !_.isNil(label)) {
return 'div'
}
})
const tabIndex = computeTabIndex(ElementType, disabled, props.tabIndex)

const handleClick = (e) => {
if (disabled) {
e.preventDefault()
return
}

_.invoke(this.props, 'onClick', e, this.props)
_.invoke(props, 'onClick', e, props)
}

hasIconClass = () => {
const { labelPosition, children, content, icon } = this.props

if (icon === true) return true
return icon && (labelPosition || (childrenUtils.isNil(children) && _.isNil(content)))
}

render() {
const {
active,
animated,
attached,
basic,
children,
circular,
className,
color,
compact,
content,
disabled,
floated,
fluid,
icon,
inverted,
label,
labelPosition,
loading,
negative,
positive,
primary,
secondary,
size,
toggle,
} = this.props

const baseClasses = cx(
color,
size,
useKeyOnly(active, 'active'),
useKeyOnly(basic, 'basic'),
useKeyOnly(circular, 'circular'),
useKeyOnly(compact, 'compact'),
useKeyOnly(fluid, 'fluid'),
useKeyOnly(this.hasIconClass(), 'icon'),
useKeyOnly(inverted, 'inverted'),
useKeyOnly(loading, 'loading'),
useKeyOnly(negative, 'negative'),
useKeyOnly(positive, 'positive'),
useKeyOnly(primary, 'primary'),
useKeyOnly(secondary, 'secondary'),
useKeyOnly(toggle, 'toggle'),
useKeyOrValueAndKey(animated, 'animated'),
useKeyOrValueAndKey(attached, 'attached'),
)
const labeledClasses = cx(useKeyOrValueAndKey(labelPosition || !!label, 'labeled'))
const wrapperClasses = cx(useKeyOnly(disabled, 'disabled'), useValueAndKey(floated, 'floated'))

const rest = getUnhandledProps(Button, this.props)
const ElementType = getElementType(Button, this.props, this.computeElementType)
const tabIndex = this.computeTabIndex(ElementType)

if (!_.isNil(label)) {
const buttonClasses = cx('ui', baseClasses, 'button', className)
const containerClasses = cx('ui', labeledClasses, 'button', className, wrapperClasses)
const labelElement = Label.create(label, {
defaultProps: {
basic: true,
pointing: labelPosition === 'left' ? 'right' : 'left',
},
autoGenerateKey: false,
})

return (
<ElementType {...rest} className={containerClasses} onClick={this.handleClick}>
{labelPosition === 'left' && labelElement}
<Ref innerRef={this.ref}>
<button
className={buttonClasses}
aria-pressed={toggle ? !!active : undefined}
disabled={disabled}
tabIndex={tabIndex}
>
{Icon.create(icon, { autoGenerateKey: false })} {content}
</button>
</Ref>
{(labelPosition === 'right' || !labelPosition) && labelElement}
</ElementType>
)
}

const classes = cx('ui', baseClasses, wrapperClasses, labeledClasses, 'button', className)
const hasChildren = !childrenUtils.isNil(children)
const role = this.computeButtonAriaRole(ElementType)
if (!_.isNil(label)) {
const buttonClasses = cx('ui', baseClasses, 'button', className)
const containerClasses = cx('ui', labeledClasses, 'button', className, wrapperClasses)
const labelElement = Label.create(label, {
defaultProps: {
basic: true,
pointing: labelPosition === 'left' ? 'right' : 'left',
},
autoGenerateKey: false,
})

return (
<Ref innerRef={this.ref}>
<ElementType
{...rest}
className={classes}
<ElementType {...rest} className={containerClasses} onClick={handleClick}>
{labelPosition === 'left' && labelElement}
<button
className={buttonClasses}
aria-pressed={toggle ? !!active : undefined}
disabled={(disabled && ElementType === 'button') || undefined}
onClick={this.handleClick}
role={role}
disabled={disabled}
tabIndex={tabIndex}
ref={elementRef}
>
{hasChildren && children}
{!hasChildren && Icon.create(icon, { autoGenerateKey: false })}
{!hasChildren && content}
</ElementType>
</Ref>
{Icon.create(icon, { autoGenerateKey: false })} {content}
</button>
{(labelPosition === 'right' || !labelPosition) && labelElement}
</ElementType>
)
}
}

const classes = cx('ui', baseClasses, wrapperClasses, labeledClasses, 'button', className)
const hasChildren = !childrenUtils.isNil(children)
const role = computeButtonAriaRole(ElementType, props.role)

return (
<ElementType
{...rest}
className={classes}
aria-pressed={toggle ? !!active : undefined}
disabled={(disabled && ElementType === 'button') || undefined}
onClick={handleClick}
role={role}
tabIndex={tabIndex}
ref={elementRef}
>
{hasChildren && children}
{!hasChildren && Icon.create(icon, { autoGenerateKey: false })}
{!hasChildren && content}
</ElementType>
)
})

Button.displayName = 'Button'
Button.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
8 changes: 5 additions & 3 deletions src/elements/Button/ButtonContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import {
/**
* Used in some Button types, such as `animated`.
*/
function ButtonContent(props) {
const ButtonContent = React.forwardRef(function (props, ref) {
const { children, className, content, hidden, visible } = props

const classes = cx(
useKeyOnly(visible, 'visible'),
useKeyOnly(hidden, 'hidden'),
Expand All @@ -25,12 +26,13 @@ function ButtonContent(props) {
const ElementType = getElementType(ButtonContent, props)

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

ButtonContent.displayName = 'ButtonContent'
ButtonContent.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
9 changes: 5 additions & 4 deletions src/elements/Button/ButtonGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Button from './Button'
/**
* Buttons can be grouped.
*/
function ButtonGroup(props) {
const ButtonGroup = React.forwardRef(function (props, ref) {
const {
attached,
basic,
Expand Down Expand Up @@ -71,19 +71,20 @@ function ButtonGroup(props) {

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

return (
<ElementType {...rest} className={classes}>
<ElementType {...rest} className={classes} ref={ref}>
{_.map(buttons, (button) => Button.create(button))}
</ElementType>
)
}
})

ButtonGroup.displayName = 'ButtonGroup'
ButtonGroup.propTypes = {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
Expand Down
Loading

0 comments on commit eef72ce

Please sign in to comment.