Skip to content

Commit

Permalink
Fix button plain & spinner & mergeRef types
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusps committed Jun 4, 2020
1 parent b362dd5 commit d2f0c0c
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 129 deletions.
247 changes: 141 additions & 106 deletions react/components/ButtonPlain/index.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,61 @@
import React, { FunctionComponent, Fragment, useEffect, RefObject } from 'react'
import PropTypes, { InferProps } from 'prop-types'
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, {
Fragment,
useEffect,
MutableRefObject,
ReactNode,
FC,
useRef,
} from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'

import Spinner from '../Spinner'
import { withForwardedRef, refShape } from '../../modules/withForwardedRef'
import { withForwardedRef } from '../../modules/withForwardedRef'
import mergeRef from '../../utilities/mergeRef'

const SIZE = {
SMALL: 'small',
REGULAR: 'regular',
LARGE: 'large',
}

const VARIATION = {
PLAIN: 'plain',
INVERTED: 'inverted',
DANGER: 'danger',
}

const loaderSize = {
[SIZE.SMALL]: 15,
[SIZE.REGULAR]: 20,
[SIZE.LARGE]: 25,
}
type MouseEventHandler = (e: React.MouseEvent<HTMLElement>) => void
type FocusEventHandler = (e: React.FocusEvent<HTMLElement>) => void

