Skip to content
Merged
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
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,27 @@ Using `react-secure-link` for outbound links prevents the new tab from having ac

1. Add `react-secure-link` to your project via `npm install react-secure-link`
2. Import the package: `import { SecureLink } from "react-secure-link";`
3. Use the following for links you want to open in a new tab: `<SecureLink url="https://www.npmjs.com/package/react-secure-link" text="react-secure-link on NPM" />`
3. Use the following for links you want to open in a new tab: `<SecureLink href="https://www.npmjs.com/package/react-secure-link">react-secure-link on NPM</SecureLink>`

### API

`SecureLink` can be used to make text, images, or other children components clickable. In addition to any prop defined as part of the `React.HTMLAttributes<HTMLAnchorElement>` interface (i.e. `className`, `id`, `role`, `style`), the `SecureLink` component has the following custom props:

| prop | Required | Type | Description |
|-------------|----------|----------|--------------------------------------------------------------------------|
| `url` | Yes | `string` | The URL to navigate to. |
| `uniqueKey` | No | `string` or `number` | A unique key to identify the link. This is being used as a `key` value for the component. For more information, refer to [React's website about keys](https://reactjs.org/docs/lists-and-keys.html#keys). |
`SecureLink` can be used to make text, images, or other children components clickable. In addition, standard `a` element attributes can be pass in as props (i.e. `href`, `className`, `id`, `role`, `style`).

### Basic Usage Example

```tsx
<SecureLink url="https://www.npmjs.com/package/react-secure-link" />
<SecureLink href="https://www.npmjs.com/package/react-secure-link" />
```

### Advance Usage Example

```tsx
<SecureLink
url="https://www.npmjs.com/package/react-secure-link"
href="https://www.npmjs.com/package/react-secure-link"
className="no-link-decoration"
style={{ color: "red" }}
uniqueKey={123}
key={123}
onClick={() => console.log("Clicked")}
>
react-secure-link on NPM
</SecureLink>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-secure-link",
"version": "2.0.0",
"version": "3.0.0",
"description": "A TypeScript compatible React component to avoid security exploits when opening a link in a new tab.",
"keywords": [
"react",
Expand Down
148 changes: 87 additions & 61 deletions src/components/__tests__/secure-link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import "@testing-library/jest-dom";

import React, { Key } from "react";
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";

import React from "react";
import { SecureLink } from "../secure-link";
import each from "jest-each";
import faker from "faker";

let url: string;
Expand All @@ -13,93 +12,120 @@ beforeAll(() => {
url = faker.internet.url();
});

const uniqueKeyPropValues = [
undefined,
null,
faker.random.number(),
faker.random.word(),
];

function renderSecureLinkWithoutChildren(uniqueKey: Key): void {
render(<SecureLink url={url} key={uniqueKey} />);
function renderSecureLinkWithoutChildren(): void {
render(<SecureLink href={url} />);
}

function renderSecureLinkWithChildren(text: string, uniqueKey: Key): void {
render(<SecureLink url={url} key={uniqueKey}>{text}</SecureLink>);
function renderSecureLinkWithChildren(text: string): void {
render(<SecureLink href={url}>{text}</SecureLink>);
}

function getLinkByRole(): HTMLAnchorElement {
return screen.getByRole("link") as HTMLAnchorElement;
}

each(uniqueKeyPropValues).describe(`when given uniqueKey: %s`, (uniqueKey?) => {
describe("when not given children", () => {
it("renders link without crashing", () => {
renderSecureLinkWithoutChildren(uniqueKey);
describe("when not given children", () => {
it("renders link without crashing", () => {
renderSecureLinkWithoutChildren();

expect(getLinkByRole()).toBeInTheDocument();
});
expect(getLinkByRole()).toBeInTheDocument();
});

it("has given text", () => {
renderSecureLinkWithoutChildren(uniqueKey);
it("has given text", () => {
renderSecureLinkWithoutChildren();

expect(getLinkByRole()).toHaveTextContent(url);
});
expect(getLinkByRole()).toHaveTextContent(url);
});

it("links to given URL", () => {
renderSecureLinkWithoutChildren(uniqueKey);
it("links to given URL", () => {
renderSecureLinkWithoutChildren();

expect(getLinkByRole()).toHaveAttribute("href", url);
});
expect(getLinkByRole()).toHaveAttribute("href", url);
});

it("has expected attributes to open link securely", () => {
renderSecureLinkWithoutChildren(uniqueKey);
it("has expected attributes to open link securely", () => {
renderSecureLinkWithoutChildren();

expect(getLinkByRole()).toHaveAttribute("rel", "noopener noreferrer");
});
expect(getLinkByRole()).toHaveAttribute("rel", "noopener noreferrer");
});

it("has expected attributes to open link in new tab", () => {
renderSecureLinkWithoutChildren(uniqueKey);
it("has expected attributes to open link in new tab", () => {
renderSecureLinkWithoutChildren();

expect(getLinkByRole()).toHaveAttribute("target", "_blank");
});
expect(getLinkByRole()).toHaveAttribute("target", "_blank");
});

describe("when given children", () => {
let text: string;
it(`can use intrinsic "a" element attributes`, () => {
const className = faker.random.word();
const style = { color: "red" };
const role = faker.random.word();
const handleClick = jest.fn();

render(<SecureLink href={url} className={className} style={style} role={role} onClick={handleClick} />);

beforeAll(() => {
text = faker.lorem.word();
});
const link = screen.getByRole(role);

it("renders link without crashing", () => {
renderSecureLinkWithChildren(text, uniqueKey);
fireEvent.click(link);

expect(getLinkByRole()).toBeInTheDocument();
});
expect(link).toBeInTheDocument();
expect(link).toHaveClass(className, { exact: true });
expect(link).toHaveStyle(style);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

describe("when given children", () => {
let text: string;

it("has given text", () => {
renderSecureLinkWithChildren(text, uniqueKey);
beforeAll(() => {
text = faker.lorem.word();
});

it("renders link without crashing", () => {
renderSecureLinkWithChildren(text);

expect(getLinkByRole()).toHaveTextContent(text);
});
expect(getLinkByRole()).toBeInTheDocument();
});

it("links to given URL", () => {
renderSecureLinkWithChildren(text, uniqueKey);
it("has given text", () => {
renderSecureLinkWithChildren(text);

expect(getLinkByRole()).toHaveTextContent(text);
});

it("links to given URL", () => {
renderSecureLinkWithChildren(text);

expect(getLinkByRole()).toHaveAttribute("href", url);
});

it("has expected attributes to open link securely", () => {
renderSecureLinkWithChildren(text);

expect(getLinkByRole()).toHaveAttribute("rel", "noopener noreferrer");
});

it("has expected attributes to open link in new tab", () => {
renderSecureLinkWithChildren(text);

expect(getLinkByRole()).toHaveAttribute("target", "_blank");
});

expect(getLinkByRole()).toHaveAttribute("href", url);
});
it(`can use intrinsic "a" element attributes`, () => {
const className = faker.random.word();
const style = { color: "red" };
const role = faker.random.word();
const handleClick = jest.fn();

it("has expected attributes to open link securely", () => {
renderSecureLinkWithChildren(text, uniqueKey);
render(<SecureLink href={url} className={className} style={style} role={role} onClick={handleClick} />);

expect(getLinkByRole()).toHaveAttribute("rel", "noopener noreferrer");
});
const link = screen.getByRole(role);

it("has expected attributes to open link in new tab", () => {
renderSecureLinkWithChildren(text, uniqueKey);
fireEvent.click(link);

expect(getLinkByRole()).toHaveAttribute("target", "_blank");
});
expect(link).toBeInTheDocument();
expect(link).toHaveClass(className, { exact: true });
expect(link).toHaveStyle(style);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
14 changes: 5 additions & 9 deletions src/components/secure-link.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import React, { Key, ReactElement } from "react";
import React, { ReactElement } from "react";

interface SecureLinkProps extends React.HTMLAttributes<HTMLAnchorElement> {
url: string;
uniqueKey?: Key;
}
type SecureLinkProps = JSX.IntrinsicElements["a"]

export function SecureLink({ url, uniqueKey, children }: SecureLinkProps): ReactElement {
export function SecureLink({ ...props }: SecureLinkProps): ReactElement {
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
key={uniqueKey}
{...props}
>
{children ? children : url}
{props.children ? props.children : props.href}
</a>
);
}