Skip to content

feat: add new ExternalLinkButton component #757

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
section: Component groups
subsection: Controls
id: External link button
source: react
propComponents: ['ExternalLinkButton']
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExternalLinkButton/ExternalLinkButton.md
---

import ExternalLinkButton from "@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton"

The **external link button** component is a button that opens links in an external tab. To further customize this component, you can also utilize all properties of the [button component](/components/button).

## Examples

### Basic external link button

In order to display a basic external link button, you can use the `href` property to specify the URL and the `variant` property to set the button style.

```js file="./ExternalLinkButtonExample.tsx"

```

### Inline external link button

You may use the external link button component inline with other text by using the `inline` property.

```js file="./ExternalLinkButtonInlineExample.tsx"

```

### Passing props to the icon

You may pass props to the icon using the `iconProps` property. This is useful for customizing the title of the icon for enhanced screen reader support.

```js file="./ExternalLinkButtonIconPropsExample.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ExternalLinkButton from '@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton';

export const BasicExample = () => (
<ExternalLinkButton
href="https://www.patternfly.org"
variant="primary"
>
Learn more about PatternFly
</ExternalLinkButton>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import ExternalLinkButton from '@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton';

export const BasicExample = () => (
<ExternalLinkButton
href="https://www.patternfly.org"
iconProps={{ title: '(Opens in new tab)' }}
variant="link"
>
Learn more about PatternFly
</ExternalLinkButton>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ExternalLinkButton from '@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton';
import { Content, ContentVariants } from '@patternfly/react-core';

export const BasicExample = () => (
<Content component={ContentVariants.p}>
Today, I had a burger for lunch. It reminded me of the
<ExternalLinkButton
href="https://www.patternfly.org"
isInline
variant="link"
>
PatternFly
</ExternalLinkButton>
design system.
</Content>
);
12 changes: 12 additions & 0 deletions packages/module/src/ExternalLinkButton/ExternalLinkButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ExternalLinkButton } from './ExternalLinkButton';
import { render } from '@testing-library/react';

describe('ExternalLinkButton component', () => {
test('should render', () => {
expect(render(<ExternalLinkButton />)).toMatchSnapshot();
});

test('should accept IconProps', () => {
expect(render(<ExternalLinkButton iconProps={{ className: "my-external-link-icon", title: "(Opens in new tab)" }} />)).toMatchSnapshot();
});
});
25 changes: 25 additions & 0 deletions packages/module/src/ExternalLinkButton/ExternalLinkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Button, ButtonProps } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import type { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
import { forwardRef, Ref } from 'react';

/** extends ButtonProps */
export interface ExternalLinkButtonProps extends ButtonProps {
/** Additional props to pass to the icon */
iconProps?: SVGIconProps;
};

export const ExternalLinkButton = forwardRef(({ iconProps, ...props }: ExternalLinkButtonProps, ref: Ref<HTMLAnchorElement>) => (
<Button
component="a"
icon={<ExternalLinkAltIcon {...iconProps} />}
iconPosition="right"
ouiaId="ExternalLinkButton"
ref={ref}
rel="noopener noreferrer"
target="_blank"
{...props}
/>
));

export default ExternalLinkButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ExternalLinkButton component should accept IconProps 1`] = `
{
"asFragment": [Function],
"baseElement": <body>
<div>
<a
class="pf-v6-c-button pf-m-primary"
data-ouia-component-id="ExternalLinkButton"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
rel="noopener noreferrer"
target="_blank"
>
<span
class="pf-v6-c-button__icon"
>
<svg
aria-labelledby="icon-title-1"
class="pf-v6-svg my-external-link-icon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 512 512"
width="1em"
>
<title
id="icon-title-1"
>
(Opens in new tab)
</title>
<path
d="M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"
/>
</svg>
</span>
</a>
</div>
</body>,
"container": <div>
<a
class="pf-v6-c-button pf-m-primary"
data-ouia-component-id="ExternalLinkButton"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
rel="noopener noreferrer"
target="_blank"
>
<span
class="pf-v6-c-button__icon"
>
<svg
aria-labelledby="icon-title-1"
class="pf-v6-svg my-external-link-icon"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 512 512"
width="1em"
>
<title
id="icon-title-1"
>
(Opens in new tab)
</title>
<path
d="M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"
/>
</svg>
</span>
</a>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

exports[`ExternalLinkButton component should render 1`] = `
{
"asFragment": [Function],
"baseElement": <body>
<div>
<a
class="pf-v6-c-button pf-m-primary"
data-ouia-component-id="ExternalLinkButton"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
rel="noopener noreferrer"
target="_blank"
>
<span
class="pf-v6-c-button__icon"
>
<svg
aria-hidden="true"
class="pf-v6-svg"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 512 512"
width="1em"
>
<path
d="M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"
/>
</svg>
</span>
</a>
</div>
</body>,
"container": <div>
<a
class="pf-v6-c-button pf-m-primary"
data-ouia-component-id="ExternalLinkButton"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
rel="noopener noreferrer"
target="_blank"
>
<span
class="pf-v6-c-button__icon"
>
<svg
aria-hidden="true"
class="pf-v6-svg"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 512 512"
width="1em"
>
<path
d="M432,320H400a16,16,0,0,0-16,16V448H64V128H208a16,16,0,0,0,16-16V80a16,16,0,0,0-16-16H48A48,48,0,0,0,0,112V464a48,48,0,0,0,48,48H400a48,48,0,0,0,48-48V336A16,16,0,0,0,432,320ZM488,0h-128c-21.37,0-32.05,25.91-17,41l35.73,35.73L135,320.37a24,24,0,0,0,0,34L157.67,377a24,24,0,0,0,34,0L435.28,133.32,471,169c15,15,41,4.5,41-17V24A24,24,0,0,0,488,0Z"
/>
</svg>
</span>
</a>
</div>,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;
2 changes: 2 additions & 0 deletions packages/module/src/ExternalLinkButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './ExternalLinkButton';
export * from './ExternalLinkButton';
Loading