diff --git a/src/components/Button/Addon/Addon.tsx b/src/components/Button/Addon/Addon.tsx index b8281931..5d5b02c4 100644 --- a/src/components/Button/Addon/Addon.tsx +++ b/src/components/Button/Addon/Addon.tsx @@ -1,7 +1,7 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; import classNames from 'classnames'; +import ButtonContext from '../Button.context'; import styles from './Addon.module.css'; import { TButtonSize } from '../types'; @@ -12,23 +12,19 @@ export interface AddonProps { const Addon: React.FC = ({ className = '', - size = 'md', + size: customSize, ...passedProps -}) => ( -
-); +}) => { + const buttonContext = useContext(ButtonContext); -Addon.propTypes = { - className: PropTypes.string, - size: PropTypes.oneOf(['sm', 'md', 'lg']), -}; + const size = customSize || buttonContext.size || 'md'; -Addon.defaultProps = { - className: '', - size: 'md', + return ( +
+ ); }; Addon.displayName = 'ButtonAddon'; diff --git a/src/components/Button/Button.context.ts b/src/components/Button/Button.context.ts new file mode 100644 index 00000000..a22c5397 --- /dev/null +++ b/src/components/Button/Button.context.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react'; + +type ContextProps = { + size?: string; +}; + +export default createContext({}); diff --git a/src/components/Button/Button.test.tsx b/src/components/Button/Button.test.tsx index bb2c29f6..c94a762f 100644 --- a/src/components/Button/Button.test.tsx +++ b/src/components/Button/Button.test.tsx @@ -1,90 +1,115 @@ import React from 'react'; -import { mount, shallow } from 'enzyme'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import Button from '.'; describe('Button', () => { it('renders a `button` tag without error', () => { - const wrapper = mount(); + expect(screen.getByRole('button').getAttribute('type')).toEqual('submit'); }); describe('props.children', () => { it('renders strings', () => { - const wrapper = shallow(); - expect(wrapper.text()).toEqual('Boom'); + render(); + expect(screen.getByRole('button').textContent).toEqual('Boom'); }); it('renders React nodes', () => { - const wrapper = shallow( - ); - expect(wrapper.text()).toEqual('Boom'); + expect(screen.getByRole('button').textContent).toEqual('Boom'); + expect(screen.getByTestId('bar')).toBeTruthy(); }); }); describe('props.className', () => { it('adds custom className', () => { - const wrapper = shallow(); + expect(screen.getByRole('button').classList).toContain('foo'); }); }); - describe('props.hollow', () => { - // Our current test build doesn't do css modules, so this won't work - // it('adds hollow className', () => { - // const wrapper = shallow(); + expect(screen.getByRole('button').classList).toContain('primary'); + }); }); describe('props.size', () => { - it('does not pass size prop to `Addon` children', () => { - const wrapper = mount( - - ); - let addon = wrapper.find(Button.Addon); - expect(addon.props().size).toEqual('md'); - wrapper.setProps({ size: 'sm' }); - addon = wrapper.find(Button.Addon); - expect(addon.props().size).toEqual('md'); - wrapper.setProps({ size: 'lg' }); - addon = wrapper.find(Button.Addon); - expect(addon.props().size).toEqual('md'); + describe('is available to `Addon` children by default', () => { + it('sm', () => { + render( + + ); + + expect(screen.getByText('Addon').classList).toContain('sm'); + }); + + it('lg', () => { + render( + + ); + + expect(screen.getByText('Addon').classList).toContain('lg'); + }); + }); + + describe('can be overridden by `Addon` children', () => { + it('sm', () => { + render( + + ); + + expect(screen.getByText('Addon').classList).toContain('md'); + }); + + it('lg', () => { + render( + + ); + + expect(screen.getByText('Addon').classList).toContain('md'); + }); }); }); describe('props.href', () => { it('renders an anchor tag when there is an href', () => { - const wrapper = mount(); + expect(screen.getByRole('link')).toBeTruthy(); }); it('renders a button tag when there is not href', () => { const callback = jest.fn(); - const wrapper = mount(); + + const button = screen.getByRole('button'); + userEvent.click(button); + + expect(button).toBeTruthy(); + expect(callback).toBeCalledTimes(1); }); }); }); diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 6693b0c7..b1ba7109 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import classNames from 'classnames'; +import Context from './Button.context'; import Addon, { AddonProps } from './Addon/Addon'; import styles from './Button.module.css'; import { TButtonLevel, TButtonSize, TButtonState } from './types'; @@ -11,11 +12,17 @@ import { TButtonLevel, TButtonSize, TButtonState } from './types'; Select (-> These might be better served as a different component) */ -type TagType = { +type CustomTagType = { className?: string; [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any }; +type TagType = + | React.FC + | React.ComponentType + | string + | null; + interface BaseButtonProps { circle?: boolean; className?: string; @@ -23,7 +30,7 @@ interface BaseButtonProps { hollow?: boolean; level?: TButtonLevel; size?: TButtonSize; - tag?: React.FC | React.ComponentType | string | null; + tag?: TagType; // States hover?: boolean; @@ -90,34 +97,18 @@ const Button: ButtonType = ({ hollow = false, level = 'secondary', size = 'lg', - tag: Tag = null, + tag = null, ...passedProps }: ButtonElementProps | AnchorElementProps) => { - const children = initChildren; + const children = React.Children.map(initChildren, (child) => + typeof child === 'string' ? {child} : child + ); - // For future ref const truthyStateKeys = useMemo( () => states.filter((state) => !!passedProps[state]), [passedProps] ); - // Filter out undefined passedProps and `active` from DOM element tags - // const safePassedProps = useMemo( - // () => - // Object.entries(passedProps).reduce((result, [key, value]) => { - // if (value === undefined) { - // return result; - // } - - // if (typeof Tag === 'string' && key === 'active') { - // return result; - // } - - // return { ...result, [key]: value }; - // }, {}), - // [passedProps, Tag] - // ); - const className = useMemo( () => classNames( @@ -130,41 +121,29 @@ const Button: ButtonType = ({ [styles.circle]: circle, [styles['primary-alt']]: level === 'teal', // For backwards compatibility with the old level name }, - /* - In addition to "real" boolean props, this adds class names for - 'disabled', 'active', etc. so consumers can force appearance on - elements easily - */ truthyStateKeys.map((truthyStateKey) => styles[truthyStateKey]) ), [circle, darkMode, hollow, level, passedClassName, size, truthyStateKeys] ); - if (Tag) { - return ( - - {children} - - ); - } + let Tag: TagType = 'button'; - if (hasHref(passedProps)) { - return ( - - {children} - - ); + if (tag) { + Tag = tag; + } else if (hasHref(passedProps)) { + Tag = 'a'; } - // button render return ( - + + + {children} + + ); }; diff --git a/src/components/Button/__snapshots__/Button.test.tsx.snap b/src/components/Button/__snapshots__/Button.test.tsx.snap new file mode 100644 index 00000000..d744892c --- /dev/null +++ b/src/components/Button/__snapshots__/Button.test.tsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Button renders a \`button\` tag without error 1`] = ` + + + + + + +

Circle