Skip to content

Commit

Permalink
Merge pull request #8 from dbudwin/IntrinsicProps
Browse files Browse the repository at this point in the history
Simplify the props to make use of `IntrinsicElements`
  • Loading branch information
dbudwin authored Dec 26, 2020
2 parents c28af8d + e1a3d96 commit 51b8418
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 83 deletions.
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>
);
}

0 comments on commit 51b8418

Please sign in to comment.