UNPKG

triage-button

Version:

The my-button component provides users a way to trigger actions in the UI.

332 lines (293 loc) 9.16 kB
import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classNamesBind from 'classnames/bind'; import * as KeyCode from 'keycode-js'; import ThemeContext from 'terra-theme-context'; import styles from './Button.module.scss'; const cx = classNamesBind.bind(styles); const ButtonVariants = { NEUTRAL: 'neutral', EMPHASIS: 'emphasis', GHOST: 'ghost', // TODO: this should be removed on the next major version bump 'DE-EMPHSASIS': 'de-emphasis', 'DE-EMPHASIS': 'de-emphasis', ACTION: 'action', UTILITY: 'utility', }; const ButtonTypes = { BUTTON: 'button', SUBMIT: 'submit', RESET: 'reset', }; const propTypes = { /** * Sets the href. When set will render the component as an anchor tag. */ href: PropTypes.string, /** * An optional icon. Nested inline with the text when provided. */ icon: PropTypes.element, /** * Whether or not the button should only display as an icon. */ isIconOnly: PropTypes.bool, /** * Whether or not the button should display as a block. */ isBlock: PropTypes.bool, /** * Whether or not the button has reduced padding */ isCompact: PropTypes.bool, /** * Whether or not the button should be disabled. */ isDisabled: PropTypes.bool, /** * Reverses the position of the icon and text. */ isReversed: PropTypes.bool, /** * Callback function triggered when mouse is pressed. */ onMouseDown: PropTypes.func, /** * Callback function triggered when clicked. */ onClick: PropTypes.func, /** * Callback function triggered when button loses focus. */ onBlur: PropTypes.func, /** * Callback function triggered when button gains focus. */ onFocus: PropTypes.func, /** * Callback function triggered when key is pressed. */ onKeyDown: PropTypes.func, /** * Callback function triggered when key is released. */ onKeyUp: PropTypes.func, /** * Callback ref to pass into the dom element. */ refCallback: PropTypes.func, /** * Sets the button text. * If the button is `isIconOnly` or variant `utility` this text is set as the aria-label and title for accessibility. */ text: PropTypes.string.isRequired, /** * Additional information to display as a native tooltip on hover. * Buttons declared as `isIconOnly` or `utility` will fallback to using `text` if not provided. */ title: PropTypes.string, /** * Sets the button type. One of `button`, `submit`, or `reset`. */ type: PropTypes.oneOf([ButtonTypes.BUTTON, ButtonTypes.SUBMIT, ButtonTypes.RESET]), /** * Sets the button variant. One of `neutral`, `emphasis`, `ghost`, `de-emphasis`, `action` or `utility`. */ variant: PropTypes.oneOf([ButtonVariants.NEUTRAL, ButtonVariants.EMPHASIS, ButtonVariants.GHOST, ButtonVariants['DE-EMPHASIS'], ButtonVariants.ACTION, ButtonVariants.UTILITY]), }; const defaultProps = { isBlock: false, isCompact: false, isDisabled: false, isIconOnly: false, isReversed: false, refCallback: undefined, title: undefined, type: ButtonTypes.BUTTON, variant: ButtonVariants.NEUTRAL, }; class Button extends React.Component { constructor(props) { super(props); this.state = { active: false, focused: false }; this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.handleOnBlur = this.handleOnBlur.bind(this); this.handleFocus = this.handleFocus.bind(this); this.handleMouseDown = this.handleMouseDown.bind(this); this.handleClick = this.handleClick.bind(this); this.shouldShowFocus = true; } handleOnBlur(event) { this.setState({ focused: false }); if (this.props.onBlur) { this.props.onBlur(event); } } handleClick(event) { // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Button#Clicking_and_focus // Button on Firefox, Safari and IE running on OS X does not receive focus when clicked. // This will put focus on the button when clicked if it is not currently the active element. if (document.activeElement !== event.currentTarget) { this.shouldShowFocus = false; event.currentTarget.focus(); this.shouldShowFocus = true; } if (this.props.onClick) { this.props.onClick(event); } } handleKeyDown(event) { // Add active state to FF browsers if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE) { this.setState({ active: true }); // Follow href on space keydown when rendered as an anchor tag if (this.props.href) { // Prevent window scrolling event.preventDefault(); window.location.href = this.props.href; } } // Add focus styles for keyboard navigation if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE || event.nativeEvent.keyCode === KeyCode.KEY_RETURN) { this.setState({ focused: true }); } if (this.props.onKeyDown) { this.props.onKeyDown(event); } } handleKeyUp(event) { // Remove active state from FF broswers if (event.nativeEvent.keyCode === KeyCode.KEY_SPACE) { this.setState({ active: false }); } // Apply focus styles for keyboard navigation if (event.nativeEvent.keyCode === KeyCode.KEY_TAB) { this.setState({ focused: true }); } if (this.props.onKeyUp) { this.props.onKeyUp(event); } } handleFocus(event) { if (this.shouldShowFocus) { this.setState({ focused: true }); } if (this.props.onFocus) { this.props.onFocus(event); } } handleMouseDown(event) { if (this.props.onMouseDown) { this.props.onMouseDown(event); } // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus#Notes // If you call HTMLElement.focus() from a mousedown event handler, you must call event.preventDefault() to keep the focus from leaving the HTMLElement. // Otherwise, when you click on the button again, focus would leave the button and onBlur would get called causing the document.activeElement would no longer be the focused button. event.preventDefault(); } render() { const { icon, isBlock, isCompact, isDisabled, isIconOnly, isReversed, text, type, variant, href, onClick, onMouseDown, onBlur, onFocus, onKeyDown, onKeyUp, refCallback, title, ...customProps } = this.props; const theme = this.context; const buttonClasses = classNames( cx([ 'button', variant, { 'is-disabled': isDisabled }, { block: isBlock }, { compact: isCompact }, { 'is-active': this.state.active && !isDisabled }, { 'is-focused': this.state.focused && !isDisabled }, theme.className, ]), customProps.className, ); const buttonLabelClasses = cx([ 'button-label', { 'text-and-icon': icon && !isIconOnly && variant !== 'utility' }, { 'icon-only': isIconOnly || variant === 'utility' }, { 'text-only': !icon }, ]); const buttonTextClasses = cx([ { 'text-first': icon && isReversed }, ]); const iconClasses = cx([ 'icon', { 'icon-first': (!isIconOnly && variant !== 'utility') && !isReversed }, ]); const buttonText = !isIconOnly && variant !== 'utility' ? <span className={buttonTextClasses}>{text}</span> : null; let buttonIcon = null; if (icon) { const iconSvgClasses = icon.props.className ? `${icon.props.className} ${cx('icon-svg')}` : cx('icon-svg'); const cloneIcon = React.cloneElement(icon, { className: iconSvgClasses }); buttonIcon = <span className={iconClasses}>{cloneIcon}</span>; } let buttonTitle = title; if (isIconOnly || variant === 'utility') { buttonTitle = title || text; } const buttonLabel = ( <span className={buttonLabelClasses}> {isReversed ? buttonText : buttonIcon} {isReversed ? buttonIcon : buttonText} </span> ); let ariaLabel = customProps['aria-label']; if (isIconOnly || variant === 'utility') { ariaLabel = ariaLabel || text; } const ComponentType = href ? 'a' : 'button'; return ( <ComponentType {...customProps} className={buttonClasses} type={type} disabled={isDisabled} tabIndex={isDisabled ? '-1' : undefined} aria-disabled={isDisabled} aria-label={ariaLabel} onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} onBlur={this.handleOnBlur} title={buttonTitle} onClick={this.handleClick} onMouseDown={this.handleMouseDown} onFocus={this.handleFocus} href={href} ref={refCallback} > {buttonLabel} </ComponentType> ); } } Button.propTypes = propTypes; Button.defaultProps = defaultProps; Button.contextType = ThemeContext; Button.Opts = {}; Button.Opts.Types = ButtonTypes; Button.Opts.Variants = ButtonVariants; export default Button; export { ButtonTypes, ButtonVariants };