diff --git a/README.md b/README.md index c546fe2..02c4e34 100644 --- a/README.md +++ b/README.md @@ -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: `` +3. Use the following for links you want to open in a new tab: `react-secure-link on NPM` ### 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` 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 - + ``` ### Advance Usage Example ```tsx console.log("Clicked")} > react-secure-link on NPM diff --git a/package-lock.json b/package-lock.json index fd9c4c3..27f86ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "react-secure-link", - "version": "2.0.0", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.0.0", + "version": "3.0.0", "license": "GPL-3.0-or-later", "devDependencies": { "@babel/core": "^7.12.9", diff --git a/package.json b/package.json index 08a3837..457b9c7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/__tests__/secure-link.test.tsx b/src/components/__tests__/secure-link.test.tsx index 923858d..57841b8 100644 --- a/src/components/__tests__/secure-link.test.tsx +++ b/src/components/__tests__/secure-link.test.tsx @@ -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; @@ -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(); +function renderSecureLinkWithoutChildren(): void { + render(); } -function renderSecureLinkWithChildren(text: string, uniqueKey: Key): void { - render({text}); +function renderSecureLinkWithChildren(text: string): void { + render({text}); } 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(); - 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(); - 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); }); }); diff --git a/src/components/secure-link.tsx b/src/components/secure-link.tsx index efc8080..ce82146 100644 --- a/src/components/secure-link.tsx +++ b/src/components/secure-link.tsx @@ -1,19 +1,15 @@ -import React, { Key, ReactElement } from "react"; +import React, { ReactElement } from "react"; -interface SecureLinkProps extends React.HTMLAttributes { - url: string; - uniqueKey?: Key; -} +type SecureLinkProps = JSX.IntrinsicElements["a"] -export function SecureLink({ url, uniqueKey, children }: SecureLinkProps): ReactElement { +export function SecureLink({ ...props }: SecureLinkProps): ReactElement { return ( - {children ? children : url} + {props.children ? props.children : props.href} ); }