diff --git a/spec/javascripts/packages/document-capture/components/button-spec.jsx b/app/javascript/packages/components/button.spec.tsx similarity index 92% rename from spec/javascripts/packages/document-capture/components/button-spec.jsx rename to app/javascript/packages/components/button.spec.tsx index 9a17e33e329..8a0ba785a56 100644 --- a/spec/javascripts/packages/document-capture/components/button-spec.jsx +++ b/app/javascript/packages/components/button.spec.tsx @@ -1,13 +1,13 @@ +import { render } from '@testing-library/react'; import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; -import Button from '@18f/identity-document-capture/components/button'; -import { render } from '../../../support/document-capture'; +import Button from './button'; describe('document-capture/components/button', () => { it('renders with default props', () => { const { getByText } = render(); - const button = getByText('Click me'); + const button = getByText('Click me') as HTMLButtonElement; userEvent.click(button); expect(button.nodeName).to.equal('BUTTON'); @@ -88,7 +88,7 @@ describe('document-capture/components/button', () => { , ); - const button = getByText('Click me'); + const button = getByText('Click me') as HTMLButtonElement; userEvent.click(button); expect(onClick.calledOnce).to.be.false(); @@ -103,7 +103,7 @@ describe('document-capture/components/button', () => { , ); - const button = getByText('Click me'); + const button = getByText('Click me') as HTMLButtonElement; expect(button.classList.contains('usa-button--disabled')); expect(button.disabled).to.be.false(); @@ -115,7 +115,7 @@ describe('document-capture/components/button', () => { it('renders with custom type', () => { const { getByText } = render(); - const button = getByText('Click me'); + const button = getByText('Click me') as HTMLButtonElement; expect(button.type).to.equal('submit'); }); diff --git a/app/javascript/packages/components/button.tsx b/app/javascript/packages/components/button.tsx new file mode 100644 index 00000000000..3c6326a3e46 --- /dev/null +++ b/app/javascript/packages/components/button.tsx @@ -0,0 +1,97 @@ +import type { MouseEvent, ReactNode } from 'react'; + +type ButtonType = 'button' | 'reset' | 'submit'; + +export interface ButtonProps { + /** + * Button type, defaulting to "button". + */ + type?: ButtonType; + + /** + * Click handler. + */ + onClick?: (event: MouseEvent) => void; + + /** + * Element children. + */ + children?: ReactNode; + + /** + * Whether button should be styled as big button. + */ + isBig?: boolean; + + /** + * Whether button should be styled as flexible width, such that it shrinks to its minimum width instead of occupying full-width on mobile viewports. + */ + isFlexibleWidth?: boolean; + + /** + * Whether button should be styled as primary button. + */ + isWide?: boolean; + + /** + * Whether button should be styled as secondary button. + */ + isOutline?: boolean; + + /** + * Whether button is disabled. + */ + isDisabled?: boolean; + + /** + * Whether button should be unstyled, visually as a link. + */ + isUnstyled?: boolean; + + /** + * Whether button should appear disabled (but remain clickable). + */ + isVisuallyDisabled?: boolean; + + /** + * Optional additional class names. + */ + className?: string; +} + +function Button({ + type = 'button', + onClick, + children, + isBig, + isFlexibleWidth, + isWide, + isOutline, + isDisabled, + isUnstyled, + isVisuallyDisabled, + className, +}: ButtonProps) { + const classes = [ + 'usa-button', + isBig && 'usa-button--big', + isFlexibleWidth && 'usa-button--flexible-width', + isWide && 'usa-button--wide', + isOutline && 'usa-button--outline', + isUnstyled && 'usa-button--unstyled', + isVisuallyDisabled && 'usa-button--disabled', + className, + ] + .filter(Boolean) + .join(' '); + + return ( + // Disable reason: We can assume `type` is provided as valid, or the default `button`. + // eslint-disable-next-line react/button-has-type + + ); +} + +export default Button; diff --git a/app/javascript/packages/components/index.js b/app/javascript/packages/components/index.js index 3adf7cf4fab..b38692beead 100644 --- a/app/javascript/packages/components/index.js +++ b/app/javascript/packages/components/index.js @@ -1,4 +1,5 @@ export { default as Alert } from './alert'; +export { default as Button } from './button'; export { default as BlockLink } from './block-link'; export { default as Icon } from './icon'; export { default as SpinnerDots } from './spinner-dots'; diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index 0e68896c584..1760593197b 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -9,6 +9,7 @@ import { } from 'react'; import { useI18n } from '@18f/identity-react-i18n'; import { useIfStillMounted, useDidUpdateEffect } from '@18f/identity-react-hooks'; +import { Button } from '@18f/identity-components'; import AnalyticsContext from '../context/analytics'; import AcuantContext from '../context/acuant'; import FailedCaptureAttemptsContext from '../context/failed-capture-attempts'; @@ -16,7 +17,6 @@ import AcuantCamera from './acuant-camera'; import AcuantCaptureCanvas from './acuant-capture-canvas'; import FileInput from './file-input'; import FullScreen from './full-screen'; -import Button from './button'; import DeviceContext from '../context/device'; import UploadContext from '../context/upload'; import useCounter from '../hooks/use-counter'; diff --git a/app/javascript/packages/document-capture/components/button-to.jsx b/app/javascript/packages/document-capture/components/button-to.jsx index 20aaf3af332..ece99c6a296 100644 --- a/app/javascript/packages/document-capture/components/button-to.jsx +++ b/app/javascript/packages/document-capture/components/button-to.jsx @@ -1,9 +1,9 @@ import { useContext, useRef } from 'react'; import { createPortal } from 'react-dom'; +import { Button } from '@18f/identity-components'; import UploadContext from '../context/upload'; -import Button from './button'; -/** @typedef {import('./button').ButtonProps} ButtonProps */ +/** @typedef {import('@18f/identity-components/button').ButtonProps} ButtonProps */ /** * @typedef NativeButtonToProps diff --git a/app/javascript/packages/document-capture/components/button.jsx b/app/javascript/packages/document-capture/components/button.jsx deleted file mode 100644 index 26f11c6359a..00000000000 --- a/app/javascript/packages/document-capture/components/button.jsx +++ /dev/null @@ -1,60 +0,0 @@ -/** @typedef {import('react').MouseEvent} ReactMouseEvent */ -/** @typedef {import('react').ReactNode} ReactNode */ -/** @typedef {"button"|"reset"|"submit"} ButtonType */ - -/** - * @typedef ButtonProps - * - * @prop {ButtonType=} type Button type, defaulting to "button". - * @prop {(ReactMouseEvent)=>void=} onClick Click handler. - * @prop {ReactNode=} children Element children. - * @prop {boolean=} isBig Whether button should be styled as big button. - * @prop {boolean=} isFlexibleWidth Whether button should be styled as flexible width, such that it - * shrinks to its minimum width instead of occupying full-width on mobile viewports. - * @prop {boolean=} isWide Whether button should be styled as primary button. - * @prop {boolean=} isOutline Whether button should be styled as secondary button. - * @prop {boolean=} isDisabled Whether button is disabled. - * @prop {boolean=} isUnstyled Whether button should be unstyled, visually as a link. - * @prop {boolean=} isVisuallyDisabled Whether button should appear disabled (but remain clickable). - * @prop {string=} className Optional additional class names. - */ - -/** - * @param {ButtonProps} props Props object. - */ -function Button({ - type = 'button', - onClick, - children, - isBig, - isFlexibleWidth, - isWide, - isOutline, - isDisabled, - isUnstyled, - isVisuallyDisabled, - className, -}) { - const classes = [ - 'usa-button', - isBig && 'usa-button--big', - isFlexibleWidth && 'usa-button--flexible-width', - isWide && 'usa-button--wide', - isOutline && 'usa-button--outline', - isUnstyled && 'usa-button--unstyled', - isVisuallyDisabled && 'usa-button--disabled', - className, - ] - .filter(Boolean) - .join(' '); - - return ( - // Disable reason: We can assume `type` is provided as valid, or the default `button`. - // eslint-disable-next-line react/button-has-type - - ); -} - -export default Button; diff --git a/app/javascript/packages/document-capture/components/form-steps.tsx b/app/javascript/packages/document-capture/components/form-steps.tsx index 8459c24997d..7059915e13d 100644 --- a/app/javascript/packages/document-capture/components/form-steps.tsx +++ b/app/javascript/packages/document-capture/components/form-steps.tsx @@ -1,9 +1,8 @@ import { useEffect, useRef, useState, createContext, useContext } from 'react'; import type { RefCallback, FormEventHandler, FC } from 'react'; import { useI18n } from '@18f/identity-react-i18n'; -import { Alert } from '@18f/identity-components'; +import { Alert, Button } from '@18f/identity-components'; import { useDidUpdateEffect, useIfStillMounted } from '@18f/identity-react-hooks'; -import Button from './button'; import FormErrorMessage, { RequiredValueMissingError } from './form-error-message'; import PromptOnNavigate from './prompt-on-navigate'; import useHistoryParam from '../hooks/use-history-param';