diff --git a/src/addons/Confirm/Confirm.js b/src/addons/Confirm/Confirm.js index 99d285f27e..0f51287af6 100644 --- a/src/addons/Confirm/Confirm.js +++ b/src/addons/Confirm/Confirm.js @@ -1,6 +1,6 @@ import _ from 'lodash' import PropTypes from 'prop-types' -import React, { Component } from 'react' +import React from 'react' import { customPropTypes, getUnhandledProps } from '../../lib' import Button from '../../elements/Button' @@ -10,55 +10,56 @@ import Modal from '../../modules/Modal' * A Confirm modal gives the user a choice to confirm or cancel an action/ * @see Modal */ -class Confirm extends Component { - handleCancel = (e) => { - _.invoke(this.props, 'onCancel', e, this.props) +const Confirm = React.forwardRef(function (props, ref) { + const { cancelButton, confirmButton, content, header, open, size } = props + const rest = getUnhandledProps(Confirm, props) + + const handleCancel = (e) => { + _.invoke(props, 'onCancel', e, props) } - handleCancelOverrides = (predefinedProps) => ({ + const handleCancelOverrides = (predefinedProps) => ({ onClick: (e, buttonProps) => { _.invoke(predefinedProps, 'onClick', e, buttonProps) - this.handleCancel(e) + handleCancel(e) }, }) - handleConfirmOverrides = (predefinedProps) => ({ + const handleConfirmOverrides = (predefinedProps) => ({ onClick: (e, buttonProps) => { _.invoke(predefinedProps, 'onClick', e, buttonProps) - _.invoke(this.props, 'onConfirm', e, this.props) + _.invoke(props, 'onConfirm', e, props) }, }) - render() { - const { cancelButton, confirmButton, content, header, open, size } = this.props - const rest = getUnhandledProps(Confirm, this.props) - - // `open` is auto controlled by the Modal - // It cannot be present (even undefined) with `defaultOpen` - // only apply it if the user provided an open prop - const openProp = {} - if (_.has(this.props, 'open')) openProp.open = open - - return ( - - {Modal.Header.create(header, { autoGenerateKey: false })} - {Modal.Content.create(content, { autoGenerateKey: false })} - - {Button.create(cancelButton, { - autoGenerateKey: false, - overrideProps: this.handleCancelOverrides, - })} - {Button.create(confirmButton, { - autoGenerateKey: false, - defaultProps: { primary: true }, - overrideProps: this.handleConfirmOverrides, - })} - - - ) + // `open` is auto controlled by the Modal + // It cannot be present (even undefined) with `defaultOpen` + // only apply it if the user provided an open prop + const openProp = {} + if (_.has(props, 'open')) { + openProp.open = open } -} + return ( + + {Modal.Header.create(header, { autoGenerateKey: false })} + {Modal.Content.create(content, { autoGenerateKey: false })} + + {Button.create(cancelButton, { + autoGenerateKey: false, + overrideProps: handleCancelOverrides, + })} + {Button.create(confirmButton, { + autoGenerateKey: false, + defaultProps: { primary: true }, + overrideProps: handleConfirmOverrides, + })} + + + ) +}) + +Confirm.displayName = 'Confirm' Confirm.propTypes = { /** The cancel button text. */ cancelButton: customPropTypes.itemShorthand, diff --git a/src/addons/Radio/Radio.js b/src/addons/Radio/Radio.js index 4edc9add52..0159880db2 100644 --- a/src/addons/Radio/Radio.js +++ b/src/addons/Radio/Radio.js @@ -9,17 +9,19 @@ import Checkbox from '../../modules/Checkbox' * @see Checkbox * @see Form */ -function Radio(props) { +const Radio = React.forwardRef(function (props, ref) { const { slider, toggle, type } = props + const rest = getUnhandledProps(Radio, props) // const ElementType = getElementType(Radio, props) // radio, slider, toggle are exclusive // use an undefined radio if slider or toggle are present const radio = !(slider || toggle) || undefined - return -} + return +}) +Radio.displayName = 'Radio' Radio.propTypes = { /** Format to emphasize the current selection state. */ slider: Checkbox.propTypes.slider, diff --git a/src/addons/TextArea/TextArea.js b/src/addons/TextArea/TextArea.js index 91e4bdbb88..d6b0537f02 100644 --- a/src/addons/TextArea/TextArea.js +++ b/src/addons/TextArea/TextArea.js @@ -1,50 +1,45 @@ -import { Ref } from '@fluentui/react-component-ref' import _ from 'lodash' import PropTypes from 'prop-types' -import React, { Component, createRef } from 'react' +import React from 'react' -import { getElementType, getUnhandledProps } from '../../lib' +import { getElementType, getUnhandledProps, useMergedRefs } from '../../lib' /** * A TextArea can be used to allow for extended user input. * @see Form */ -class TextArea extends Component { - ref = createRef() +const TextArea = React.forwardRef(function (props, ref) { + const { rows, value } = props + const elementRef = useMergedRefs(ref, React.useRef()) - focus = () => this.ref.current.focus() + const handleChange = (e) => { + const newValue = _.get(e, 'target.value') - handleChange = (e) => { - const value = _.get(e, 'target.value') - - _.invoke(this.props, 'onChange', e, { ...this.props, value }) + _.invoke(props, 'onChange', e, { ...props, value: newValue }) } - handleInput = (e) => { - const value = _.get(e, 'target.value') + const handleInput = (e) => { + const newValue = _.get(e, 'target.value') - _.invoke(this.props, 'onInput', e, { ...this.props, value }) + _.invoke(props, 'onInput', e, { ...props, value: newValue }) } - render() { - const { rows, value } = this.props - const rest = getUnhandledProps(TextArea, this.props) - const ElementType = getElementType(TextArea, this.props) - - return ( - - - - ) - } -} - + const rest = getUnhandledProps(TextArea, props) + const ElementType = getElementType(TextArea, props) + + return ( + + ) +}) + +TextArea.displayName = 'TextArea' TextArea.propTypes = { /** An element type to render as (string or function). */ as: PropTypes.elementType, diff --git a/src/modules/Checkbox/Checkbox.js b/src/modules/Checkbox/Checkbox.js index bd4e02f10f..00eeeb80a6 100644 --- a/src/modules/Checkbox/Checkbox.js +++ b/src/modules/Checkbox/Checkbox.js @@ -1,11 +1,9 @@ -import { Ref } from '@fluentui/react-component-ref' import cx from 'clsx' import _ from 'lodash' import PropTypes from 'prop-types' -import React, { createRef } from 'react' +import React from 'react' import { - ModernAutoControlledComponent as Component, createHTMLLabel, customPropTypes, getElementType, @@ -14,6 +12,9 @@ import { makeDebugger, partitionHTMLProps, useKeyOnly, + useAutoControlledValue, + useMergedRefs, + useIsomorphicLayoutEffect, } from '../../lib' const debug = makeDebugger('checkbox') @@ -23,39 +24,92 @@ const debug = makeDebugger('checkbox') * @see Form * @see Radio */ -export default class Checkbox extends Component { - inputRef = createRef() - labelRef = createRef() +const Checkbox = React.forwardRef(function (props, ref) { + const { + className, + disabled, + label, + id, + name, + radio, + readOnly, + slider, + tabIndex, + toggle, + type, + value, + } = props + + const [checked, setChecked] = useAutoControlledValue({ + state: props.checked, + defaultState: props.defaultChecked, + initialState: false, + }) + const [indeterminate, setIndeterminate] = useAutoControlledValue({ + state: props.indeterminate, + defaultState: props.defaultIndeterminate, + initialState: false, + }) + + const inputRef = useMergedRefs(React.useRef(), ref) + const labelRef = React.useRef() + + const isClickFromMouse = React.useRef() + + // ---------------------------------------- + // Effects + // ---------------------------------------- + + useIsomorphicLayoutEffect(() => { + // Note: You can't directly set the indeterminate prop on the input, so we + // need to maintain a ref to the input and set it manually whenever the + // component updates. + if (inputRef.current) { + inputRef.current.indeterminate = !!indeterminate + } + }) - componentDidMount() { - this.setIndeterminate() - } + // ---------------------------------------- + // Helpers + // ---------------------------------------- - componentDidUpdate() { - this.setIndeterminate() + const canToggle = () => { + return !disabled && !readOnly && !(radio && checked) } - canToggle = () => { - const { disabled, radio, readOnly } = this.props - const { checked } = this.state + const computeTabIndex = () => { + if (!_.isNil(tabIndex)) { + return tabIndex + } - return !disabled && !readOnly && !(radio && checked) + return disabled ? -1 : 0 } - computeTabIndex = () => { - const { disabled, tabIndex } = this.props + // ---------------------------------------- + // Handlers + // ---------------------------------------- - if (!_.isNil(tabIndex)) return tabIndex - return disabled ? -1 : 0 + const handleChange = (e) => { + if (!canToggle()) { + return + } + + debug('handleChange()', _.get(e, 'target.tagName')) + + _.invoke(props, 'onChange', e, { + ...props, + checked: !checked, + indeterminate: false, + }) + setChecked(!checked) + setIndeterminate(false) } - handleClick = (e) => { + const handleClick = (e) => { debug('handleClick()', _.get(e, 'target.tagName')) - const { id } = this.props - const { checked, indeterminate } = this.state - const isInputClick = _.invoke(this.inputRef.current, 'contains', e.target) - const isLabelClick = _.invoke(this.labelRef.current, 'contains', e.target) + const isInputClick = _.invoke(inputRef.current, 'contains', e.target) + const isLabelClick = _.invoke(labelRef.current, 'contains', e.target) const isRootClick = !isLabelClick && !isInputClick const hasId = !_.isNil(id) @@ -63,23 +117,23 @@ export default class Checkbox extends Component { // /~https://github.com/Semantic-Org/Semantic-UI-React/pull/3351 if (!isLabelClickAndForwardedToInput) { - _.invoke(this.props, 'onClick', e, { - ...this.props, + _.invoke(props, 'onClick', e, { + ...props, checked: !checked, indeterminate: !!indeterminate, }) } - if (this.isClickFromMouse) { - this.isClickFromMouse = false + if (isClickFromMouse.current) { + isClickFromMouse.current = false if (isLabelClick && !hasId) { - this.handleChange(e) + handleChange(e) } // Changes should be triggered for the slider variation if (isRootClick) { - this.handleChange(e) + handleChange(e) } if (isLabelClick && hasId) { @@ -90,32 +144,17 @@ export default class Checkbox extends Component { } } - handleChange = (e) => { - const { checked } = this.state - - if (!this.canToggle()) return - debug('handleChange()', _.get(e, 'target.tagName')) - - _.invoke(this.props, 'onChange', e, { - ...this.props, - checked: !checked, - indeterminate: false, - }) - this.setState({ checked: !checked, indeterminate: false }) - } - - handleMouseDown = (e) => { + const handleMouseDown = (e) => { debug('handleMouseDown()') - const { checked, indeterminate } = this.state - _.invoke(this.props, 'onMouseDown', e, { - ...this.props, + _.invoke(props, 'onMouseDown', e, { + ...props, checked: !!checked, indeterminate: !!indeterminate, }) if (!e.defaultPrevented) { - _.invoke(this.inputRef.current, 'focus') + _.invoke(inputRef.current, 'focus') } // Heads up! @@ -123,98 +162,75 @@ export default class Checkbox extends Component { e.preventDefault() } - handleMouseUp = (e) => { + const handleMouseUp = (e) => { debug('handleMouseUp()') - const { checked, indeterminate } = this.state - this.isClickFromMouse = true - _.invoke(this.props, 'onMouseUp', e, { - ...this.props, + isClickFromMouse.current = true + _.invoke(props, 'onMouseUp', e, { + ...props, checked: !!checked, indeterminate: !!indeterminate, }) } - // Note: You can't directly set the indeterminate prop on the input, so we - // need to maintain a ref to the input and set it manually whenever the - // component updates. - setIndeterminate = () => { - const { indeterminate } = this.state - - _.set(this.inputRef, 'current.indeterminate', !!indeterminate) - } - - render() { - const { - className, - disabled, - label, - id, - name, - radio, - readOnly, - slider, - toggle, - type, - value, - } = this.props - const { checked, indeterminate } = this.state - - const classes = cx( - 'ui', - useKeyOnly(checked, 'checked'), - useKeyOnly(disabled, 'disabled'), - useKeyOnly(indeterminate, 'indeterminate'), - // auto apply fitted class to compact white space when there is no label - // https://semantic-ui.com/modules/checkbox.html#fitted - useKeyOnly(_.isNil(label), 'fitted'), - useKeyOnly(radio, 'radio'), - useKeyOnly(readOnly, 'read-only'), - useKeyOnly(slider, 'slider'), - useKeyOnly(toggle, 'toggle'), - 'checkbox', - className, - ) - const unhandled = getUnhandledProps(Checkbox, this.props) - const ElementType = getElementType(Checkbox, this.props) - const [htmlInputProps, rest] = partitionHTMLProps(unhandled, { htmlProps: htmlInputAttrs }) - - // Heads Up! - // Do not remove empty labels, they are required by SUI CSS - const labelElement = createHTMLLabel(label, { - defaultProps: { htmlFor: id }, - autoGenerateKey: false, - }) ||