Skip to content

Commit

Permalink
Add backButtonProps and cancelButtonProps to FormFooter component and…
Browse files Browse the repository at this point in the history
… maintain backwards compatibility
  • Loading branch information
stephl3 committed Feb 27, 2024
1 parent e57c35c commit 0c4e54a
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 25 deletions.
130 changes: 113 additions & 17 deletions packages/form-footer/src/FormFooter.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import { render } from '@testing-library/react';
import { axe } from 'jest-axe';

import Button from '@leafygreen-ui/button';
import X from '@leafygreen-ui/icon/dist/X';

import { FormFooterProps } from './FormFooter';
import FormFooter from '.';

const buttonTestId = {
back: 'lg-form_footer-back-button',
cancel: 'lg-form_footer-cancel-button',
primary: 'lg-form_footer-primary-button',
};

const renderFooter = (props: FormFooterProps) => {
return render(<FormFooter {...props} />);
};
Expand Down Expand Up @@ -39,30 +46,119 @@ describe('packages/form-footer', () => {
expect(ButtonElement).toBeInTheDocument();
});

test('Renders cancel button', () => {
const { getByText } = renderFooter({
primaryButton: { text: 'Test button' },
// remove once deprecated props are removed
describe('deprecated cancel button and back button props', () => {
test('Renders cancel button', () => {
const { getByText } = renderFooter({
primaryButton: { text: 'Test button' },
});
const Cancel = getByText('Cancel');
expect(Cancel).toBeInTheDocument();
});

test('Renders cancel button with custom text', () => {
const { getByText } = renderFooter({
primaryButton: { text: 'Test button' },
cancelButtonText: 'CancelText',
});
const Cancel = getByText('CancelText');
expect(Cancel).toBeInTheDocument();
});

test('Renders back button', () => {
const { getByText } = renderFooter({
primaryButton: { text: 'Test button' },
backButtonText: 'Back',
});
const Back = getByText('Back');
expect(Back).toBeInTheDocument();
});
const Cancel = getByText('Cancel');
expect(Cancel).toBeInTheDocument();
});

test('Renders cancel button with custom text', () => {
const { getByText } = renderFooter({
primaryButton: { text: 'Test button' },
cancelButtonText: 'CancelText',
describe('cancel button', () => {
test('renders with custom text if cancelButtonProps is defined', () => {
const cancelButtonText = 'Cancel';
const { queryByText } = renderFooter({
primaryButton: { text: 'Test button' },
cancelButtonProps: {
text: cancelButtonText,
},
});
const Cancel = queryByText(cancelButtonText);
expect(Cancel).toBeInTheDocument();
});

test('does not render if cancelButtonProps is not defined', () => {
const { queryByTestId } = renderFooter({
primaryButton: { text: 'Test button' },
cancelButtonText: '', // remove once deprecated props are removed
});
const Cancel = queryByTestId(buttonTestId.cancel);
expect(Cancel).not.toBeInTheDocument();
});
const Cancel = getByText('CancelText');
expect(Cancel).toBeInTheDocument();
});

test('Renders back button', () => {
const { getByText } = renderFooter({
primaryButton: { text: 'Test button' },
backButtonText: 'Back',
describe('back button', () => {
test('renders with custom text if backButtonProps is defined', () => {
const backButtonText = 'Back';
const { queryByText } = renderFooter({
primaryButton: { text: 'Test button' },
backButtonProps: {
text: backButtonText,
},
});
const Back = queryByText(backButtonText);
expect(Back).toBeInTheDocument();
});

test('does not render', () => {
const { queryByTestId } = renderFooter({
primaryButton: { text: 'Test button' },
});
const Back = queryByTestId(buttonTestId.back);
expect(Back).not.toBeInTheDocument();
});

describe('Back button left glyph', () => {
test('Renders ArrowLeftIcon if leftGlyph is undefined', () => {
const { queryByTestId } = renderFooter({
primaryButton: { text: 'Test button' },
backButtonProps: {
text: 'Back',
leftGlyph: undefined,
},
});
const BackButtonIcon = queryByTestId(
'lg-form_footer-back-button-icon',
);
expect(BackButtonIcon).toBeInTheDocument();
});

test('Does not render if leftGlyph is null', () => {
const { getByTestId } = renderFooter({
primaryButton: { text: 'Test button' },
backButtonProps: {
text: 'Back',
leftGlyph: null,
},
});
const Back = getByTestId(buttonTestId.back);
expect(Back.querySelector('svg')).toBeNull();
});

test('Renders custom leftGlyph icon if leftGlyph is ReactElement', () => {
const leftGlyphTestId = 'custom-icon-id';
const { queryByTestId } = renderFooter({
primaryButton: { text: 'Test button' },
backButtonProps: {
text: 'Back',
leftGlyph: <X data-testid={leftGlyphTestId} />,
},
});
const BackButtonIcon = queryByTestId(leftGlyphTestId);
expect(BackButtonIcon).toBeInTheDocument();
});
});
const Back = getByText('Back');
expect(Back).toBeInTheDocument();
});

test('Renders error message', () => {
Expand Down
86 changes: 78 additions & 8 deletions packages/form-footer/src/FormFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ const buttonStyle = css`
/**
* Types
*/
interface BaseButtonProps {
text?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}

interface BackButtonProps extends BaseButtonProps {
leftGlyph?: React.ReactElement | null;
}

interface CancelButtonProps extends BackButtonProps {}

export interface FormFooterProps extends HTMLElementProps<'footer'> {
/**
Expand All @@ -78,26 +88,65 @@ export interface FormFooterProps extends HTMLElementProps<'footer'> {
*/
primaryButton: React.ReactElement | PrimaryButtonProps;

/**
* The cancel button which will only appear if cancelButtonProps is defined.
* Defined as an object with the shape:
*
* ```ts
* interface CancelButtonProps {
* text: string;
* onClick?: React.MouseEventHandler<HTMLButtonElement>;
* leftGlyph?: React.ReactElement | null;
* }
* ```
*
* darkMode is handled internally so you do not have to pass the darkMode prop.
*/
cancelButtonProps?: CancelButtonProps;

/**
* The back button which will only appear if backButtonProps is defined.
* Defined as an object with the shape:
*
* ```ts
* interface BackButtonProps {
* text: string;
* onClick?: React.MouseEventHandler<HTMLButtonElement>;
* leftGlyph?: React.ReactElement | null;
* }
* ```
*
* darkMode is handled internally so you do not have to pass the darkMode prop.
*/
backButtonProps?: BackButtonProps;

/**
* Text for the cancel button.
* A cancel button will only appear if this text is defined.
*
* @default "Cancel"
* @deprecated since version 3.0.17 - use cancelButtonProps instead
*/
cancelButtonText?: string;

/**
* onClick callback for the cancel button
*
* @deprecated since version 3.0.17 - use cancelButtonProps instead
*/
onCancel?: React.MouseEventHandler<HTMLButtonElement>;

/**
* Text for the back button. A back button will only appear if text is defined.
*
* @deprecated since version 3.0.17 - use backButtonProps instead
*/
backButtonText?: string;

/**
* onClick callback for the back button
*
* @deprecated since version 3.0.17 - use backButtonProps instead
*/
onBackClick?: React.MouseEventHandler<HTMLButtonElement>;

Expand Down Expand Up @@ -130,6 +179,8 @@ export interface FormFooterProps extends HTMLElementProps<'footer'> {
*/
export default function FormFooter({
primaryButton,
cancelButtonProps,
backButtonProps,
onCancel,
cancelButtonText = 'Cancel',
backButtonText,
Expand All @@ -141,23 +192,41 @@ export default function FormFooter({
...rest
}: FormFooterProps) {
const { theme, darkMode } = useDarkMode(darkModeProp);

const backButton = {
text: backButtonProps?.text || backButtonText,
onClick: backButtonProps?.onClick || onBackClick,
leftGlyph:
backButtonProps?.leftGlyph === undefined ? (
<ArrowLeftIcon data-testid="lg-form_footer-back-button-icon" />
) : (
backButtonProps.leftGlyph ?? undefined
),
};

const cancelButton = {
text: cancelButtonProps?.text || cancelButtonText,
onClick: cancelButtonProps?.onClick || onCancel,
leftGlyph: cancelButtonProps?.leftGlyph ?? undefined,
};

return (
<footer
data-testid="lg-form-footer-footer"
data-testid="lg-form_footer-footer"
className={cx(footerBaseStyle, footerThemeStyle[theme], className)}
{...rest}
>
<div className={cx(contentStyle, contentClassName)}>
{backButtonText && (
{backButton.text && (
<Button
variant="default"
onClick={onBackClick}
onClick={backButton.onClick}
className={buttonStyle}
leftGlyph={<ArrowLeftIcon />}
leftGlyph={backButton.leftGlyph}
darkMode={darkMode}
data-testid="lg-form_footer-back-button"
>
{backButtonText}
{backButton.text}
</Button>
)}
<div className={flexEndContent}>
Expand All @@ -170,15 +239,16 @@ export default function FormFooter({
{errorMessage}
</Banner>
)}
{cancelButtonText && (
{cancelButton.text && (
<Button
variant="default"
onClick={onCancel}
onClick={cancelButton.onClick}
className={buttonStyle}
leftGlyph={cancelButton.leftGlyph}
darkMode={darkMode}
data-testid="lg-form_footer-cancel-button"
>
{cancelButtonText || 'Cancel'}
{cancelButton.text}
</Button>
)}
{isComponentType(primaryButton as React.ReactElement, 'Button') ? (
Expand Down

0 comments on commit 0c4e54a

Please sign in to comment.