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';