const propTypes = {
/** Button size */
size: PropTypes.oneOf(Object.values(SIZE)),
/** Button prominence variation */
variation: PropTypes.oneOf(Object.values(VARIATION)),
/** [DEPRECATED] If you are using just an Icon component inside, use this as true */
icon: PropTypes.bool,
/** Loading state */
isLoading: PropTypes.bool,
/** @ignore For internal use
* Sets reduced paddings in order to keep the button squareish if it
* only has an icon */
iconOnly: PropTypes.bool,
/** (Button spec attribute) */
id: PropTypes.string,
/** Data attribute */
testId: PropTypes.string,
/** (Button spec attribute) */
autoFocus: PropTypes.bool,
/** (Button spec attribute) */
autoComplete: PropTypes.string,
/** (Button spec attribute) */
disabled: PropTypes.bool,
/** @ignore Forwarded Ref */
forwardedRef: refShape,
/** (Button spec attribute) */
name: PropTypes.string,
/** (Button spec attribute) */
type: PropTypes.string,
/** (Button spec attribute) */
value: PropTypes.string,
/** Label of the Button */
children: PropTypes.node.isRequired,
/** onClick event. */
onClick: PropTypes.func,
interface Props {
children: ReactNode
size?: 'small' | 'regular' | 'large'
variation?: 'plain' | 'inverted' | 'danger'
icon?: boolean
isLoading?: boolean
iconOnly?: boolean
id?: string
testId?: string
autoFocus?: boolean
autoComplete?: string
disabled?: boolean
forwardedRef?: MutableRefObject<HTMLElement & HTMLButtonElement>
name?: string
value?: string
type?: 'button' | 'submit' | 'reset'
/* events */
onClick?: MouseEventHandler
onMouseEnter?: MouseEventHandler
onMouseLeave?: MouseEventHandler
onMouseOver?: MouseEventHandler
onMouseOut?: MouseEventHandler
onMouseUp?: MouseEventHandler
onMouseDown?: MouseEventHandler
onFocus?: FocusEventHandler
onBlur?: FocusEventHandler
/** URL for link mode. Converts the button internally to a link. */
href: PropTypes.string,
/** onMouseEnter event */
onMouseEnter: PropTypes.func,
/** onMouseLeave event */
onMouseLeave: PropTypes.func,
/** onMouseOver event */
onMouseOver: PropTypes.func,
/** onMouseOut event */
onMouseOut: PropTypes.func,
/** onMouseUp event */
onMouseUp: PropTypes.func,
/** onMouseDown event */
onMouseDown: PropTypes.func,
/** onFocus event */
onFocus: PropTypes.func,
/** onBlur event */
onBlur: PropTypes.func,
/** Link spec */
target: PropTypes.string,
/** Link spec */
rel: PropTypes.string,
/** Link spec */
referrerPolicy: PropTypes.string,
/** Link spec */
download: PropTypes.string,
/** Disables label wrapping */
noWrap: PropTypes.bool,
href?: string
target?: string
rel?: string
referrerPolicy?: string
download?: string
noWrap?: boolean
}

type Props = InferProps<typeof propTypes>
const ButtonPlain: FC<Props> = props => {
const innerRef = useRef<any>()

const ButtonPlain: FunctionComponent<Props> = props => {
const handleClick = (event: React.MouseEvent) => {
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
!props.disabled && !props.isLoading && props.onClick && props.onClick(event)
}

Expand All @@ -103,11 +67,21 @@ const ButtonPlain: FunctionComponent<Props> = props => {
}
}, [props.icon])

useEffect(
function handleAutofocus() {
if (props.autoFocus && innerRef.current && !props.iconOnly) {
innerRef?.current.setFocus()
}
},
[props.iconOnly, props.autoFocus]
)

const {
size,
variation,
size = 'regular',
variation = 'plain',
type = 'button',
children,
icon,
icon = false,
isLoading,
href,
target,
Expand All @@ -126,19 +100,19 @@ const ButtonPlain: FunctionComponent<Props> = props => {
'vtex-button bw1 ba fw5 v-mid relative pa0 br2 bn',
horizontalCompensation,
{
't-action--small': size === SIZE.SMALL,
't-action': size === SIZE.REGULAR,
't-action--large': size === SIZE.LARGE,
't-action--small': size === 'small',
't-action': size === 'regular',
't-action--large': size === 'large',
},
{ 'icon-button dib': iconOnly },
{
'bg-transparent b--transparent c-disabled': disabled,
'bg-transparent b--transparent c-action-primary hover-b--transparent hover-bg-action-secondary hover-b--action-secondary':
!disabled && variation === VARIATION.PLAIN,
!disabled && variation === 'plain',
'bg-transparent b--transparent c-on-base--inverted':
!disabled && variation === VARIATION.INVERTED,
!disabled && variation === 'inverted',
'bg-transparent b--transparent c-danger hover-bg-danger--faded hover-b--danger--faded':
!disabled && variation === VARIATION.DANGER,
!disabled && variation === 'danger',
},
{ pointer: !disabled },
{ 'inline-flex items-center no-underline': href }
Expand Down Expand Up @@ -169,7 +143,6 @@ const ButtonPlain: FunctionComponent<Props> = props => {
<Element
id={props.id}
data-testid={props.testId}
autoFocus={iconOnly ? undefined : props.autoFocus}
disabled={iconOnly ? undefined : props.disabled}
name={iconOnly ? undefined : props.name}
value={iconOnly ? undefined : props.value}
Expand All @@ -185,21 +158,22 @@ const ButtonPlain: FunctionComponent<Props> = props => {
onMouseDown={props.onMouseDown}
onFocus={props.onFocus}
onBlur={props.onBlur}
ref={
props.forwardedRef as RefObject<HTMLAnchorElement & HTMLButtonElement>
}
ref={mergeRef(innerRef, props.forwardedRef)}
style={style}
// Button-mode exclusive props
type={href ? undefined : (props.type as 'button' | 'submit' | 'reset')}
// Link-mode exclusive props
type={href ? undefined : type}
{...(href && linkModeProps)}
>
{isLoading ? (
<Fragment>
<span className="top-0 left-0 w-100 h-100 absolute flex justify-center items-center">
<Spinner
secondary={variation === VARIATION.DANGER}
size={loaderSize[size]}
size={
{
small: 15,
regular: 20,
large: 25,
}[size]
}
/>
</span>
<span className={`${labelClasses} o-0`}>{children}</span>
Expand All @@ -212,15 +186,76 @@ const ButtonPlain: FunctionComponent<Props> = props => {
}

ButtonPlain.defaultProps = {
size: SIZE.REGULAR,
variation: VARIATION.PLAIN,
disabled: false,
autoFocus: false,
icon: false,
type: 'button',
isLoading: false,
}

ButtonPlain.propTypes = propTypes
ButtonPlain.propTypes = {
/** Button size */
size: PropTypes.oneOf(['small', 'regular', 'large']),
/** Button prominence variation */
variation: PropTypes.oneOf(['plain', 'inverted', 'danger']),
/** [DEPRECATED] If you are using just an Icon component inside, use this as true */
icon: PropTypes.bool,
/** Loading state */
isLoading: PropTypes.bool,
/** @ignore For internal use
* Sets reduced paddings in order to keep the button squareish if it
* only has an icon */
iconOnly: PropTypes.bool,
/** (Button spec attribute) */
id: PropTypes.string,
/** Data attribute */
testId: PropTypes.string,
/** (Button spec attribute) */
autoFocus: PropTypes.bool,
/** (Button spec attribute) */
autoComplete: PropTypes.string,
/** (Button spec attribute) */
disabled: PropTypes.bool,
/** @ignore Forwarded Ref */
forwardedRef: PropTypes.any,
/** (Button spec attribute) */
name: PropTypes.string,
/** (Button spec attribute) */
type: PropTypes.oneOf(['button', 'submit', 'reset']),
/** (Button spec attribute) */
value: PropTypes.string,
/** Label of the Button */
children: PropTypes.node.isRequired,
/** onClick event. */
onClick: PropTypes.func,
/** URL for link mode. Converts the button internally to a link. */
href: PropTypes.string,
/** onMouseEnter event */
onMouseEnter: PropTypes.func,
/** onMouseLeave event */
onMouseLeave: PropTypes.func,
/** onMouseOver event */
onMouseOver: PropTypes.func,
/** onMouseOut event */
onMouseOut: PropTypes.func,
/** onMouseUp event */
onMouseUp: PropTypes.func,
/** onMouseDown event */
onMouseDown: PropTypes.func,
/** onFocus event */
onFocus: PropTypes.func,
/** onBlur event */
onBlur: PropTypes.func,
/** Link spec */
target: PropTypes.string,
/** Link spec */
rel: PropTypes.string,
/** Link spec */
referrerPolicy: PropTypes.string,
/** Link spec */
download: PropTypes.string,
/** Disables label wrapping */
noWrap: PropTypes.bool,
}

export default withForwardedRef(ButtonPlain)
28 changes: 12 additions & 16 deletions react/components/Spinner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@ import { baseClassname } from '../icon/utils'
const radius = 40
const circ = 2 * radius * Math.PI

const propTypes = {
/** Color of the spinner */
color: PropTypes.string,
/** Size (diameter) of the spinner */
size: PropTypes.number,
/** Sets the display to block */
block: PropTypes.bool,
interface Props {
color?: string
size?: number
block?: boolean
}

type Props = PropTypes.InferProps<typeof propTypes>

const Spinner: FC<Props> = ({ color, size, block }) => (
const Spinner: FC<Props> = ({ color, size = 40, block = false }) => (
<svg
className={classNames(baseClassname('spinner'), {
'c-action-primary': !color,
Expand Down Expand Up @@ -60,7 +55,6 @@ const Spinner: FC<Props> = ({ color, size, block }) => (
}
`}
</style>

<circle
className="vtex-spinner_circle"
cx="50"
Expand All @@ -76,11 +70,13 @@ const Spinner: FC<Props> = ({ color, size, block }) => (
</svg>
)

Spinner.propTypes = propTypes

Spinner.defaultProps = {
block: false,
size: 40,
Spinner.propTypes = {
/** Color of the spinner */
color: PropTypes.string,
/** Size (diameter) of the spinner */
size: PropTypes.number,
/** Sets the display to block */
block: PropTypes.bool,
}

export default Spinner
12 changes: 5 additions & 7 deletions react/utilities/mergeRef/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { MutableRefObject } from 'react'
import memoize from 'memoize-one'

export function setRef(
ref: MutableRefObject<unknown> | Function,
value: unknown = null
ref: MutableRefObject<any> | Function | undefined,
value: any = null
) {
if (!ref) return

Expand All @@ -17,14 +18,11 @@ export function setRef(
/**
* Merges/Aligns the values of two refs
*/
function mergeRef(
refA: MutableRefObject<unknown>,
refB: MutableRefObject<unknown>
) {
function mergeRef(refA?: MutableRefObject<any>, refB?: MutableRefObject<any>) {
if (refA == null && refB == null) {
return null
}
return (value: unknown) => {
return (value: any) => {
setRef(refA, value)
setRef(refB, value)
}
Expand Down

0 comments on commit d2f0c0c

Please sign in to comment.