Skip to content

Commit c430b12

Browse files
authored
feat: add new ExternalLinkButton component (#757)
* feat: add new `ExternalLinkButton` component component ported over from OCP web console * docs: add usage notes for `ExternalLinkButton`
1 parent cf8553e commit c430b12

File tree

8 files changed

+355
-0
lines changed

8 files changed

+355
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
section: Component groups
3+
subsection: Controls
4+
id: External link button
5+
source: react
6+
propComponents: ['ExternalLinkButton']
7+
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ExternalLinkButton/ExternalLinkButton.md
8+
---
9+
10+
import ExternalLinkButton from "@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton"
11+
12+
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).
13+
14+
## Examples
15+
16+
### Basic external link button
17+
18+
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.
19+
20+
```js file="./ExternalLinkButtonExample.tsx"
21+
22+
```
23+
24+
### Inline external link button
25+
26+
You may use the external link button component inline with other text by using the `inline` property.
27+
28+
```js file="./ExternalLinkButtonInlineExample.tsx"
29+
30+
```
31+
32+
### Passing props to the icon
33+
34+
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.
35+
36+
```js file="./ExternalLinkButtonIconPropsExample.tsx"
37+
38+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import ExternalLinkButton from '@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton';
2+
3+
export const BasicExample = () => (
4+
<ExternalLinkButton
5+
href="https://www.patternfly.org"
6+
variant="primary"
7+
>
8+
Learn more about PatternFly
9+
</ExternalLinkButton>
10+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import ExternalLinkButton from '@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton';
2+
3+
export const BasicExample = () => (
4+
<ExternalLinkButton
5+
href="https://www.patternfly.org"
6+
iconProps={{ title: '(Opens in new tab)' }}
7+
variant="link"
8+
>
9+
Learn more about PatternFly
10+
</ExternalLinkButton>
11+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import ExternalLinkButton from '@patternfly/react-component-groups/dist/dynamic/ExternalLinkButton';
2+
import { Content, ContentVariants } from '@patternfly/react-core';
3+
4+
export const BasicExample = () => (
5+
<Content component={ContentVariants.p}>
6+
Today, I had a burger for lunch. It reminded me of the
7+
<ExternalLinkButton
8+
href="https://www.patternfly.org"
9+
isInline
10+
variant="link"
11+
>
12+
PatternFly
13+
</ExternalLinkButton>
14+
design system.
15+
</Content>
16+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ExternalLinkButton } from './ExternalLinkButton';
2+
import { render } from '@testing-library/react';
3+
4+
describe('ExternalLinkButton component', () => {
5+
test('should render', () => {
6+
expect(render(<ExternalLinkButton />)).toMatchSnapshot();
7+
});
8+
9+
test('should accept IconProps', () => {
10+
expect(render(<ExternalLinkButton iconProps={{ className: "my-external-link-icon", title: "(Opens in new tab)" }} />)).toMatchSnapshot();
11+
});
12+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Button, ButtonProps } from '@patternfly/react-core';
2+
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
3+
import type { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon';
4+
import { forwardRef, Ref } from 'react';
5+
6+
/** extends ButtonProps */
7+
export interface ExternalLinkButtonProps extends ButtonProps {
8+
/** Additional props to pass to the icon */
9+
iconProps?: SVGIconProps;
10+
};
11+
12+
export const ExternalLinkButton = forwardRef(({ iconProps, ...props }: ExternalLinkButtonProps, ref: Ref<HTMLAnchorElement>) => (
13+
<Button
14+
component="a"
15+
icon={<ExternalLinkAltIcon {...iconProps} />}
16+
iconPosition="right"
17+
ouiaId="ExternalLinkButton"
18+
ref={ref}
19+
rel="noopener noreferrer"
20+
target="_blank"
21+
{...props}
22+
/>
23+
));
24+
25+
export default ExternalLinkButton;
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`ExternalLinkButton component should accept IconProps 1`] = `
4+
{
5+
"asFragment": [Function],
6+
"baseElement": <body>
7+
<div>
8+
<a
9+
class="pf-v6-c-button pf-m-primary"
10+
data-ouia-component-id="ExternalLinkButton"
11+
data-ouia-component-type="PF6/Button"
12+
data-ouia-safe="true"
13+
rel="noopener noreferrer"
14+
target="_blank"
15+
>
16+
<span
17+
class="pf-v6-c-button__icon"
18+
>
19+
<svg
20+
aria-labelledby="icon-title-1"
21+
class="pf-v6-svg my-external-link-icon"
22+
fill="currentColor"
23+
height="1em"
24+
role="img"
25+
viewBox="0 0 512 512"
26+
width="1em"
27+
>
28+
<title
29+
id="icon-title-1"
30+
>
31+
(Opens in new tab)
32+
</title>
33+
<path
34+
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"
35+
/>
36+
</svg>
37+
</span>
38+
</a>
39+
</div>
40+
</body>,
41+
"container": <div>
42+
<a
43+
class="pf-v6-c-button pf-m-primary"
44+
data-ouia-component-id="ExternalLinkButton"
45+
data-ouia-component-type="PF6/Button"
46+
data-ouia-safe="true"
47+
rel="noopener noreferrer"
48+
target="_blank"
49+
>
50+
<span
51+
class="pf-v6-c-button__icon"
52+
>
53+
<svg
54+
aria-labelledby="icon-title-1"
55+
class="pf-v6-svg my-external-link-icon"
56+
fill="currentColor"
57+
height="1em"
58+
role="img"
59+
viewBox="0 0 512 512"
60+
width="1em"
61+
>
62+
<title
63+
id="icon-title-1"
64+
>
65+
(Opens in new tab)
66+
</title>
67+
<path
68+
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"
69+
/>
70+
</svg>
71+
</span>
72+
</a>
73+
</div>,
74+
"debug": [Function],
75+
"findAllByAltText": [Function],
76+
"findAllByDisplayValue": [Function],
77+
"findAllByLabelText": [Function],
78+
"findAllByPlaceholderText": [Function],
79+
"findAllByRole": [Function],
80+
"findAllByTestId": [Function],
81+
"findAllByText": [Function],
82+
"findAllByTitle": [Function],
83+
"findByAltText": [Function],
84+
"findByDisplayValue": [Function],
85+
"findByLabelText": [Function],
86+
"findByPlaceholderText": [Function],
87+
"findByRole": [Function],
88+
"findByTestId": [Function],
89+
"findByText": [Function],
90+
"findByTitle": [Function],
91+
"getAllByAltText": [Function],
92+
"getAllByDisplayValue": [Function],
93+
"getAllByLabelText": [Function],
94+
"getAllByPlaceholderText": [Function],
95+
"getAllByRole": [Function],
96+
"getAllByTestId": [Function],
97+
"getAllByText": [Function],
98+
"getAllByTitle": [Function],
99+
"getByAltText": [Function],
100+
"getByDisplayValue": [Function],
101+
"getByLabelText": [Function],
102+
"getByPlaceholderText": [Function],
103+
"getByRole": [Function],
104+
"getByTestId": [Function],
105+
"getByText": [Function],
106+
"getByTitle": [Function],
107+
"queryAllByAltText": [Function],
108+
"queryAllByDisplayValue": [Function],
109+
"queryAllByLabelText": [Function],
110+
"queryAllByPlaceholderText": [Function],
111+
"queryAllByRole": [Function],
112+
"queryAllByTestId": [Function],
113+
"queryAllByText": [Function],
114+
"queryAllByTitle": [Function],
115+
"queryByAltText": [Function],
116+
"queryByDisplayValue": [Function],
117+
"queryByLabelText": [Function],
118+
"queryByPlaceholderText": [Function],
119+
"queryByRole": [Function],
120+
"queryByTestId": [Function],
121+
"queryByText": [Function],
122+
"queryByTitle": [Function],
123+
"rerender": [Function],
124+
"unmount": [Function],
125+
}
126+
`;
127+
128+
exports[`ExternalLinkButton component should render 1`] = `
129+
{
130+
"asFragment": [Function],
131+
"baseElement": <body>
132+
<div>
133+
<a
134+
class="pf-v6-c-button pf-m-primary"
135+
data-ouia-component-id="ExternalLinkButton"
136+
data-ouia-component-type="PF6/Button"
137+
data-ouia-safe="true"
138+
rel="noopener noreferrer"
139+
target="_blank"
140+
>
141+
<span
142+
class="pf-v6-c-button__icon"
143+
>
144+
<svg
145+
aria-hidden="true"
146+
class="pf-v6-svg"
147+
fill="currentColor"
148+
height="1em"
149+
role="img"
150+
viewBox="0 0 512 512"
151+
width="1em"
152+
>
153+
<path
154+
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"
155+
/>
156+
</svg>
157+
</span>
158+
</a>
159+
</div>
160+
</body>,
161+
"container": <div>
162+
<a
163+
class="pf-v6-c-button pf-m-primary"
164+
data-ouia-component-id="ExternalLinkButton"
165+
data-ouia-component-type="PF6/Button"
166+
data-ouia-safe="true"
167+
rel="noopener noreferrer"
168+
target="_blank"
169+
>
170+
<span
171+
class="pf-v6-c-button__icon"
172+
>
173+
<svg
174+
aria-hidden="true"
175+
class="pf-v6-svg"
176+
fill="currentColor"
177+
height="1em"
178+
role="img"
179+
viewBox="0 0 512 512"
180+
width="1em"
181+
>
182+
<path
183+
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"
184+
/>
185+
</svg>
186+
</span>
187+
</a>
188+
</div>,
189+
"debug": [Function],
190+
"findAllByAltText": [Function],
191+
"findAllByDisplayValue": [Function],
192+
"findAllByLabelText": [Function],
193+
"findAllByPlaceholderText": [Function],
194+
"findAllByRole": [Function],
195+
"findAllByTestId": [Function],
196+
"findAllByText": [Function],
197+
"findAllByTitle": [Function],
198+
"findByAltText": [Function],
199+
"findByDisplayValue": [Function],
200+
"findByLabelText": [Function],
201+
"findByPlaceholderText": [Function],
202+
"findByRole": [Function],
203+
"findByTestId": [Function],
204+
"findByText": [Function],
205+
"findByTitle": [Function],
206+
"getAllByAltText": [Function],
207+
"getAllByDisplayValue": [Function],
208+
"getAllByLabelText": [Function],
209+
"getAllByPlaceholderText": [Function],
210+
"getAllByRole": [Function],
211+
"getAllByTestId": [Function],
212+
"getAllByText": [Function],
213+
"getAllByTitle": [Function],
214+
"getByAltText": [Function],
215+
"getByDisplayValue": [Function],
216+
"getByLabelText": [Function],
217+
"getByPlaceholderText": [Function],
218+
"getByRole": [Function],
219+
"getByTestId": [Function],
220+
"getByText": [Function],
221+
"getByTitle": [Function],
222+
"queryAllByAltText": [Function],
223+
"queryAllByDisplayValue": [Function],
224+
"queryAllByLabelText": [Function],
225+
"queryAllByPlaceholderText": [Function],
226+
"queryAllByRole": [Function],
227+
"queryAllByTestId": [Function],
228+
"queryAllByText": [Function],
229+
"queryAllByTitle": [Function],
230+
"queryByAltText": [Function],
231+
"queryByDisplayValue": [Function],
232+
"queryByLabelText": [Function],
233+
"queryByPlaceholderText": [Function],
234+
"queryByRole": [Function],
235+
"queryByTestId": [Function],
236+
"queryByText": [Function],
237+
"queryByTitle": [Function],
238+
"rerender": [Function],
239+
"unmount": [Function],
240+
}
241+
`;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './ExternalLinkButton';
2+
export * from './ExternalLinkButton';

0 commit comments

Comments
 (0)