Skip to content

Commit a584114

Browse files
committed
chore(Button): use React.forwardRef() (#4256)
1 parent a94a559 commit a584114

File tree

10 files changed

+225
-170
lines changed

10 files changed

+225
-170
lines changed

src/elements/Button/Button.js

Lines changed: 146 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Ref } from '@fluentui/react-component-ref'
21
import cx from 'clsx'
32
import _ from 'lodash'
43
import PropTypes from 'prop-types'
5-
import React, { Component, createRef } from 'react'
4+
import React from 'react'
65

76
import {
87
childrenUtils,
@@ -14,6 +13,7 @@ import {
1413
useKeyOnly,
1514
useKeyOrValueAndKey,
1615
useValueAndKey,
16+
useMergedRefs,
1717
} from '../../lib'
1818
import Icon from '../Icon/Icon'
1919
import Label from '../Label/Label'
@@ -22,165 +22,176 @@ import ButtonGroup from './ButtonGroup'
2222
import ButtonOr from './ButtonOr'
2323

2424
/**
25-
* A Button indicates a possible user action.
26-
* @see Form
27-
* @see Icon
28-
* @see Label
25+
* @param {React.ElementType} ElementType
26+
* @param {String} role
2927
*/
30-
class Button extends Component {
31-
ref = createRef()
32-
33-
computeButtonAriaRole(ElementType) {
34-
const { role } = this.props
35-
36-
if (!_.isNil(role)) return role
37-
if (ElementType !== 'button') return 'button'
28+
function computeButtonAriaRole(ElementType, role) {
29+
if (!_.isNil(role)) {
30+
return role
3831
}
3932

40-
computeElementType = () => {
41-
const { attached, label } = this.props
33+
if (ElementType !== 'button') {
34+
return 'button'
35+
}
36+
}
4237

43-
if (!_.isNil(attached) || !_.isNil(label)) return 'div'
38+
/**
39+
* @param {React.ElementType} ElementType
40+
* @param {Boolean} disabled
41+
* @param {Number} tabIndex
42+
*/
43+
function computeTabIndex(ElementType, disabled, tabIndex) {
44+
if (!_.isNil(tabIndex)) {
45+
return tabIndex
46+
}
47+
if (disabled) {
48+
return -1
49+
}
50+
if (ElementType === 'div') {
51+
return 0
4452
}
53+
}
4554

46-
computeTabIndex = (ElementType) => {
47-
const { disabled, tabIndex } = this.props
55+
function hasIconClass(props) {
56+
const { children, content, icon, labelPosition } = props
4857

49-
if (!_.isNil(tabIndex)) return tabIndex
50-
if (disabled) return -1
51-
if (ElementType === 'div') return 0
58+
if (icon === true) {
59+
return true
5260
}
5361

54-
focus = () => _.invoke(this.ref.current, 'focus')
62+
if (icon) {
63+
return labelPosition || (childrenUtils.isNil(children) && _.isNil(content))
64+
}
65+
}
5566

56-
handleClick = (e) => {
57-
const { disabled } = this.props
67+
/**
68+
* A Button indicates a possible user action.
69+
* @see Form
70+
* @see Icon
71+
* @see Label
72+
*/
73+
const Button = React.forwardRef(function (props, ref) {
74+
const {
75+
active,
76+
animated,
77+
attached,
78+
basic,
79+
children,
80+
circular,
81+
className,
82+
color,
83+
compact,
84+
content,
85+
disabled,
86+
floated,
87+
fluid,
88+
icon,
89+
inverted,
90+
label,
91+
labelPosition,
92+
loading,
93+
negative,
94+
positive,
95+
primary,
96+
secondary,
97+
size,
98+
toggle,
99+
type
100+
} = props
101+
const elementRef = useMergedRefs(ref, React.useRef())
102+
103+
const baseClasses = cx(
104+
color,
105+
size,
106+
useKeyOnly(active, 'active'),
107+
useKeyOnly(basic, 'basic'),
108+
useKeyOnly(circular, 'circular'),
109+
useKeyOnly(compact, 'compact'),
110+
useKeyOnly(fluid, 'fluid'),
111+
useKeyOnly(hasIconClass(props), 'icon'),
112+
useKeyOnly(inverted, 'inverted'),
113+
useKeyOnly(loading, 'loading'),
114+
useKeyOnly(negative, 'negative'),
115+
useKeyOnly(positive, 'positive'),
116+
useKeyOnly(primary, 'primary'),
117+
useKeyOnly(secondary, 'secondary'),
118+
useKeyOnly(toggle, 'toggle'),
119+
useKeyOrValueAndKey(animated, 'animated'),
120+
useKeyOrValueAndKey(attached, 'attached'),
121+
)
122+
const labeledClasses = cx(useKeyOrValueAndKey(labelPosition || !!label, 'labeled'))
123+
const wrapperClasses = cx(useKeyOnly(disabled, 'disabled'), useValueAndKey(floated, 'floated'))
124+
125+
const rest = getUnhandledProps(Button, props)
126+
const ElementType = getElementType(Button, props, () => {
127+
if (!_.isNil(attached) || !_.isNil(label)) {
128+
return 'div'
129+
}
130+
})
131+
const tabIndex = computeTabIndex(ElementType, disabled, props.tabIndex)
58132

133+
const handleClick = (e) => {
59134
if (disabled) {
60135
e.preventDefault()
61136
return
62137
}
63138

64-
_.invoke(this.props, 'onClick', e, this.props)
65-
}
66-
67-
hasIconClass = () => {
68-
const { labelPosition, children, content, icon } = this.props
69-
70-
if (icon === true) return true
71-
return icon && (labelPosition || (childrenUtils.isNil(children) && _.isNil(content)))
139+
_.invoke(props, 'onClick', e, props)
72140
}
73141

74-
render() {
75-
const {
76-
active,
77-
animated,
78-
attached,
79-
basic,
80-
children,
81-
circular,
82-
className,
83-
color,
84-
compact,
85-
content,
86-
disabled,
87-
floated,
88-
fluid,
89-
icon,
90-
inverted,
91-
label,
92-
labelPosition,
93-
loading,
94-
negative,
95-
positive,
96-
primary,
97-
secondary,
98-
size,
99-
toggle,
100-
type,
101-
} = this.props
102-
103-
const baseClasses = cx(
104-
color,
105-
size,
106-
useKeyOnly(active, 'active'),
107-
useKeyOnly(basic, 'basic'),
108-
useKeyOnly(circular, 'circular'),
109-
useKeyOnly(compact, 'compact'),
110-
useKeyOnly(fluid, 'fluid'),
111-
useKeyOnly(this.hasIconClass(), 'icon'),
112-
useKeyOnly(inverted, 'inverted'),
113-
useKeyOnly(loading, 'loading'),
114-
useKeyOnly(negative, 'negative'),
115-
useKeyOnly(positive, 'positive'),
116-
useKeyOnly(primary, 'primary'),
117-
useKeyOnly(secondary, 'secondary'),
118-
useKeyOnly(toggle, 'toggle'),
119-
useKeyOrValueAndKey(animated, 'animated'),
120-
useKeyOrValueAndKey(attached, 'attached'),
121-
)
122-
const labeledClasses = cx(useKeyOrValueAndKey(labelPosition || !!label, 'labeled'))
123-
const wrapperClasses = cx(useKeyOnly(disabled, 'disabled'), useValueAndKey(floated, 'floated'))
124-
125-
const rest = getUnhandledProps(Button, this.props)
126-
const ElementType = getElementType(Button, this.props, this.computeElementType)
127-
const tabIndex = this.computeTabIndex(ElementType)
128-
129-
if (!_.isNil(label)) {
130-
const buttonClasses = cx('ui', baseClasses, 'button', className)
131-
const containerClasses = cx('ui', labeledClasses, 'button', className, wrapperClasses)
132-
const labelElement = Label.create(label, {
133-
defaultProps: {
134-
basic: true,
135-
pointing: labelPosition === 'left' ? 'right' : 'left',
136-
},
137-
autoGenerateKey: false,
138-
})
139-
140-
return (
141-
<ElementType {...rest} className={containerClasses} onClick={this.handleClick}>
142-
{labelPosition === 'left' && labelElement}
143-
<Ref innerRef={this.ref}>
144-
<button
145-
className={buttonClasses}
146-
aria-pressed={toggle ? !!active : undefined}
147-
disabled={disabled}
148-
type={type}
149-
tabIndex={tabIndex}
150-
>
151-
{Icon.create(icon, { autoGenerateKey: false })} {content}
152-
</button>
153-
</Ref>
154-
{(labelPosition === 'right' || !labelPosition) && labelElement}
155-
</ElementType>
156-
)
157-
}
158-
159-
const classes = cx('ui', baseClasses, wrapperClasses, labeledClasses, 'button', className)
160-
const hasChildren = !childrenUtils.isNil(children)
161-
const role = this.computeButtonAriaRole(ElementType)
142+
if (!_.isNil(label)) {
143+
const buttonClasses = cx('ui', baseClasses, 'button', className)
144+
const containerClasses = cx('ui', labeledClasses, 'button', className, wrapperClasses)
145+
const labelElement = Label.create(label, {
146+
defaultProps: {
147+
basic: true,
148+
pointing: labelPosition === 'left' ? 'right' : 'left',
149+
},
150+
autoGenerateKey: false,
151+
})
162152

163153
return (
164-
<Ref innerRef={this.ref}>
165-
<ElementType
166-
{...rest}
167-
className={classes}
154+
<ElementType {...rest} className={containerClasses} onClick={handleClick}>
155+
{labelPosition === 'left' && labelElement}
156+
<button
157+
className={buttonClasses}
168158
aria-pressed={toggle ? !!active : undefined}
169-
disabled={(disabled && ElementType === 'button') || undefined}
170-
onClick={this.handleClick}
171-
role={role}
172-
type={type}
159+
disabled={disabled}
173160
tabIndex={tabIndex}
161+
type={type}
162+
ref={elementRef}
174163
>
175-
{hasChildren && children}
176-
{!hasChildren && Icon.create(icon, { autoGenerateKey: false })}
177-
{!hasChildren && content}
178-
</ElementType>
179-
</Ref>
164+
{Icon.create(icon, { autoGenerateKey: false })} {content}
165+
</button>
166+
{(labelPosition === 'right' || !labelPosition) && labelElement}
167+
</ElementType>
180168
)
181169
}
182-
}
183170

171+
const classes = cx('ui', baseClasses, wrapperClasses, labeledClasses, 'button', className)
172+
const hasChildren = !childrenUtils.isNil(children)
173+
const role = computeButtonAriaRole(ElementType, props.role)
174+
175+
return (
176+
<ElementType
177+
{...rest}
178+
className={classes}
179+
aria-pressed={toggle ? !!active : undefined}
180+
disabled={(disabled && ElementType === 'button') || undefined}
181+
onClick={handleClick}
182+
role={role}
183+
tabIndex={tabIndex}
184+
type={type}
185+
ref={elementRef}
186+
>
187+
{hasChildren && children}
188+
{!hasChildren && Icon.create(icon, { autoGenerateKey: false })}
189+
{!hasChildren && content}
190+
</ElementType>
191+
)
192+
})
193+
194+
Button.displayName = 'Button'
184195
Button.propTypes = {
185196
/** An element type to render as (string or function). */
186197
as: PropTypes.elementType,

src/elements/Button/ButtonContent.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import {
1313
/**
1414
* Used in some Button types, such as `animated`.
1515
*/
16-
function ButtonContent(props) {
16+
const ButtonContent = React.forwardRef(function (props, ref) {
1717
const { children, className, content, hidden, visible } = props
18+
1819
const classes = cx(
1920
useKeyOnly(visible, 'visible'),
2021
useKeyOnly(hidden, 'hidden'),
@@ -25,12 +26,13 @@ function ButtonContent(props) {
2526
const ElementType = getElementType(ButtonContent, props)
2627

2728
return (
28-
<ElementType {...rest} className={classes}>
29+
<ElementType {...rest} className={classes} ref={ref}>
2930
{childrenUtils.isNil(children) ? content : children}
3031
</ElementType>
3132
)
32-
}
33+
})
3334

35+
ButtonContent.displayName = 'ButtonContent'
3436
ButtonContent.propTypes = {
3537
/** An element type to render as (string or function). */
3638
as: PropTypes.elementType,

src/elements/Button/ButtonGroup.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Button from './Button'
1919
/**
2020
* Buttons can be grouped.
2121
*/
22-
function ButtonGroup(props) {
22+
const ButtonGroup = React.forwardRef(function (props, ref) {
2323
const {
2424
attached,
2525
basic,
@@ -71,19 +71,20 @@ function ButtonGroup(props) {
7171

7272
if (_.isNil(buttons)) {
7373
return (
74-
<ElementType {...rest} className={classes}>
74+
<ElementType {...rest} className={classes} ref={ref}>
7575
{childrenUtils.isNil(children) ? content : children}
7676
</ElementType>
7777
)
7878
}
7979

8080
return (
81-
<ElementType {...rest} className={classes}>
81+
<ElementType {...rest} className={classes} ref={ref}>
8282
{_.map(buttons, (button) => Button.create(button))}
8383
</ElementType>
8484
)
85-
}
85+
})
8686

87+
ButtonGroup.displayName = 'ButtonGroup'
8788
ButtonGroup.propTypes = {
8889
/** An element type to render as (string or function). */
8990
as: PropTypes.elementType,

0 commit comments

Comments
 (0)