Skip to content

Commit

Permalink
Merge branch 'release-22.x' into merge-22-into-23
Browse files Browse the repository at this point in the history
  • Loading branch information
brian-smith-tcril committed Feb 6, 2025
2 parents ec8553d + b0187dd commit 1d64636
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 88 deletions.
7 changes: 6 additions & 1 deletion src/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import renderer from 'react-test-renderer';
Expand Down Expand Up @@ -96,7 +97,11 @@ describe('<Button />', () => {
test('test button as hyperlink', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ref = (_current: HTMLAnchorElement) => {}; // Check typing of a ref - should not show type errors.
render(<Button as={Hyperlink} ref={ref} destination="https://www.poop.com/💩">Button</Button>);
render(
<IntlProvider locale="en">
<Button as={Hyperlink} ref={ref} destination="https://www.poop.com/💩">Button</Button>
</IntlProvider>,
);
expect(screen.getByRole('link').getAttribute('href')).toEqual('https://www.poop.com/💩');
});
});
Expand Down
5 changes: 5 additions & 0 deletions src/DataTable/TablePagination.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import React, { useContext } from 'react';
import { useIntl } from 'react-intl';
import DataTableContext from './DataTableContext';
import Pagination from '../Pagination';
import messages from './messages';

function TablePagination() {
const intl = useIntl();

const {
pageCount, state, gotoPage,
} = useContext(DataTableContext);
Expand All @@ -19,6 +23,7 @@ function TablePagination() {
currentPage={pageIndex + 1}
onPageSelect={(pageNum) => gotoPage(pageNum - 1)}
pageCount={pageCount}
paginationLabel={intl.formatMessage(messages.paginationLabel)}
icons={{
leftIcon: null,
rightIcon: null,
Expand Down
6 changes: 5 additions & 1 deletion src/DataTable/TablePaginationMinimal.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React, { useContext } from 'react';
import { useIntl } from 'react-intl';
import DataTableContext from './DataTableContext';
import Pagination from '../Pagination';
import { ArrowBackIos, ArrowForwardIos } from '../../icons';
import messages from './messages';

function TablePaginationMinimal() {
const intl = useIntl();

const {
nextPage, pageCount, gotoPage, state,
} = useContext(DataTableContext);
Expand All @@ -20,7 +24,7 @@ function TablePaginationMinimal() {
variant="minimal"
currentPage={pageIndex + 1}
pageCount={pageCount}
paginationLabel="table pagination"
paginationLabel={intl.formatMessage(messages.paginationLabel)}
onPageSelect={(pageNum) => gotoPage(pageNum - 1)}
icons={{
leftIcon: ArrowBackIos,
Expand Down
11 changes: 11 additions & 0 deletions src/DataTable/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';

const messages = defineMessages({
paginationLabel: {
id: 'pgn.DataTable.paginationLabel',
defaultMessage: 'table pagination',
description: 'Accessibile name for the navigation element of a pagination component',
},
});

export default messages;
6 changes: 5 additions & 1 deletion src/DataTable/tests/TableFooter.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ describe('<TableFooter />', () => {
it('Renders the default footer', () => {
render(<TableFooterWrapper />);
expect(screen.getByTestId('row-status')).toBeInTheDocument();
expect(screen.getByLabelText('table pagination')).toBeInTheDocument();

// The TableFooter contains two components that have the aria-label
// "table pagination" - DataTable and DataTableMinimal.
const tables = screen.getAllByLabelText('table pagination');
tables.forEach(table => expect(table).toBeInTheDocument());
});

it('accepts a class name', () => {
Expand Down
9 changes: 6 additions & 3 deletions src/DataTable/tests/TablePagination.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { render, act, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import userEvent from '@testing-library/user-event';

import TablePagination from '../TablePagination';
Expand All @@ -14,9 +15,11 @@ const instance = {
// eslint-disable-next-line react/prop-types
function PaginationWrapper({ value }) {
return (
<DataTableContext.Provider value={value}>
<TablePagination />
</DataTableContext.Provider>
<IntlProvider>
<DataTableContext.Provider value={value}>
<TablePagination />
</DataTableContext.Provider>
</IntlProvider>
);
}

Expand Down
70 changes: 50 additions & 20 deletions src/Hyperlink/Hyperlink.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import Hyperlink from '.';
import Hyperlink, { HyperlinkProps } from '.';

const destination = 'destination';
const destination = 'http://destination.example';
const content = 'content';
const onClick = jest.fn();
const onClick = jest.fn().mockImplementation((e) => e.preventDefault());
const props = {
destination,
onClick,
Expand All @@ -20,13 +20,37 @@ const externalLinkProps = {
...props,
};

interface LinkProps extends HyperlinkProps {
to: string;
}

function Link({ to, children, ...rest }: LinkProps) {
return (
<a
data-testid="custom-hyperlink-element"
href={to}
{...rest}
>
{children}
</a>
);
}

function HyperlinkWrapper({ children, ...rest }: HyperlinkProps) {
return (
<IntlProvider locale="en">
<Hyperlink {...rest}>{children}</Hyperlink>
</IntlProvider>
);
}

describe('correct rendering', () => {
beforeEach(() => {
onClick.mockClear();
jest.clearAllMocks();
});

it('renders Hyperlink', async () => {
const { getByRole } = render(<Hyperlink {...props}>{content}</Hyperlink>);
const { getByRole } = render(<HyperlinkWrapper {...props}>{content}</HyperlinkWrapper>);
const wrapper = getByRole('link');
expect(wrapper).toBeInTheDocument();

Expand All @@ -36,12 +60,29 @@ describe('correct rendering', () => {
expect(wrapper).toHaveAttribute('href', destination);
expect(wrapper).toHaveAttribute('target', '_self');

// Clicking on the link should call the onClick handler
await userEvent.click(wrapper);
expect(onClick).toHaveBeenCalledTimes(1);
});

it('renders with custom element type via "as" prop', () => {
const propsWithoutDestination = {
to: destination, // `to` simulates common `Link` components' prop
};
const { getByRole } = render(<HyperlinkWrapper as={Link} {...propsWithoutDestination}>{content}</HyperlinkWrapper>);
const wrapper = getByRole('link');
expect(wrapper).toBeInTheDocument();

expect(wrapper).toHaveClass('pgn__hyperlink');
expect(wrapper).toHaveClass('standalone-link');
expect(wrapper).toHaveTextContent(content);
expect(wrapper).toHaveAttribute('href', destination);
expect(wrapper).toHaveAttribute('target', '_self');
expect(wrapper).toHaveAttribute('data-testid', 'custom-hyperlink-element');
});

it('renders an underlined Hyperlink', async () => {
const { getByRole } = render(<Hyperlink isInline {...props}>{content}</Hyperlink>);
const { getByRole } = render(<HyperlinkWrapper isInline {...props}>{content}</HyperlinkWrapper>);
const wrapper = getByRole('link');
expect(wrapper).toBeInTheDocument();
expect(wrapper).toHaveClass('pgn__hyperlink');
Expand All @@ -50,7 +91,7 @@ describe('correct rendering', () => {
});

it('renders external Hyperlink', () => {
const { getByRole, getByTestId } = render(<Hyperlink {...externalLinkProps}>{content}</Hyperlink>);
const { getByRole, getByTestId } = render(<HyperlinkWrapper {...externalLinkProps}>{content}</HyperlinkWrapper>);
const wrapper = getByRole('link');
const icon = getByTestId('hyperlink-icon');
const iconSvg = icon.querySelector('svg');
Expand All @@ -66,19 +107,8 @@ describe('correct rendering', () => {

describe('security', () => {
it('prevents reverse tabnabbing for links with target="_blank"', () => {
const { getByRole } = render(<Hyperlink {...externalLinkProps}>{content}</Hyperlink>);
const { getByRole } = render(<HyperlinkWrapper {...externalLinkProps}>{content}</HyperlinkWrapper>);
const wrapper = getByRole('link');
expect(wrapper).toHaveAttribute('rel', 'noopener noreferrer');
});
});

describe('event handlers are triggered correctly', () => {
it('should fire onClick', async () => {
const spy = jest.fn();
const { getByRole } = render(<Hyperlink {...props} onClick={spy}>{content}</Hyperlink>);
const wrapper = getByRole('link');
expect(spy).toHaveBeenCalledTimes(0);
await userEvent.click(wrapper);
expect(spy).toHaveBeenCalledTimes(1);
});
});
15 changes: 14 additions & 1 deletion src/Hyperlink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ categories:
- Buttonlike
status: 'Needs Work'
designStatus: 'Done'
devStatus: 'To Do'
devStatus: 'Done'
notes: |
Improve prop naming. Deprecate content prop.
Use React.forwardRef for ref forwarding.
Expand Down Expand Up @@ -100,3 +100,16 @@ notes: |
</div>
</div>
```

## with custom link element (e.g., using a router)

``Hyperlink`` typically relies on the standard HTML anchor tag (i.e., ``a``); however, this behavior may be overriden when the destination link is to an internal route where it should be using routing instead (e.g., ``Link`` from React Router).

```jsx live
<Hyperlink
as={GatsbyLink}
to="/components/button"
>
Button
</Hyperlink>
```
Loading

0 comments on commit 1d64636

Please sign in to comment.