diff --git a/.codesandbox/templates/reach-ui/package.json b/.codesandbox/templates/reach-ui/package.json
index 5e0e85008..fe8005453 100644
--- a/.codesandbox/templates/reach-ui/package.json
+++ b/.codesandbox/templates/reach-ui/package.json
@@ -30,10 +30,10 @@
"@reach/utils": "latest",
"@reach/visually-hidden": "latest",
"@reach/window-size": "latest",
- "@types/react": "^17.0.2",
- "@types/react-dom": "^17.0.2",
- "react": "^17.0.1",
- "react-dom": "^17.0.1",
+ "@types/react": "^17.0.3",
+ "@types/react-dom": "^17.0.3",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
"react-scripts": "3.4.1"
},
"devDependencies": {
diff --git a/.gitignore b/.gitignore
index 395195e16..cd9c6c763 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,7 @@ pids
/doc.json
# bundled files
-/packages/*/dist
+/packages/*/**/dist
/packages/*/errors
# editor specific
diff --git a/README.md b/README.md
index 76475b17d..005eca5d4 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,11 @@ This is our current release process. It's not perfect, but it has almost the rig
$ yarn build
$ yarn test
+# If you aren't already on the main branch, be sure to check it out
+# and merge release changes from `develop`
+$ git checkout main
+$ git merge develop
+
# Generate the changelog and copy it somewhere for later. We'll
# automate this part eventually, but for now you can get the changelog
# with:
diff --git a/jest.config.js b/jest.config.js
index 5b9bb6ad2..ba217f3a7 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -25,3 +25,13 @@ module.exports = {
require.resolve("jest-watch-typeahead/testname"),
],
};
+
+if (process.env.USE_REACT_16 === "true") {
+ module.exports.cacheDirectory = ".cache/jest-cache-react-16";
+ module.exports.moduleNameMapper = {
+ ...module.exports.moduleNameMapper,
+ "^react-is((\\/.*)?)$": "react-is-16$1",
+ "^react-dom((\\/.*)?)$": "react-dom-16$1",
+ "^react((\\/.*)?)$": "react-16$1",
+ };
+}
diff --git a/lerna.json b/lerna.json
index dc2d82d06..9d8e4b5e1 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,9 +1,15 @@
{
- "version": "0.13.1",
+ "version": "0.15.0",
"registry": "https://registry.npmjs.org/",
"publishConfig": {
"access": "public"
},
"npmClient": "yarn",
- "useWorkspaces": true
+ "useWorkspaces": true,
+ "ignoreChanges": [
+ "**/__fixtures__/**",
+ "**/__tests__/**",
+ "**/*.md",
+ "**/examples/**"
+ ]
}
diff --git a/package.json b/package.json
index accfc85fc..bad29e59b 100644
--- a/package.json
+++ b/package.json
@@ -10,52 +10,55 @@
},
"scripts": {
"start": "start-storybook -p 9001 -c .storybook",
+ "test:react-16": "USE_REACT_16=true jest",
"test": "jest",
"build": "yarn build:packages",
"build:packages": "preconstruct build",
"ver": "lerna version --no-push --exact",
"changes": "dotenv lerna-changelog",
"clean": "git clean -e '!/.env' -e '!/website-deploy-key' -e '!/website-deploy-key.pub' -fdX .",
- "lint": "eslint .",
+ "lint:packages": "eslint packages",
+ "lint": "yarn lint:packages",
"fix": "manypkg fix && preconstruct fix",
- "dev": "yarn start",
+ "dev": "preconstruct dev && yarn start",
+ "pc": "preconstruct",
"postinstall": "manypkg check && preconstruct dev",
"release": "lerna publish from-git --yes --pre-dist-tag next"
},
"dependencies": {
- "@babel/core": "^7.12.10",
- "@babel/plugin-proposal-class-properties": "^7.12.1",
- "@babel/preset-env": "^7.12.11",
- "@babel/preset-react": "^7.12.10",
- "@babel/preset-typescript": "^7.12.7",
+ "@babel/core": "^7.13.14",
+ "@babel/plugin-proposal-class-properties": "^7.13.0",
+ "@babel/preset-env": "^7.13.12",
+ "@babel/preset-react": "^7.13.13",
+ "@babel/preset-typescript": "^7.13.0",
"@manypkg/cli": "^0.17.0",
- "@preconstruct/cli": "^2.0.1",
+ "@preconstruct/cli": "^2.0.6",
"@reach/router": "^1.3.4",
"@storybook/addon-actions": "^6.0.28",
"@storybook/addon-docs": "^6.0.28",
"@storybook/addon-links": "^6.0.28",
"@storybook/addons": "^6.0.28",
"@storybook/react": "^6.0.28",
- "@testing-library/dom": "^7.29.0",
- "@testing-library/jest-dom": "^5.11.6",
- "@testing-library/react": "^11.2.2",
- "@testing-library/user-event": "^12.6.0",
- "@types/eslint": "^7.2.0",
- "@types/highlight-words-core": "^1.2.0",
+ "@testing-library/dom": "^7.30.1",
+ "@testing-library/jest-dom": "^5.11.10",
+ "@testing-library/react": "^11.2.5",
+ "@testing-library/react-hooks": "^5.1.0",
+ "@testing-library/user-event": "^13.0.16",
+ "@types/eslint": "^7.2.7",
"@types/invariant": "^2.2.33",
"@types/jest": "^26.0.0",
"@types/lodash": "^4.14.155",
"@types/node": "^12.12.47",
- "@types/react": "^17.0.2",
- "@types/react-dom": "^17.0.1",
+ "@types/react": "^17.0.3",
+ "@types/react-dom": "^17.0.3",
"@types/react-router-dom": "^5.1.7",
"@types/react-test-renderer": "^17.0.1",
"@types/sinon": "^9.0.4",
"@types/styled-components": "^5.1.2",
"@types/tabbable": "^3.1.0",
"@types/warning": "^3.0.0",
- "@typescript-eslint/eslint-plugin": "^3.9.1",
- "@typescript-eslint/parser": "^3.9.1",
+ "@typescript-eslint/eslint-plugin": "^4.20.0",
+ "@typescript-eslint/parser": "^4.20.0",
"autoprefixer": "^9.8.6",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
@@ -65,40 +68,44 @@
"babel-plugin-macros": "^3.0.1",
"cross-env": "^7.0.2",
"dotenv-cli": "^3.2.0",
- "eslint": "^7.7.0",
- "eslint-config-react-app": "^5.2.1",
- "eslint-plugin-flowtype": "^5.2.0",
- "eslint-plugin-import": "^2.22.0",
- "eslint-plugin-jsx-a11y": "^6.3.1",
- "eslint-plugin-react": "^7.20.6",
- "eslint-plugin-react-hooks": "^4.1.0",
+ "eslint": "^7.23.0",
+ "eslint-config-react-app": "^6.0.0",
+ "eslint-plugin-flowtype": "^5.4.0",
+ "eslint-plugin-import": "^2.22.1",
+ "eslint-plugin-jest": "^24.3.2",
+ "eslint-plugin-jsx-a11y": "^6.4.1",
+ "eslint-plugin-react": "^7.23.1",
+ "eslint-plugin-react-hooks": "^4.2.0",
+ "eslint-plugin-testing-library": "^3.10.2",
"husky": "^4.2.5",
- "jest": "^26.4.1",
- "jest-axe": "^3.5.0",
- "jest-watch-typeahead": "0.6.0",
+ "jest": "^26.6.3",
+ "jest-axe": "^4.1.0",
+ "jest-watch-typeahead": "0.6.1",
"lerna": "^3.22.1",
"lerna-changelog": "^1.0.1",
- "lodash": "^4.17.20",
+ "lodash": "^4.17.21",
"match-sorter": "^6.0.1",
"prettier": "^2.0.5",
"pretty-quick": "^3.0.0",
"prop-types": "^15.7.2",
- "react": "^17.0.1",
- "react-dom": "^17.0.1",
+ "react": "^17.0.2",
+ "react-16": "npm:react@^16.14.0",
+ "react-dom": "^17.0.2",
+ "react-dom-16": "npm:react-dom@^16.14.0",
+ "react-is": "^17.0.2",
+ "react-is-16": "npm:react-is@^16.13.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-spring": "^8.0.27",
"react-test-renderer": "^16.13.1",
"sinon": "^9.0.3",
"styled-components": "^5.1.1",
- "ts-jest": "^26.3.0",
- "ts-loader": "^8.0.2",
- "ts-node": "^8.10.2",
- "typescript": "^4.0.2"
+ "ts-jest": "^26.5.4",
+ "typescript": "^4.2.3"
},
"resolutions": {
- "@types/react": "^17.0.2",
- "@types/react-dom": "^17.0.1"
+ "@types/react": "^17.0.3",
+ "@types/react-dom": "^17.0.3"
},
"workspaces": [
"packages/*"
@@ -131,15 +138,8 @@
"__DEV__": "readonly"
},
"rules": {
- "no-unused-vars": [
- 1,
- {
- "args": "after-used",
- "ignoreRestSiblings": true,
- "argsIgnorePattern": "^(event|_)$"
- }
- ],
"import/first": 0,
+ "import/no-anonymous-default-export": 0,
"jsx-a11y/no-static-element-interactions": [
1,
{
@@ -157,7 +157,7 @@
},
"eslintIgnore": [
"node_modules",
- "packages/*/dist"
+ "packages/*/**/dist"
],
"prettier": {
"singleQuote": false
diff --git a/packages/accordion/__tests__/accordion.test.tsx b/packages/accordion/__tests__/accordion.test.tsx
index 6c4b3e1fc..b29473318 100644
--- a/packages/accordion/__tests__/accordion.test.tsx
+++ b/packages/accordion/__tests__/accordion.test.tsx
@@ -1,6 +1,5 @@
import * as React from "react";
-import { render, act, fireEvent } from "$test/utils";
-import { AxeResults } from "$test/types";
+import { render, fireEvent } from "$test/utils";
import { axe } from "jest-axe";
import {
Accordion,
@@ -11,110 +10,81 @@ import {
describe("", () => {
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
- let { container, getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
- let results: AxeResults = null as any;
- await act(async () => {
- results = await axe(container);
- });
+ it("Should not have ARIA violations", async () => {
+ let { container, buttons } = renderTestAccordion();
+ let results = await axe(container);
+
expect(results).toHaveNoViolations();
- act(() => void fireEvent.click(getByText("Button One")));
- await act(async () => {
- results = await axe(container);
- });
+ // Toggle to another panel and check again
+ fireEvent.click(buttons[1]);
+ results = await axe(container);
expect(results).toHaveNoViolations();
});
- it("accepts a custom ID", () => {
- let { getByTestId } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ describe("ARIA attributes", () => {
+ let buttons: HTMLElement[];
+ let panels: HTMLElement[];
+ beforeEach(() => {
+ let rendered = renderTestAccordion((props) => (
+
+ ));
+ buttons = rendered.buttons;
+ panels = rendered.panels;
+ });
- expect(getByTestId("wrapper")).toHaveAttribute("id", "test-id");
- });
+ it("`role` is set to `region` for panel elements", () => {
+ expect(panels[0]).toHaveAttribute("role", "region");
+ expect(panels[1]).toHaveAttribute("role", "region");
+ });
- it("sets the correct state-related aria attributes on toggle", () => {
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ it("`aria-controls` for button elements points to the corresponding panel element id", () => {
+ for (let i = 0; i < panels.length; i++) {
+ let id = panels[i].getAttribute("id");
+ expect(buttons[i]).toHaveAttribute("aria-controls", id);
+ }
+ });
- expect(getByText("Button One")).toHaveAttribute("aria-expanded", "true");
- expect(getByText("Panel One")).toHaveAttribute("data-state", "open");
+ it("`aria-labelledby` for panel elements points to the corresponding button element id", () => {
+ for (let i = 0; i < panels.length; i++) {
+ let id = buttons[i].getAttribute("id");
+ expect(panels[i]).toHaveAttribute("aria-labelledby", id);
+ }
+ });
- expect(getByText("Button Two")).toHaveAttribute("aria-expanded", "false");
- expect(getByText("Panel Two")).toHaveAttribute("data-state", "collapsed");
- expect(getByText("Panel Two")).toHaveAttribute("hidden");
+ it("`aria-expanded` is true for the active button element", () => {
+ expect(buttons[0]).toHaveAttribute("aria-expanded", "true");
+ });
+
+ it("`aria-expanded` is false for the inactive button element", () => {
+ expect(buttons[1]).toHaveAttribute("aria-expanded", "false");
+ });
});
});
describe("rendering", () => {
- it("should open the first panel by default", () => {
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
-
- expect(getByText("Panel One")).toBeVisible();
- expect(getByText("Panel Two")).not.toBeVisible();
+ it("passes DOM props to the wrapper", () => {
+ let { wrapper } = renderTestAccordion((props) => (
+
+ ));
+ expect(wrapper).toHaveAttribute("id", "test-id");
});
- it("should not open any panels by default when using collapsed", () => {
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ it("should show the first panel by default", () => {
+ let { panels } = renderTestAccordion();
+ expect(panels[0]).toBeVisible();
+ expect(panels[1]).not.toBeVisible();
+ });
- expect(getByText("Panel One")).not.toBeVisible();
- expect(getByText("Panel Two")).not.toBeVisible();
+ it("should not show any panels by default when using `collapsed` prop", () => {
+ let { panels } = renderTestAccordion((props) => (
+
+ ));
+ expect(panels[0]).not.toBeVisible();
+ expect(panels[1]).not.toBeVisible();
});
- it("should open panel as specified by defaultIndex", () => {
+ it("should show panel specified by defaultIndex", () => {
let { getByText } = render(
@@ -132,114 +102,182 @@ describe("", () => {
expect(getByText("Panel Two")).toBeVisible();
});
- it("assigns the correct @reach data attributes", () => {
- let { getByTestId, getByText } = render(
-
-
- Button One
- Panel One
-
-
- );
- expect(getByTestId("wrapper")).toHaveAttribute("data-reach-accordion");
- expect(getByTestId("item1")).toHaveAttribute("data-reach-accordion-item");
- expect(getByText("Button One")).toHaveAttribute(
- "data-reach-accordion-button"
- );
- expect(getByText("Panel One")).toHaveAttribute(
- "data-reach-accordion-panel"
- );
+ describe("Internal DOM attributes", () => {
+ let wrapper: HTMLElement;
+ let items: HTMLElement[];
+ let buttons: HTMLElement[];
+ let panels: HTMLElement[];
+ beforeEach(() => {
+ let rendered = renderTestAccordion((props) => (
+
+ ));
+ wrapper = rendered.wrapper;
+ items = rendered.items;
+ buttons = rendered.buttons;
+ panels = rendered.panels;
+ });
+
+ it("`data-reach-accordion` is present on the wrapper element", () => {
+ expect(wrapper).toHaveAttribute("data-reach-accordion");
+ });
+
+ it("`data-reach-accordion-item` is present on the item elements", () => {
+ for (let item of items) {
+ expect(item).toHaveAttribute("data-reach-accordion-item");
+ }
+ });
+
+ it("`data-reach-accordion-button` is present on the button elements", () => {
+ for (let button of buttons) {
+ expect(button).toHaveAttribute("data-reach-accordion-button");
+ }
+ });
+
+ it("`data-reach-accordion-panel` is present on the panel elements", () => {
+ for (let panel of panels) {
+ expect(panel).toHaveAttribute("data-reach-accordion-panel");
+ }
+ });
+
+ it("`data-state` is `open` for the active button element", () => {
+ expect(buttons[0]).toHaveAttribute("data-state", "open");
+ });
+
+ it("`data-state` is `collapsed` for the inactive button element", () => {
+ expect(buttons[1]).toHaveAttribute("data-state", "collapsed");
+ });
+
+ it("`data-state` is `open` for the active panel element", () => {
+ expect(panels[0]).toHaveAttribute("data-state", "open");
+ });
+
+ it("`data-state` is `collapsed` for the inactive panel element", () => {
+ expect(panels[1]).toHaveAttribute("data-state", "collapsed");
+ });
+
+ it("`hidden` is not present for the active panel element", () => {
+ expect(panels[0]).not.toHaveAttribute("hidden");
+ });
+
+ it("`hidden` is present for the inactive panel element", () => {
+ expect(panels[1]).toHaveAttribute("hidden");
+ });
});
});
describe("user events", () => {
- it("should change panel on click", () => {
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ describe("when clicking an inactive button", () => {
+ it("should change the visible panel", () => {
+ let { panels, buttons } = renderTestAccordion();
- let panelOneContent = getByText("Panel One");
+ expect(panels[1]).not.toBeVisible();
+ expect(panels[0]).toBeVisible();
- expect(panelOneContent).toBeVisible();
- fireEvent.click(getByText("Button Two"));
- expect(panelOneContent).not.toBeVisible();
- expect(getByText("Panel Two")).toBeVisible();
- });
+ fireEvent.click(buttons[1]);
+ expect(panels[0]).not.toBeVisible();
+ expect(panels[1]).toBeVisible();
+ });
- it("should call onChange", () => {
- let mockOnChange = jest.fn();
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ it("should call `onChange`", () => {
+ let mockOnChange = jest.fn();
+ let { buttons } = renderTestAccordion((props) => (
+
+ ));
- fireEvent.click(getByText("Button Two"));
- expect(mockOnChange).toHaveBeenCalledTimes(1);
+ fireEvent.click(buttons[1]);
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
+ });
});
- it("should allow collapsing when collapsible", () => {
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ describe("when navigating between focused buttons", () => {
+ // let panels: HTMLElement[];
+ let buttons: HTMLElement[];
+ beforeEach(() => {
+ let rendered = renderTestAccordion();
+ // panels = rendered.panels;
+ buttons = rendered.buttons;
+ });
+
+ it("should move focus to the next focusable button on `ArrowDown` press", () => {
+ buttons[0].focus();
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowDown" });
+ expect(buttons[1]).toHaveFocus();
+ });
- let panelOneContent = getByText("Panel One");
- let panelOneButton = getByText("Button One");
+ it("should move focus to the previous focusable button on `ArrowUp` press", () => {
+ buttons[1].focus();
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowUp" });
+ expect(buttons[0]).toHaveFocus();
+ });
+
+ it("should move focus to the first focusable button on `Home` press", () => {
+ buttons[1].focus();
+ fireEvent.keyDown(document.activeElement!, { key: "Home" });
+ expect(buttons[0]).toHaveFocus();
+ });
- expect(panelOneContent).not.toBeVisible();
- fireEvent.click(panelOneButton);
- expect(panelOneContent).toBeVisible();
- fireEvent.click(panelOneButton);
- expect(panelOneContent).not.toBeVisible();
+ it("should move focus to the last focusable button on `End` press", () => {
+ buttons[0].focus();
+ fireEvent.keyDown(document.activeElement!, { key: "End" });
+ expect(buttons[buttons.length - 1]).toHaveFocus();
+ });
});
- it("should allow multiple when multiple", () => {
- let { getByText } = render(
-
-
- Button One
- Panel One
-
-
- Button Two
- Panel Two
-
-
- );
+ describe("with a fully collapsible accordion", () => {
+ it("should allow collapsing the open panel", () => {
+ let { panels, buttons } = renderTestAccordion((props) => (
+
+ ));
+ fireEvent.click(buttons[0]);
+ expect(panels[0]).not.toBeVisible();
+ });
+ });
- let panelOneContent = getByText("Panel One");
- let panelTwoContent = getByText("Panel Two");
+ describe("with a multi-select accordion", () => {
+ it("should allow multiple visible panels", () => {
+ let { panels, buttons } = renderTestAccordion((props) => (
+
+ ));
- expect(panelOneContent).toBeVisible();
- expect(panelTwoContent).not.toBeVisible();
- fireEvent.click(getByText("Button Two"));
- expect(panelOneContent).toBeVisible();
- expect(panelTwoContent).toBeVisible();
+ fireEvent.click(buttons[1]);
+ expect(panels[0]).toBeVisible();
+ expect(panels[1]).toBeVisible();
+ });
});
});
});
+
+function renderTestAccordion(wrapper?: React.ComponentType) {
+ let Outer = wrapper || Accordion;
+ let { getByText, getByTestId, container } = render(
+
+
+ Button One
+ Panel One
+
+
+ Button Two
+ Panel Two
+
+
+ Button Three
+ Panel Three
+
+
+ );
+ return {
+ container,
+ wrapper: getByTestId("wrapper"),
+ items: [getByTestId("item1"), getByTestId("item2"), getByTestId("item3")],
+ buttons: [
+ getByText("Button One"),
+ getByText("Button Two"),
+ getByText("Button Three"),
+ ],
+ panels: [
+ getByText("Panel One"),
+ getByText("Panel Two"),
+ getByText("Panel Three"),
+ ],
+ };
+}
diff --git a/packages/accordion/package.json b/packages/accordion/package.json
index 03682b8c2..22b437b64 100644
--- a/packages/accordion/package.json
+++ b/packages/accordion/package.json
@@ -1,24 +1,28 @@
{
"name": "@reach/accordion",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React accordion component",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/accordion"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/descendants": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/descendants": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/accordion/src/index.tsx b/packages/accordion/src/index.tsx
index 07fc75caf..f388dd46e 100644
--- a/packages/accordion/src/index.tsx
+++ b/packages/accordion/src/index.tsx
@@ -9,18 +9,14 @@
*/
import * as React from "react";
-import {
- createNamedContext,
- forwardRefWithAs,
- isBoolean,
- isNumber,
- makeId,
- noop,
- useCheckStyles,
- useForkedRef,
- warning,
- wrapEvent,
-} from "@reach/utils";
+import { createNamedContext } from "@reach/utils/context";
+import { isBoolean, isNumber } from "@reach/utils/type-check";
+import { makeId } from "@reach/utils/make-id";
+import { noop } from "@reach/utils/noop";
+import { useCheckStyles } from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
+import warning from "tiny-warning";
import {
createDescendantContext,
DescendantProvider,
@@ -31,6 +27,7 @@ import {
import { useId } from "@reach/auto-id";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { Descendant } from "@reach/descendants";
const AccordionDescendantContext = createDescendantContext(
@@ -62,7 +59,7 @@ enum AccordionStates {
*
* @see Docs https://reach.tech/accordion#accordion-1
*/
-const Accordion = forwardRefWithAs(function Accordion(
+const Accordion = React.forwardRef(function Accordion(
{
as: Comp = "div",
children,
@@ -205,12 +202,12 @@ const Accordion = forwardRefWithAs(function Accordion(
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", AccordionProps>;
/**
* @see Docs https://reach.tech/accordion#accordion-props
*/
-type AccordionProps = {
+interface AccordionProps {
/**
* `Accordion` can accept `AccordionItem` components as children.
*
@@ -276,7 +273,7 @@ type AccordionProps = {
* by the index prop.
*/
multiple?: boolean;
-};
+}
if (__DEV__) {
Accordion.displayName = "Accordion";
@@ -340,10 +337,7 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/accordion#accordionitem
*/
-const AccordionItem = forwardRefWithAs<
- AccordionItemProps & React.ComponentPropsWithRef<"div">,
- "div"
->(function AccordionItem(
+const AccordionItem = React.forwardRef(function AccordionItem(
{ as: Comp = "div", children, disabled = false, ...props },
forwardedRef
) {
@@ -395,12 +389,12 @@ const AccordionItem = forwardRefWithAs<
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", AccordionItemProps>;
/**
* @see Docs https://reach.tech/accordion#accordionitem-props
*/
-type AccordionItemProps = {
+interface AccordionItemProps {
/**
* An `AccordionItem` expects to receive an `AccordionButton` and
* `AccordionPanel` components as its children, though you can also nest other
@@ -417,7 +411,7 @@ type AccordionItemProps = {
* @see Docs https://reach.tech/accordion#accordionitem-disabled
*/
disabled?: boolean;
-};
+}
if (__DEV__) {
AccordionItem.displayName = "AccordionItem";
@@ -437,112 +431,111 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/accordion#accordionbutton
*/
-const AccordionButton = forwardRefWithAs(
- function AccordionButton(
- {
- as: Comp = "button",
- children,
- onClick,
- onKeyDown,
- onMouseDown,
- onPointerDown,
- tabIndex,
- ...props
- },
- forwardedRef
- ) {
- let { onSelectPanel } = React.useContext(AccordionContext);
+const AccordionButton = React.forwardRef(function AccordionButton(
+ {
+ as: Comp = "button",
+ children,
+ onClick,
+ onKeyDown,
+ onMouseDown,
+ onPointerDown,
+ tabIndex,
+ ...props
+ },
+ forwardedRef
+) {
+ let { onSelectPanel } = React.useContext(AccordionContext);
- let {
- disabled,
- buttonId,
- buttonRef: ownRef,
- index,
- panelId,
- state,
- } = React.useContext(AccordionItemContext);
+ let {
+ disabled,
+ buttonId,
+ buttonRef: ownRef,
+ index,
+ panelId,
+ state,
+ } = React.useContext(AccordionItemContext);
- let ref = useForkedRef(forwardedRef, ownRef);
+ let ref = useComposedRefs(forwardedRef, ownRef);
- function handleClick(event: React.MouseEvent) {
- event.preventDefault();
- if (disabled) {
- return;
- }
- ownRef.current.focus();
- onSelectPanel(index);
+ function handleClick(event: React.MouseEvent) {
+ event.preventDefault();
+ if (disabled) {
+ return;
}
-
- let handleKeyDown = useDescendantKeyDown(AccordionDescendantContext, {
- currentIndex: index,
- orientation: "vertical",
- key: "element",
- rotate: true,
- callback(element: HTMLElement) {
- element && element.focus();
- },
- filter: (button) => !button.disabled,
- });
-
- return (
-
- //
- // Click Me
- //
- //
- //
-
- // The title of each accordion header is contained in an element with
- // role `button`. We use an HTML button by default, so we can omit
- // this attribute.
- // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
- // role="button"
-
- // The accordion header `button` element has `aria-controls` set to the
- // ID of the element containing the accordion panel content.
- // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
- aria-controls={panelId}
- // If the accordion panel associated with an accordion header is
- // visible, the header `button` element has `aria-expanded` set to
- // `true`. If the panel is not visible, `aria-expanded` is set to
- // `false`.
- // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
- aria-expanded={state === AccordionStates.Open}
- tabIndex={disabled ? -1 : tabIndex}
- {...props}
- ref={ref}
- data-reach-accordion-button=""
- // If the accordion panel associated with an accordion header is
- // visible, and if the accordion does not permit the panel to be
- // collapsed, the header `button` element has `aria-disabled` set to
- // `true`. We can use `disabled` since we opt for an HTML5 `button`
- // element.
- // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
- disabled={disabled || undefined}
- id={buttonId}
- onClick={wrapEvent(onClick, handleClick)}
- onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
- >
- {children}
-
- );
+ ownRef.current.focus();
+ onSelectPanel(index);
}
-);
+
+ let handleKeyDown = useDescendantKeyDown(AccordionDescendantContext, {
+ currentIndex: index,
+ orientation: "vertical",
+ key: "element",
+ rotate: true,
+ callback(element: HTMLElement) {
+ element && element.focus();
+ },
+ filter: (button) => !button.disabled,
+ });
+
+ return (
+
+ //
+ // Click Me
+ //
+ //
+ //
+
+ // The title of each accordion header is contained in an element with
+ // role `button`. We use an HTML button by default, so we can omit
+ // this attribute.
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
+ // role="button"
+
+ // The accordion header `button` element has `aria-controls` set to the
+ // ID of the element containing the accordion panel content.
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
+ aria-controls={panelId}
+ // If the accordion panel associated with an accordion header is
+ // visible, the header `button` element has `aria-expanded` set to
+ // `true`. If the panel is not visible, `aria-expanded` is set to
+ // `false`.
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
+ aria-expanded={state === AccordionStates.Open}
+ tabIndex={disabled ? -1 : tabIndex}
+ {...props}
+ ref={ref}
+ data-reach-accordion-button=""
+ data-state={getDataState(state)}
+ // If the accordion panel associated with an accordion header is
+ // visible, and if the accordion does not permit the panel to be
+ // collapsed, the header `button` element has `aria-disabled` set to
+ // `true`. We can use `disabled` since we opt for an HTML5 `button`
+ // element.
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#accordion
+ disabled={disabled || undefined}
+ id={buttonId}
+ onClick={composeEventHandlers(onClick, handleClick)}
+ onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
+ >
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"button", AccordionButtonProps>;
/**
* @see Docs https://reach.tech/accordion#accordionbutton-props
*/
-type AccordionButtonProps = {
+interface AccordionButtonProps {
/**
* Typically a text string that serves as a label for the accordion, though
* nested DOM nodes can be passed as well so long as they are valid children
@@ -552,7 +545,7 @@ type AccordionButtonProps = {
* @see Docs https://reach.tech/accordion#accordionbutton-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
AccordionButton.displayName = "AccordionButton";
@@ -572,58 +565,55 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/accordion#accordionpanel
*/
-const AccordionPanel = forwardRefWithAs(
- function AccordionPanel(
- { as: Comp = "div", children, ...props },
- forwardedRef
- ) {
- const { disabled, panelId, buttonId, state } = React.useContext(
- AccordionItemContext
- );
+const AccordionPanel = React.forwardRef(function AccordionPanel(
+ { as: Comp = "div", children, ...props },
+ forwardedRef
+) {
+ const { disabled, panelId, buttonId, state } = React.useContext(
+ AccordionItemContext
+ );
- return (
-
- {children}
-
- );
- }
-);
+ return (
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", AccordionPanelProps>;
/**
* @see Docs https://reach.tech/accordion#accordionpanel-props
*/
-type AccordionPanelProps = {
+interface AccordionPanelProps {
/**
* Inner collapsible content for the accordion item.
*
* @see Docs https://reach.tech/accordion#accordionpanel-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
AccordionPanel.displayName = "AccordionPanel";
@@ -677,22 +667,20 @@ function getDataState(state: AccordionStates) {
////////////////////////////////////////////////////////////////////////////////
// Types
-type AccordionContextValue = {
+interface AccordionContextValue {
id: string | undefined;
openPanels: number[];
-};
+}
-type AccordionItemContextValue = {
+interface AccordionItemContextValue {
index: number;
isExpanded: boolean;
-};
+}
type AccordionDescendant = Descendant & {
disabled: boolean;
};
-type ResultBox = { v: T };
-
type ButtonRef = React.MutableRefObject;
type AccordionIndex = number | number[];
diff --git a/packages/alert-dialog/__tests__/alert-dialog.test.tsx b/packages/alert-dialog/__tests__/alert-dialog.test.tsx
index fedc37d76..40ef4506c 100644
--- a/packages/alert-dialog/__tests__/alert-dialog.test.tsx
+++ b/packages/alert-dialog/__tests__/alert-dialog.test.tsx
@@ -26,7 +26,7 @@ describe("", () => {
});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
let { container, getByText, getByTestId } = render();
let results: AxeResults = null as any;
await act(async () => {
diff --git a/packages/alert-dialog/package.json b/packages/alert-dialog/package.json
index 3fb0299bf..fb3ec2ffc 100644
--- a/packages/alert-dialog/package.json
+++ b/packages/alert-dialog/package.json
@@ -1,25 +1,28 @@
{
"name": "@reach/alert-dialog",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React Alert Dialog.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/alert-dialog"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/dialog": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/dialog": "0.15.0",
+ "@reach/utils": "0.15.0",
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/alert-dialog/src/index.tsx b/packages/alert-dialog/src/index.tsx
index 37620a1b8..210da9034 100644
--- a/packages/alert-dialog/src/index.tsx
+++ b/packages/alert-dialog/src/index.tsx
@@ -33,16 +33,14 @@
import * as React from "react";
import { DialogOverlay, DialogContent } from "@reach/dialog";
import { useId } from "@reach/auto-id";
-import {
- createNamedContext,
- forwardRefWithAs,
- getOwnerDocument,
- makeId,
- useForkedRef,
-} from "@reach/utils";
+import { getOwnerDocument } from "@reach/utils/owner-document";
+import { createNamedContext } from "@reach/utils/context";
+import { makeId } from "@reach/utils/make-id";
+import { useComposedRefs } from "@reach/utils/compose-refs";
import invariant from "invariant";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { DialogProps, DialogContentProps } from "@reach/dialog";
let AlertDialogContext = createNamedContext(
@@ -63,33 +61,34 @@ let AlertDialogContext = createNamedContext(
*
* @see Docs https://reach.tech/alert-dialog#alertdialogoverlay
*/
-const AlertDialogOverlay = forwardRefWithAs(
- function AlertDialogOverlay({ leastDestructiveRef, ...props }, forwardedRef) {
- let ownRef = React.useRef(null);
- let ref = useForkedRef(forwardedRef, ownRef);
- let id = useId(props.id);
- let labelId = id ? makeId("alert-dialog", id) : undefined;
- let descriptionId = id ? makeId("alert-dialog-description", id) : undefined;
+const AlertDialogOverlay = React.forwardRef(function AlertDialogOverlay(
+ { leastDestructiveRef, ...props },
+ forwardedRef
+) {
+ let ownRef = React.useRef(null);
+ let ref = useComposedRefs(forwardedRef, ownRef);
+ let id = useId(props.id);
+ let labelId = id ? makeId("alert-dialog", id) : undefined;
+ let descriptionId = id ? makeId("alert-dialog-description", id) : undefined;
- return (
-
-
-
- );
- }
-);
+ return (
+
+
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", AlertDialogProps>;
if (__DEV__) {
AlertDialogOverlay.displayName = "AlertDialogOverlay";
@@ -117,65 +116,66 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/alert-dialog#alertdialogcontent
*/
-const AlertDialogContent = forwardRefWithAs(
- function AlertDialogContent({ children, ...props }, forwardedRef) {
- let {
- descriptionId,
- labelId,
- leastDestructiveRef,
- overlayRef,
- } = React.useContext(AlertDialogContext);
- React.useEffect(() => {
- let ownerDocument = getOwnerDocument(overlayRef.current)!;
- if (labelId) {
- invariant(
- ownerDocument.getElementById(labelId),
- `@reach/alert-dialog: You must render a \`\`
- inside an \`\`.`
- );
- }
+const AlertDialogContent = React.forwardRef(function AlertDialogContent(
+ { children, ...props },
+ forwardedRef
+) {
+ let {
+ descriptionId,
+ labelId,
+ leastDestructiveRef,
+ overlayRef,
+ } = React.useContext(AlertDialogContext);
+ React.useEffect(() => {
+ let ownerDocument = getOwnerDocument(overlayRef.current)!;
+ if (labelId) {
invariant(
- leastDestructiveRef,
- `@reach/alert-dialog: You must provide a \`leastDestructiveRef\` to
+ ownerDocument.getElementById(labelId),
+ `@reach/alert-dialog: You must render a \`\`
+ inside an \`\`.`
+ );
+ }
+ invariant(
+ leastDestructiveRef,
+ `@reach/alert-dialog: You must provide a \`leastDestructiveRef\` to
\`\` or \`\`. Please see
https://ui.reach.tech/alert-dialog/#alertdialogoverlay-leastdestructiveref`
- );
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [labelId, leastDestructiveRef]);
- return (
-
- {children}
-
);
- }
-);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [labelId, leastDestructiveRef]);
+ return (
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", AlertDialogContentProps>;
/**
* @see Docs https://reach.tech/alert-dialog#alertdialogcontent-props
*/
-type AlertDialogContentProps = {
+interface AlertDialogContentProps extends DialogContentProps {
/**
* Accepts any renderable content but should generally be restricted to
* `AlertDialogLabel`, `AlertDialogDescription` and action buttons, other
@@ -184,7 +184,7 @@ type AlertDialogContentProps = {
* @see Docs https://reach.tech/alert-dialog#alertdialogcontent-children
*/
children: React.ReactNode;
-} & DialogContentProps;
+}
if (__DEV__) {
AlertDialogContent.displayName = "AlertDialogContent";
@@ -206,25 +206,26 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/alert-dialog#alertdialoglabel
*/
-const AlertDialogLabel = forwardRefWithAs(
- function ({ as: Comp = "div", ...props }, forwardedRef) {
- const { labelId } = React.useContext(AlertDialogContext);
- return (
-
- );
- }
-);
+const AlertDialogLabel = React.forwardRef(function (
+ { as: Comp = "div", ...props },
+ forwardedRef
+) {
+ const { labelId } = React.useContext(AlertDialogContext);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", AlertDialogLabelProps>;
if (__DEV__) {
AlertDialogLabel.displayName = "AlertDialogLabel";
}
-type AlertDialogLabelProps = {};
+interface AlertDialogLabelProps {}
////////////////////////////////////////////////////////////////////////////////
@@ -238,10 +239,7 @@ type AlertDialogLabelProps = {};
* @see Docs https://reach.tech/alert-dialog#alertdialogdescription
* @param props
*/
-const AlertDialogDescription = forwardRefWithAs<
- AlertDialogDescriptionProps,
- "div"
->(function AlertDialogDescription(
+const AlertDialogDescription = React.forwardRef(function AlertDialogDescription(
{ as: Comp = "div", ...props },
forwardedRef
) {
@@ -254,13 +252,13 @@ const AlertDialogDescription = forwardRefWithAs<
data-reach-alert-dialog-description
/>
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", AlertDialogDescriptionProps>;
if (__DEV__) {
AlertDialogDescription.displayName = "AlertDialogDescription";
}
-type AlertDialogDescriptionProps = {};
+interface AlertDialogDescriptionProps {}
////////////////////////////////////////////////////////////////////////////////
@@ -272,23 +270,21 @@ type AlertDialogDescriptionProps = {};
* @see Docs https://reach.tech/alert-dialog#alertdialog
* @param props
*/
-const AlertDialog = forwardRefWithAs(
- function AlertDialog(
- { id, isOpen, onDismiss, leastDestructiveRef, ...props },
- forwardedRef
- ) {
- return (
-
-
-
- );
- }
-);
+const AlertDialog = React.forwardRef(function AlertDialog(
+ { id, isOpen, onDismiss, leastDestructiveRef, ...props },
+ forwardedRef
+) {
+ return (
+
+
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", AlertDialogProps>;
/**
* @see Docs https://reach.tech/alert-dialog#alertdialog-props
*/
-type AlertDialogProps = {
+interface AlertDialogProps extends DialogProps {
id?: string;
/**
* Controls whether the dialog is open or not.
@@ -319,7 +315,7 @@ type AlertDialogProps = {
* @see Docs: https://reach.tech/alert-dialog#alertdialog-children
*/
children: React.ReactNode;
-} & DialogProps;
+}
if (__DEV__) {
AlertDialog.displayName = "AlertDialog";
diff --git a/packages/alert/__tests__/alert.test.tsx b/packages/alert/__tests__/alert.test.tsx
index 475c68273..d371d1ffb 100644
--- a/packages/alert/__tests__/alert.test.tsx
+++ b/packages/alert/__tests__/alert.test.tsx
@@ -2,15 +2,15 @@ import * as React from "react";
import { render, act, fireEvent } from "$test/utils";
import { AxeResults } from "$test/types";
import { axe } from "jest-axe";
-import Alert from "@reach/alert";
-import { usePrevious } from "@reach/utils";
-import VisuallyHidden from "@reach/visually-hidden";
+import { Alert } from "@reach/alert";
+import { VisuallyHidden } from "@reach/visually-hidden";
+import { usePrevious } from "@reach/utils/use-previous";
const MESSAGE_TIMEOUT = 5000;
describe("", () => {
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
let { container, getByTestId } = render();
let results: AxeResults = null as any;
await act(async () => {
diff --git a/packages/alert/examples/basic-ts.example.tsx b/packages/alert/examples/basic-ts.example.tsx
index f0eb33f30..1b6f0203e 100644
--- a/packages/alert/examples/basic-ts.example.tsx
+++ b/packages/alert/examples/basic-ts.example.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
-import Alert from "@reach/alert";
-import { usePrevious } from "@reach/utils";
-import VisuallyHidden from "@reach/visually-hidden";
+import { Alert } from "@reach/alert";
+import { VisuallyHidden } from "@reach/visually-hidden";
+import { usePrevious } from "@reach/utils/use-previous";
const MESSAGE_TIMEOUT = 5000;
diff --git a/packages/alert/examples/basic.example.js b/packages/alert/examples/basic.example.js
index 73338a136..5693c2654 100644
--- a/packages/alert/examples/basic.example.js
+++ b/packages/alert/examples/basic.example.js
@@ -1,7 +1,7 @@
import * as React from "react";
-import Alert from "@reach/alert";
-import { usePrevious } from "@reach/utils";
-import VisuallyHidden from "@reach/visually-hidden";
+import { Alert } from "@reach/alert";
+import { usePrevious } from "@reach/utils/use-previous";
+import { VisuallyHidden } from "@reach/visually-hidden";
import LoremIpsum from "./LoremIpsum.js";
let name = "Basic";
diff --git a/packages/alert/package.json b/packages/alert/package.json
index 06f48cdca..f96c9b642 100644
--- a/packages/alert/package.json
+++ b/packages/alert/package.json
@@ -1,23 +1,26 @@
{
"name": "@reach/alert",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Screen-reader-friendly alert messages.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/alert"
},
"dependencies": {
- "@reach/utils": "0.13.1",
- "@reach/visually-hidden": "0.13.1",
+ "@reach/utils": "0.15.0",
+ "@reach/visually-hidden": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/alert/src/index.tsx b/packages/alert/src/index.tsx
index 912f7d43d..331787998 100644
--- a/packages/alert/src/index.tsx
+++ b/packages/alert/src/index.tsx
@@ -25,14 +25,13 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { VisuallyHidden } from "@reach/visually-hidden";
-import {
- forwardRefWithAs,
- getOwnerDocument,
- usePrevious,
- useForkedRef,
-} from "@reach/utils";
+import { usePrevious } from "@reach/utils/use-previous";
+import { getOwnerDocument } from "@reach/utils/owner-document";
+import { useComposedRefs } from "@reach/utils/compose-refs";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
+
/*
* Singleton state is fine because you don't server render
* an alert (SRs don't read them on first load anyway)
@@ -65,12 +64,12 @@ let renderTimer: number | null;
*
* @see Docs https://reach.tech/alert
*/
-const Alert = forwardRefWithAs(function Alert(
+const Alert = React.forwardRef(function Alert(
{ as: Comp = "div", children, type: regionType = "polite", ...props },
forwardedRef
) {
const ownRef = React.useRef(null);
- const ref = useForkedRef(forwardedRef, ownRef);
+ const ref = useComposedRefs(forwardedRef, ownRef);
const child = React.useMemo(
() => (
@@ -83,12 +82,12 @@ const Alert = forwardRefWithAs(function Alert(
useMirrorEffects(regionType, child, ownRef);
return child;
-});
+}) as Polymorphic.ForwardRefComponent<"div", AlertProps>;
/**
* @see Docs https://reach.tech/alert#alert-props
*/
-type AlertProps = {
+interface AlertProps {
/**
* Controls whether the assistive technology should read immediately
* ("assertive") or wait until the user is idle ("polite").
@@ -97,7 +96,7 @@ type AlertProps = {
*/
type?: "assertive" | "polite";
children: React.ReactNode;
-};
+}
if (__DEV__) {
Alert.displayName = "Alert";
diff --git a/packages/auto-id/package.json b/packages/auto-id/package.json
index d44a5f816..0dc9e9087 100644
--- a/packages/auto-id/package.json
+++ b/packages/auto-id/package.json
@@ -1,21 +1,24 @@
{
"name": "@reach/auto-id",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Autogenerate IDs to facilitate WAI-ARIA and server rendering.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/auto-id"
},
"dependencies": {
- "@reach/utils": "0.13.1",
- "tslib": "^2.0.0"
+ "@reach/utils": "0.15.0",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/auto-id/src/index.tsx b/packages/auto-id/src/index.tsx
index b0c4df8c5..6386015eb 100644
--- a/packages/auto-id/src/index.tsx
+++ b/packages/auto-id/src/index.tsx
@@ -55,7 +55,7 @@
*/
import * as React from "react";
-import { useIsomorphicLayoutEffect } from "@reach/utils";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
let serverHandoffComplete = false;
let id = 0;
@@ -84,7 +84,7 @@ function useId(idFromProps?: string | null) {
const [id, setId] = React.useState(initialId);
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
if (id === null) {
/*
* Patch the ID after render. We do this in `useLayoutEffect` to avoid any
diff --git a/packages/checkbox/__tests__/checkbox.test.tsx b/packages/checkbox/__tests__/checkbox.test.tsx
index d608404aa..5d10e0e18 100644
--- a/packages/checkbox/__tests__/checkbox.test.tsx
+++ b/packages/checkbox/__tests__/checkbox.test.tsx
@@ -9,12 +9,12 @@ import {
import { render, fireEvent } from "$test/utils";
describe("", () => {
- it("should not have basic a11y issues after render", async () => {
+ it("Should not have ARIA violations after render", async () => {
let { container } = render();
await expect(container).toHaveNoAxeViolations();
});
- it("should not have basic a11y issues after initial click", async () => {
+ it("Should not have ARIA violations after initial click", async () => {
let { container, getByTestId } = render();
fireEvent.click(getByTestId("checkbox"));
await expect(container).toHaveNoAxeViolations();
@@ -24,18 +24,18 @@ describe("", () => {
});
describe("", () => {
- it("should not have basic a11y issues after render", async () => {
+ it("Should not have ARIA violations after render", async () => {
let { container } = render();
await expect(container).toHaveNoAxeViolations();
});
- it("should not have basic a11y issues after initial click (1)", async () => {
+ it("Should not have ARIA violations after initial click (1)", async () => {
let { container, getByTestId } = render();
fireEvent.click(getByTestId("checkbox-1"));
await expect(container).toHaveNoAxeViolations();
});
- it("should not have basic a11y issues after initial click (2)", async () => {
+ it("Should not have ARIA violations after initial click (2)", async () => {
let { container, getByTestId } = render();
fireEvent.click(getByTestId("checkbox-2"));
await expect(container).toHaveNoAxeViolations();
diff --git a/packages/checkbox/package.json b/packages/checkbox/package.json
index a92fa29a4..2e3467195 100644
--- a/packages/checkbox/package.json
+++ b/packages/checkbox/package.json
@@ -1,25 +1,29 @@
{
"name": "@reach/checkbox",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible components to build custom, tri-state checkboxes in React.",
"author": "React Training ",
"license": "MIT",
"source": "",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/checkbox"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/machine": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/machine": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/checkbox/src/custom.tsx b/packages/checkbox/src/custom.tsx
index 87b1bf651..b925ba6d7 100644
--- a/packages/checkbox/src/custom.tsx
+++ b/packages/checkbox/src/custom.tsx
@@ -32,14 +32,11 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
import * as React from "react";
-import {
- createNamedContext,
- forwardRefWithAs,
- isFunction,
- useCheckStyles,
- useForkedRef,
- wrapEvent,
-} from "@reach/utils";
+import { createNamedContext } from "@reach/utils/context";
+import { isFunction } from "@reach/utils/type-check";
+import { useCheckStyles } from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import {
internal_checkedPropToStateValue as checkedPropToStateValue,
internal_useControlledSwitchWarning as useControlledSwitchWarning,
@@ -47,6 +44,7 @@ import {
} from "./mixed";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { MixedOrBool, UseMixedCheckboxProps } from "./mixed";
////////////////////////////////////////////////////////////////////////////////
@@ -68,77 +66,81 @@ function useCustomCheckboxContext() {
*
* @see Docs https://reach.tech/checkbox#customcheckboxcontainer
*/
-const CustomCheckboxContainer = forwardRefWithAs<
- CustomCheckboxContainerProps & { _componentName?: string },
- "span"
->(function CustomCheckboxContainer(
- {
- as: Comp = "span",
- checked: controlledChecked,
- children,
- defaultChecked,
- disabled,
- onClick,
- onChange,
- _componentName = "CustomCheckboxContainer",
- ...props
- },
- forwardedRef
-) {
- let inputRef: CustomCheckboxInputRef = React.useRef(null);
- let [inputProps, stateData] = useMixedCheckbox(inputRef, {
- defaultChecked,
- checked: controlledChecked,
- disabled,
- onChange,
- });
- let [focused, setFocused] = React.useState(false);
-
- function handleClick() {
- // Wait a frame so the input event is triggered, then focus the input
- window.requestAnimationFrame(() => {
- inputRef.current && inputRef.current.focus();
- });
- }
+const CustomCheckboxContainer = React.forwardRef(
+ function CustomCheckboxContainer(
+ {
+ as: Comp = "span",
+ checked: controlledChecked,
+ children,
+ defaultChecked,
+ disabled,
+ onClick,
+ onChange,
+ // @ts-expect-error
+ __componentName = "CustomCheckboxContainer",
+ ...props
+ },
+ forwardedRef
+ ) {
+ let inputRef: CustomCheckboxInputRef = React.useRef(null);
+ let [inputProps, stateData] = useMixedCheckbox(
+ inputRef,
+ {
+ defaultChecked,
+ checked: controlledChecked,
+ disabled,
+ onChange,
+ },
+ __componentName
+ );
+ let [focused, setFocused] = React.useState(false);
- let context: CustomCheckboxContextValue = {
- defaultChecked,
- disabled,
- focused,
- inputProps,
- inputRef,
- setFocused,
- };
+ function handleClick() {
+ // Wait a frame so the input event is triggered, then focus the input
+ window.requestAnimationFrame(() => {
+ inputRef.current && inputRef.current.focus();
+ });
+ }
- useControlledSwitchWarning(controlledChecked, "checked", _componentName);
- useCheckStyles("checkbox");
+ let context: CustomCheckboxContextValue = {
+ defaultChecked,
+ disabled,
+ focused,
+ inputProps,
+ inputRef,
+ setFocused,
+ };
- return (
-
-
- {isFunction(children)
- ? children({
- checked: inputProps["aria-checked"],
- inputRef,
- focused,
- })
- : children}
-
-
- );
-});
+ useControlledSwitchWarning(controlledChecked, "checked", __componentName);
+ useCheckStyles("checkbox");
+
+ return (
+
+
+ {isFunction(children)
+ ? children({
+ checked: inputProps["aria-checked"],
+ inputRef,
+ focused,
+ })
+ : children}
+
+
+ );
+ }
+) as Polymorphic.ForwardRefComponent<"span", CustomCheckboxContainerProps>;
/**
* @see Docs https://reach.tech/checkbox#custom-checkboxcontainer-props
*/
-type CustomCheckboxContainerProps = {
+interface CustomCheckboxContainerProps {
/**
* Whether or not the checkbox is checked or in a `mixed` (indeterminate)
* state.
@@ -191,7 +193,7 @@ type CustomCheckboxContainerProps = {
*
*/
onChange?(event: React.ChangeEvent): void;
-};
+}
if (__DEV__) {
CustomCheckboxContainer.displayName = "CustomCheckboxContainer";
@@ -217,57 +219,68 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/checkbox#customcheckboxinput
*/
-const CustomCheckboxInput = forwardRefWithAs(
- function CustomCheckboxInput(
- { as: Comp = "input", onBlur, onFocus, ...props },
- forwardedRef
- ) {
- let {
- focused,
- inputProps,
- inputRef,
- setFocused,
- } = useCustomCheckboxContext();
+const CustomCheckboxInput = React.forwardRef(function CustomCheckboxInput(
+ { as: Comp = "input", onBlur, onFocus, ...props },
+ forwardedRef
+) {
+ let {
+ focused,
+ inputProps,
+ inputRef,
+ setFocused,
+ } = useCustomCheckboxContext();
- let ref = useForkedRef(forwardedRef, inputRef);
- let mounted = React.useRef(true);
+ let ref = useComposedRefs(forwardedRef, inputRef);
+ let mounted = React.useRef(true);
- function handleBlur() {
- // window.requestAnimationFrame(() => send(CustomCheckboxEvents.Blur));
- window.requestAnimationFrame(() => {
- if (mounted.current) {
- setFocused(false);
- }
- });
- }
+ function handleBlur() {
+ // window.requestAnimationFrame(() => send(CustomCheckboxEvents.Blur));
+ window.requestAnimationFrame(() => {
+ if (mounted.current) {
+ setFocused(false);
+ }
+ });
+ }
- function handleFocus() {
- // window.requestAnimationFrame(() => send(CustomCheckboxEvents.Focus));
- window.requestAnimationFrame(() => {
- if (mounted.current) {
- setFocused(true);
- }
- });
- }
+ function handleFocus() {
+ // window.requestAnimationFrame(() => send(CustomCheckboxEvents.Focus));
+ window.requestAnimationFrame(() => {
+ if (mounted.current) {
+ setFocused(true);
+ }
+ });
+ }
- React.useEffect(() => () => void (mounted.current = false), []);
+ React.useEffect(() => () => void (mounted.current = false), []);
- return (
-
- );
- }
-);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"input", CustomCheckboxInputProps>;
-type CustomCheckboxInputProps = Pick;
+interface CustomCheckboxInputProps {
+ /**
+ * The `name` attribute passed to the checkbox form input.
+ *
+ * @see Docs https://reach.tech/checkbox#custom-checkbox-name
+ */
+ name?: React.ComponentProps<"input">["name"];
+ /**
+ * The `value` attribute passed to the checkbox form input.
+ *
+ * @see Docs https://reach.tech/checkbox#custom-checkbox-value
+ */
+ value?: React.ComponentProps<"input">["value"];
+}
if (__DEV__) {
CustomCheckboxInput.displayName = "CustomCheckboxInput";
@@ -283,33 +296,32 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/checkbox#customcheckbox-1
*/
-const CustomCheckbox = forwardRefWithAs(
- function CustomCheckbox(
- { children, id, name, value, ...props },
- forwardedRef
- ) {
- return (
-
-
- {children}
-
- );
- }
-);
+const CustomCheckbox = React.forwardRef(function CustomCheckbox(
+ { children, id, name, value, ...props },
+ forwardedRef
+) {
+ return (
+ // @ts-ignore
+
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"input", CustomCheckboxProps>;
/**
* @see Docs https://reach.tech/checkbox#custom-checkbox-props
*/
-type CustomCheckboxProps = {
+interface CustomCheckboxProps {
/**
* Whether or not the checkbox is checked or in a `mixed` (indeterminate)
* state.
@@ -360,7 +372,7 @@ type CustomCheckboxProps = {
* @see Docs https://reach.tech/checkbox#custom-checkbox-value
*/
value?: React.ComponentProps<"input">["value"];
-};
+}
if (__DEV__) {
CustomCheckbox.displayName = "CustomCheckbox";
diff --git a/packages/checkbox/src/mixed.tsx b/packages/checkbox/src/mixed.tsx
index 98a63a2cc..118fab199 100644
--- a/packages/checkbox/src/mixed.tsx
+++ b/packages/checkbox/src/mixed.tsx
@@ -32,16 +32,14 @@
*/
import * as React from "react";
-import {
- forwardRefWithAs,
- useForkedRef,
- useIsomorphicLayoutEffect,
- warning,
- wrapEvent,
-} from "@reach/utils";
+import { useIsomorphicLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import { assign, useCreateMachine, useMachine } from "@reach/machine";
+import warning from "tiny-warning";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { MachineEventWithRefs, StateMachine } from "@reach/machine";
// Used for development only, not recommended for production code!
@@ -204,15 +202,12 @@ const createMachineDefinition = (
*
* @see Docs https://reach.tech/checkbox#mixedcheckbox-1
*/
-const MixedCheckbox = forwardRefWithAs<
- MixedCheckboxProps & { _componentName?: string },
- "input"
->(function MixedCheckbox(
+const MixedCheckbox = React.forwardRef(function MixedCheckbox(
{ as: Comp = "input", checked, defaultChecked, disabled, onChange, ...props },
forwardedRef
) {
let ownRef: MixedCheckboxInputRef = React.useRef(null);
- let ref = useForkedRef(forwardedRef, ownRef);
+ let ref = useComposedRefs(forwardedRef, ownRef);
let [inputProps] = useMixedCheckbox(
ownRef,
{
@@ -229,16 +224,16 @@ const MixedCheckbox = forwardRefWithAs<
return (
);
-});
+}) as Polymorphic.ForwardRefComponent<"input", MixedCheckboxProps>;
-type MixedCheckboxProps = {
+interface MixedCheckboxProps {
/**
* Whether or not the checkbox is checked or in a `mixed` (indeterminate)
* state.
*/
checked?: MixedOrBool;
onChange?: React.ComponentProps<"input">["onChange"];
-};
+}
if (__DEV__) {
MixedCheckbox.displayName = "MixedCheckbox";
@@ -312,8 +307,8 @@ function useMixedCheckbox(
"aria-checked": stateValueToAriaChecked(current.value),
checked: stateValueToChecked(current.value),
disabled: !!disabled,
- onChange: wrapEvent(onChange, handleChange),
- onClick: wrapEvent(onClick, handleClick),
+ onChange: composeEventHandlers(onChange, handleChange),
+ onClick: composeEventHandlers(onClick, handleClick),
type: "checkbox",
};
@@ -352,13 +347,10 @@ function useMixedCheckbox(
}
}
- React.useEffect(() => {
- if (__DEV__ && !ref.current) {
- throw new Error(
- `A ref was not assigned to an input element in ${functionOrComponentName}.`
- );
- }
- }, [ref, functionOrComponentName]);
+ useRefDevWarning(
+ ref,
+ `A ref was not assigned to an input element in ${functionOrComponentName}.`
+ );
React.useEffect(() => {
if (isControlled) {
@@ -474,23 +466,6 @@ type MixedCheckboxEvent = MixedCheckboxEventBase &
}
);
-/**
- * State object for the checkbox state machine.
- */
-type MixedCheckboxState =
- | {
- value: MixedCheckboxStates.Checked;
- context: MixedCheckboxData;
- }
- | {
- value: MixedCheckboxStates.Unchecked;
- context: MixedCheckboxData;
- }
- | {
- value: MixedCheckboxStates.Mixed;
- context: MixedCheckboxData;
- };
-
/**
* DOM nodes for all of the refs used in the mixed checkbox state machine.
*/
@@ -515,3 +490,17 @@ export {
checkedPropToStateValue as internal_checkedPropToStateValue,
useControlledSwitchWarning as internal_useControlledSwitchWarning,
};
+
+function useRefDevWarning(ref: React.RefObject, message: string) {
+ if (__DEV__) {
+ /* eslint-disable react-hooks/rules-of-hooks */
+ let messageRef = React.useRef(message);
+ React.useEffect(() => {
+ messageRef.current = message;
+ }, [message]);
+ React.useEffect(() => {
+ warning(ref.current, messageRef.current);
+ }, [ref]);
+ /* eslint-enable react-hooks/rules-of-hooks */
+ }
+}
diff --git a/packages/combobox/__tests__/combobox.test.tsx b/packages/combobox/__tests__/combobox.test.tsx
index 77b6e25eb..3b3e3402f 100644
--- a/packages/combobox/__tests__/combobox.test.tsx
+++ b/packages/combobox/__tests__/combobox.test.tsx
@@ -100,7 +100,7 @@ describe("", () => {
});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
jest.useRealTimers();
let { container } = render();
let results: AxeResults = null as any;
diff --git a/packages/combobox/__tests__/utils.test.ts b/packages/combobox/__tests__/utils.test.ts
new file mode 100644
index 000000000..4371aa0c9
--- /dev/null
+++ b/packages/combobox/__tests__/utils.test.ts
@@ -0,0 +1,139 @@
+import { HighlightWords } from "../src/utils";
+import latinize from "latinize";
+
+describe(" : utils", () => {
+ describe("HighlightWords", () => {
+ // Forked from https://github.com/bvaughn/highlight-words-core
+ // Positions: 01234567890123456789012345678901234567
+ const TEXT = "This is a string with words to search.";
+
+ it("should handle empty `textToHighlight`", () => {
+ let result = HighlightWords.findAll({
+ searchWords: ["search"],
+ textToHighlight: "",
+ });
+ expect(result.length).toBe(0);
+ });
+
+ it("should handle undefined `textToHighlight`", () => {
+ let result = HighlightWords.findAll({
+ searchWords: ["search"],
+ });
+ expect(result.length).toBe(0);
+ });
+
+ it("should highlight all occurrences of a word, regardless of capitalization", () => {
+ let rawChunks = HighlightWords.findChunks({
+ searchWords: ["th"],
+ textToHighlight: TEXT,
+ });
+ expect(rawChunks).toEqual([
+ { start: 0, end: 2, highlight: false },
+ { start: 19, end: 21, highlight: false },
+ ]);
+ });
+
+ it("should highlight words that partially overlap", () => {
+ let combinedChunks = HighlightWords.combineChunks({
+ chunks: HighlightWords.findChunks({
+ searchWords: ["thi", "is"],
+ textToHighlight: TEXT,
+ }),
+ });
+ expect(combinedChunks).toEqual([
+ { start: 0, end: 4, highlight: false },
+ { start: 5, end: 7, highlight: false },
+ ]);
+ });
+
+ it("should combine into the minimum number of marked and unmarked chunks", () => {
+ let filledInChunks = HighlightWords.findAll({
+ searchWords: ["thi", "is"],
+ textToHighlight: TEXT,
+ });
+ expect(filledInChunks).toEqual([
+ { start: 0, end: 4, highlight: true },
+ { start: 4, end: 5, highlight: false },
+ { start: 5, end: 7, highlight: true },
+ { start: 7, end: 38, highlight: false },
+ ]);
+ });
+
+ it("should handle unclosed parentheses when autoEscape prop is truthy", () => {
+ let rawChunks = HighlightWords.findChunks({
+ autoEscape: true,
+ searchWords: ["text)"],
+ textToHighlight: "(This is text)",
+ });
+ expect(rawChunks).toEqual([{ start: 9, end: 14, highlight: false }]);
+ });
+
+ it("should match terms without accents against text with accents", () => {
+ let rawChunks = HighlightWords.findChunks({
+ sanitize: latinize,
+ searchWords: ["example"],
+ textToHighlight: "ỆᶍǍᶆṔƚÉ",
+ });
+ expect(rawChunks).toEqual([{ start: 0, end: 7, highlight: false }]);
+ });
+
+ it("should support case sensitive matches", () => {
+ let rawChunks = HighlightWords.findChunks({
+ caseSensitive: true,
+ searchWords: ["t"],
+ textToHighlight: TEXT,
+ });
+ expect(rawChunks).toEqual([
+ { start: 11, end: 12, highlight: false },
+ { start: 19, end: 20, highlight: false },
+ { start: 28, end: 29, highlight: false },
+ ]);
+
+ rawChunks = HighlightWords.findChunks({
+ caseSensitive: true,
+ searchWords: ["T"],
+ textToHighlight: TEXT,
+ });
+ expect(rawChunks).toEqual([{ start: 0, end: 1, highlight: false }]);
+ });
+
+ it("should handle zero-length matches correctly", () => {
+ let rawChunks = HighlightWords.findChunks({
+ caseSensitive: true,
+ searchWords: [".*"],
+ textToHighlight: TEXT,
+ });
+ expect(rawChunks).toEqual([{ start: 0, end: 38, highlight: false }]);
+
+ rawChunks = HighlightWords.findChunks({
+ caseSensitive: true,
+ searchWords: ["w?"],
+ textToHighlight: TEXT,
+ });
+ expect(rawChunks).toEqual([
+ { start: 17, end: 18, highlight: false },
+ { start: 22, end: 23, highlight: false },
+ ]);
+ });
+
+ it("should use custom findChunks", () => {
+ let filledInChunks = HighlightWords.findAll({
+ findChunks: (_: any) => [{ highlight: false, start: 1, end: 3 }],
+ searchWords: ["xxx"],
+ textToHighlight: TEXT,
+ });
+ expect(filledInChunks).toEqual([
+ { start: 0, end: 1, highlight: false },
+ { start: 1, end: 3, highlight: true },
+ { start: 3, end: 38, highlight: false },
+ ]);
+
+ filledInChunks = HighlightWords.findAll({
+ findChunks: () => [],
+ searchWords: ["This"],
+ textToHighlight: TEXT,
+ });
+ expect(filledInChunks).toEqual([{ start: 0, end: 38, highlight: false }]);
+ });
+ });
+});
diff --git a/packages/combobox/examples/token-input.example.js b/packages/combobox/examples/token-input.example.js
index 5156069ac..b868e136f 100644
--- a/packages/combobox/examples/token-input.example.js
+++ b/packages/combobox/examples/token-input.example.js
@@ -6,7 +6,7 @@ import {
ComboboxOption,
ComboboxPopover,
} from "@reach/combobox";
-import { wrapEvent } from "@reach/utils";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import { useCityMatch } from "./utils";
import "@reach/combobox/styles.css";
@@ -120,7 +120,10 @@ function ExampleTokenLabel({ onRemove, onKeyDown, ...props }) {
return (
-
+
);
}
@@ -144,7 +147,7 @@ function ExampleTokenbox({ onSelect, ...props }) {
const handleSelect = () => {};
return (
@@ -164,7 +167,10 @@ function ExampleTokenInput({ onKeyDown, ...props }) {
}
};
return (
-
+
);
}
diff --git a/packages/combobox/package.json b/packages/combobox/package.json
index 92aae3b50..ca73253c0 100644
--- a/packages/combobox/package.json
+++ b/packages/combobox/package.json
@@ -1,27 +1,30 @@
{
"name": "@reach/combobox",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React Combobox (Autocomplete).",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/combobox"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/descendants": "0.13.1",
- "@reach/popover": "0.13.1",
- "@reach/portal": "0.13.1",
- "@reach/utils": "0.13.1",
- "highlight-words-core": "1.2.2",
+ "@reach/auto-id": "0.15.0",
+ "@reach/descendants": "0.15.0",
+ "@reach/popover": "0.15.0",
+ "@reach/portal": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "latinize": "0.3.0",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/combobox/src/index.tsx b/packages/combobox/src/index.tsx
index 02bea2422..274b89154 100644
--- a/packages/combobox/src/index.tsx
+++ b/packages/combobox/src/index.tsx
@@ -20,20 +20,15 @@
import * as React from "react";
import PropTypes from "prop-types";
-import {
- createNamedContext,
- forwardRefWithAs,
- getOwnerDocument,
- isFunction,
- makeId,
- noop,
- useCheckStyles,
- useForkedRef,
- useIsomorphicLayoutEffect,
- useLazyRef,
- useUpdateEffect,
- wrapEvent,
-} from "@reach/utils";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { createNamedContext } from "@reach/utils/context";
+import { isFunction } from "@reach/utils/type-check";
+import { makeId } from "@reach/utils/make-id";
+import { noop } from "@reach/utils/noop";
+import { useCheckStyles } from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { useUpdateEffect } from "@reach/utils/use-update-effect";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import {
createDescendantContext,
DescendantProvider,
@@ -41,10 +36,11 @@ import {
useDescendants,
useDescendantsInit,
} from "@reach/descendants";
-import { findAll } from "highlight-words-core";
+import { HighlightWords } from "./utils";
import { useId } from "@reach/auto-id";
import { Popover, positionMatchWidth } from "@reach/popover";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { PopoverProps } from "@reach/popover";
import type { Descendant } from "@reach/descendants";
@@ -261,109 +257,107 @@ const OptionContext = createNamedContext(
*
* @see Docs https://reach.tech/combobox#combobox
*/
-export const Combobox = forwardRefWithAs(
- function Combobox(
- {
- onSelect,
- openOnFocus = false,
- children,
- as: Comp = "div",
- "aria-label": ariaLabel,
- "aria-labelledby": ariaLabelledby,
- ...props
- },
- forwardedRef
- ) {
- let [options, setOptions] = useDescendantsInit();
+export const Combobox = React.forwardRef(function Combobox(
+ {
+ onSelect,
+ openOnFocus = false,
+ children,
+ as: Comp = "div",
+ "aria-label": ariaLabel,
+ "aria-labelledby": ariaLabelledby,
+ ...props
+ },
+ forwardedRef
+) {
+ let [options, setOptions] = useDescendantsInit();
- // Need this to focus it
- const inputRef = React.useRef();
+ // Need this to focus it
+ const inputRef = React.useRef();
- const popoverRef = React.useRef();
+ const popoverRef = React.useRef();
- const buttonRef = React.useRef();
+ const buttonRef = React.useRef();
- // When we don't want cycle back to
- // the user's value while navigating (because it's always the user's value),
- // but we need to know this in useKeyDown which is far away from the prop
- // here, so we do something sneaky and write it to this ref on context so we
- // can use it anywhere else 😛. Another new trick for me and I'm excited
- // about this one too!
- const autocompletePropRef = React.useRef();
+ // When we don't want cycle back to
+ // the user's value while navigating (because it's always the user's value),
+ // but we need to know this in useKeyDown which is far away from the prop
+ // here, so we do something sneaky and write it to this ref on context so we
+ // can use it anywhere else 😛. Another new trick for me and I'm excited
+ // about this one too!
+ const autocompletePropRef = React.useRef(false);
- const persistSelectionRef = React.useRef();
+ const persistSelectionRef = React.useRef(false);
- const defaultData: StateData = {
- // The value the user has typed. We derive this also when the developer is
- // controlling the value of ComboboxInput.
- value: "",
- // the value the user has navigated to with the keyboard
- navigationValue: null,
- };
+ const defaultData: StateData = {
+ // The value the user has typed. We derive this also when the developer is
+ // controlling the value of ComboboxInput.
+ value: "",
+ // the value the user has navigated to with the keyboard
+ navigationValue: null,
+ };
- const [state, data, transition] = useReducerMachine(
- stateChart,
- reducer,
- defaultData
- );
-
- useFocusManagement(data.lastEventType, inputRef);
-
- const id = useId(props.id);
- const listboxId = id ? makeId("listbox", id) : "listbox";
-
- const context: InternalComboboxContextValue = {
- ariaLabel,
- ariaLabelledby,
- autocompletePropRef,
- buttonRef,
- comboboxId: id,
- data,
- inputRef,
- isExpanded: popoverIsExpanded(state),
- listboxId,
- onSelect: onSelect || noop,
- openOnFocus,
- persistSelectionRef,
- popoverRef,
- state,
- transition,
- };
+ const [state, data, transition] = useReducerMachine(
+ stateChart,
+ reducer,
+ defaultData
+ );
- useCheckStyles("combobox");
+ useFocusManagement(data.lastEventType, inputRef);
- return (
-
-
-
- {isFunction(children)
- ? children({
- id,
- isExpanded: popoverIsExpanded(state),
- navigationValue: data.navigationValue ?? null,
- state,
- })
- : children}
-
-
-
- );
- }
-);
+ const id = useId(props.id);
+ const listboxId = id ? makeId("listbox", id) : "listbox";
+
+ const context: InternalComboboxContextValue = {
+ ariaLabel,
+ ariaLabelledby,
+ autocompletePropRef,
+ buttonRef,
+ comboboxId: id,
+ data,
+ inputRef,
+ isExpanded: popoverIsExpanded(state),
+ listboxId,
+ onSelect: onSelect || noop,
+ openOnFocus,
+ persistSelectionRef,
+ popoverRef,
+ state,
+ transition,
+ };
+
+ useCheckStyles("combobox");
+
+ return (
+
+
+
+ {isFunction(children)
+ ? children({
+ id,
+ isExpanded: popoverIsExpanded(state),
+ navigationValue: data.navigationValue ?? null,
+ state,
+ })
+ : children}
+
+
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", ComboboxProps>;
/**
* @see Docs https://reach.tech/combobox#combobox-props
*/
-export type ComboboxProps = {
+export interface ComboboxProps {
/**
* @see Docs https://reach.tech/combobox#combobox-children
*/
@@ -393,7 +387,7 @@ export type ComboboxProps = {
* @see Docs https://reach.tech/combobox#accessibility
*/
"aria-labelledby"?: string;
-};
+}
if (__DEV__) {
Combobox.displayName = "Combobox";
@@ -413,162 +407,163 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/combobox#comboboxinput
*/
-export const ComboboxInput = forwardRefWithAs(
- function ComboboxInput(
- {
- as: Comp = "input",
- selectOnClick = false,
- autocomplete = true,
- onClick,
- onChange,
- onKeyDown,
- onBlur,
- onFocus,
- value: controlledValue,
- ...props
- },
- forwardedRef
- ) {
- // https://github.com/reach/reach-ui/issues/464
- let { current: initialControlledValue } = React.useRef(controlledValue);
- let controlledValueChangedRef = React.useRef(false);
- useUpdateEffect(() => {
- controlledValueChangedRef.current = true;
- }, [controlledValue]);
-
- let {
- data: { navigationValue, value, lastEventType },
- inputRef,
- state,
- transition,
- listboxId,
- autocompletePropRef,
- openOnFocus,
- isExpanded,
- ariaLabel,
- ariaLabelledby,
- } = React.useContext(ComboboxContext);
-
- let ref = useForkedRef(inputRef, forwardedRef);
-
- // Because we close the List on blur, we need to track if the blur is
- // caused by clicking inside the list, and if so, don't close the List.
- let selectOnClickRef = React.useRef(false);
-
- let handleKeyDown = useKeyDown();
-
- let handleBlur = useBlur();
-
- let isControlled = controlledValue != null;
-
- // Layout effect should be SSR-safe here because we don't actually do
- // anything with this ref that involves rendering until after we've
- // let the client hydrate in nested components.
- useIsomorphicLayoutEffect(() => {
- autocompletePropRef.current = autocomplete;
- }, [autocomplete, autocompletePropRef]);
-
- const handleValueChange = React.useCallback(
- (value: ComboboxValue) => {
- if (value.trim() === "") {
- transition(CLEAR);
- } else if (
- value === initialControlledValue &&
- !controlledValueChangedRef.current
- ) {
- transition(INITIAL_CHANGE, { value });
- } else {
- transition(CHANGE, { value });
- }
- },
- [initialControlledValue, transition]
- );
-
- React.useEffect(() => {
- // If they are controlling the value we still need to do our transitions,
- // so we have this derived state to emulate onChange of the input as we
- // receive new `value`s ...[*]
- if (
- isControlled &&
- controlledValue !== value &&
- // https://github.com/reach/reach-ui/issues/481
- (controlledValue!.trim() === "" ? (value || "").trim() !== "" : true)
+export const ComboboxInput = React.forwardRef(function ComboboxInput(
+ {
+ as: Comp = "input",
+ selectOnClick = false,
+ autocomplete = true,
+ onClick,
+ onChange,
+ onKeyDown,
+ onBlur,
+ onFocus,
+ value: controlledValue,
+ ...props
+ },
+ forwardedRef
+) {
+ // https://github.com/reach/reach-ui/issues/464
+ let { current: initialControlledValue } = React.useRef(controlledValue);
+ let controlledValueChangedRef = React.useRef(false);
+ useUpdateEffect(() => {
+ controlledValueChangedRef.current = true;
+ }, [controlledValue]);
+
+ let {
+ data: { navigationValue, value, lastEventType },
+ inputRef,
+ state,
+ transition,
+ listboxId,
+ autocompletePropRef,
+ openOnFocus,
+ isExpanded,
+ ariaLabel,
+ ariaLabelledby,
+ persistSelectionRef,
+ } = React.useContext(ComboboxContext);
+
+ let ref = useComposedRefs(inputRef, forwardedRef);
+
+ // Because we close the List on blur, we need to track if the blur is
+ // caused by clicking inside the list, and if so, don't close the List.
+ let selectOnClickRef = React.useRef(false);
+
+ let handleKeyDown = useKeyDown();
+
+ let handleBlur = useBlur();
+
+ let isControlled = controlledValue != null;
+
+ // Layout effect should be SSR-safe here because we don't actually do
+ // anything with this ref that involves rendering until after we've
+ // let the client hydrate in nested components.
+ useLayoutEffect(() => {
+ autocompletePropRef.current = autocomplete;
+ }, [autocomplete, autocompletePropRef]);
+
+ const handleValueChange = React.useCallback(
+ (value: ComboboxValue) => {
+ if (value.trim() === "") {
+ transition(CLEAR);
+ } else if (
+ value === initialControlledValue &&
+ !controlledValueChangedRef.current
) {
- handleValueChange(controlledValue!);
- }
- }, [controlledValue, handleValueChange, isControlled, value]);
-
- // [*]... and when controlled, we don't trigger handleValueChange as the
- // user types, instead the developer controls it with the normal input
- // onChange prop
- function handleChange(event: React.ChangeEvent) {
- const { value } = event.target;
- if (!isControlled) {
- handleValueChange(value);
+ transition(INITIAL_CHANGE, { value });
+ } else {
+ transition(CHANGE, { value });
}
+ },
+ [initialControlledValue, transition]
+ );
+
+ React.useEffect(() => {
+ // If they are controlling the value we still need to do our transitions,
+ // so we have this derived state to emulate onChange of the input as we
+ // receive new `value`s ...[*]
+ if (
+ isControlled &&
+ controlledValue !== value &&
+ // https://github.com/reach/reach-ui/issues/481
+ (controlledValue!.trim() === "" ? (value || "").trim() !== "" : true)
+ ) {
+ handleValueChange(controlledValue!);
+ }
+ }, [controlledValue, handleValueChange, isControlled, value]);
+
+ // [*]... and when controlled, we don't trigger handleValueChange as the
+ // user types, instead the developer controls it with the normal input
+ // onChange prop
+ function handleChange(event: React.ChangeEvent) {
+ const { value } = event.target;
+ if (!isControlled) {
+ handleValueChange(value);
}
+ }
- function handleFocus() {
- if (selectOnClick) {
- selectOnClickRef.current = true;
- }
+ function handleFocus() {
+ if (selectOnClick) {
+ selectOnClickRef.current = true;
+ }
- // If we select an option with click, useFocusManagement will focus the
- // input, in those cases we don't want to cause the menu to open back up,
- // so we guard behind these states.
- if (openOnFocus && lastEventType !== SELECT_WITH_CLICK) {
- transition(FOCUS);
- }
+ // If we select an option with click, useFocusManagement will focus the
+ // input, in those cases we don't want to cause the menu to open back up,
+ // so we guard behind these states.
+ if (openOnFocus && lastEventType !== SELECT_WITH_CLICK) {
+ transition(FOCUS, {
+ persistSelection: persistSelectionRef.current,
+ });
}
+ }
- function handleClick() {
- if (selectOnClickRef.current) {
- selectOnClickRef.current = false;
- inputRef.current.select();
- }
+ function handleClick() {
+ if (selectOnClickRef.current) {
+ selectOnClickRef.current = false;
+ inputRef.current?.select();
+ }
- if (openOnFocus) {
- transition("OPEN_WITH_BUTTON");
- }
+ if (openOnFocus) {
+ transition("OPEN_WITH_BUTTON");
}
+ }
- const inputValue =
- autocomplete && (state === NAVIGATING || state === INTERACTING)
- ? // When idle, we don't have a navigationValue on ArrowUp/Down
- navigationValue || controlledValue || value
- : controlledValue || value;
+ const inputValue =
+ autocomplete && (state === NAVIGATING || state === INTERACTING)
+ ? // When idle, we don't have a navigationValue on ArrowUp/Down
+ navigationValue || controlledValue || value
+ : controlledValue || value;
- return (
-
- );
- }
-);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"input", ComboboxInputProps>;
/**
* @see Docs https://reach.tech/combobox#comboboxinput-props
*/
-export type ComboboxInputProps = {
+export interface ComboboxInputProps {
/**
* If true, when the user clicks inside the text box the current value will
* be selected. Use this if the user is likely to delete all the text anyway
@@ -598,7 +593,7 @@ export type ComboboxInputProps = {
* @see Docs https://reach.tech/combobox#comboboxinput-value
*/
value?: ComboboxValue;
-};
+}
if (__DEV__) {
ComboboxInput.displayName = "ComboboxInput";
@@ -615,10 +610,7 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/combobox#comboboxpopover
*/
-export const ComboboxPopover = forwardRefWithAs<
- ComboboxPopoverProps & Partial,
- "div"
->(function ComboboxPopover(
+export const ComboboxPopover = React.forwardRef(function ComboboxPopover(
{
as: Comp = "div",
children,
@@ -628,20 +620,20 @@ export const ComboboxPopover = forwardRefWithAs<
position = positionMatchWidth,
...props
},
- forwardedRef: React.Ref
+ forwardedRef
) {
const { popoverRef, inputRef, isExpanded, state } = React.useContext(
ComboboxContext
);
- const ref = useForkedRef(popoverRef, forwardedRef);
+ const ref = useComposedRefs(popoverRef, forwardedRef);
const handleKeyDown = useKeyDown();
const handleBlur = useBlur();
const sharedProps = {
"data-reach-combobox-popover": "",
"data-state": getDataState(state),
- onKeyDown: wrapEvent(onKeyDown, handleKeyDown),
- onBlur: wrapEvent(onBlur, handleBlur),
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
+ onBlur: composeEventHandlers(onBlur, handleBlur),
// Instead of conditionally rendering the popover we use the `hidden` prop
// because we don't want to unmount on close (from escape or onSelect).
// However, the developer can conditionally render the ComboboxPopover if
@@ -664,7 +656,10 @@ export const ComboboxPopover = forwardRefWithAs<
) : (
);
-});
+}) as Polymorphic.ForwardRefComponent<
+ "div",
+ ComboboxPopoverProps & Partial
+>;
if (__DEV__) {
ComboboxPopover.displayName = "ComboboxPopover";
@@ -673,7 +668,7 @@ if (__DEV__) {
/**
* @see Docs https://reach.tech/combobox#comboboxpopover-props
*/
-export type ComboboxPopoverProps = {
+export interface ComboboxPopoverProps {
/**
* If you pass `` the popover will not
* render inside of a portal, but in the same order as the React tree. This
@@ -683,7 +678,7 @@ export type ComboboxPopoverProps = {
* @see Docs https://reach.tech/combobox#comboboxpopover-portal
*/
portal?: boolean;
-};
+}
////////////////////////////////////////////////////////////////////////////////
@@ -695,41 +690,37 @@ export type ComboboxPopoverProps = {
*
* @see Docs https://reach.tech/combobox#comboboxlist
*/
-export const ComboboxList = forwardRefWithAs(
- function ComboboxList(
- {
- // when true, and the list opens again, the option with a matching value
- // will be automatically highlighted.
- persistSelection = false,
- as: Comp = "ul",
- ...props
- },
- forwardedRef
- ) {
- const { persistSelectionRef, listboxId } = React.useContext(
- ComboboxContext
- );
-
- if (persistSelection) {
- persistSelectionRef.current = true;
- }
+export const ComboboxList = React.forwardRef(function ComboboxList(
+ {
+ // when true, and the list opens again, the option with a matching value
+ // will be automatically highlighted.
+ persistSelection = false,
+ as: Comp = "ul",
+ ...props
+ },
+ forwardedRef
+) {
+ const { persistSelectionRef, listboxId } = React.useContext(ComboboxContext);
- return (
-
- );
+ if (persistSelection) {
+ persistSelectionRef.current = true;
}
-);
+
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"ul", ComboboxListProps>;
/**
* @see Docs https://reach.tech/combobox#comboboxlist-props
*/
-export type ComboboxListProps = {
+export interface ComboboxListProps {
/**
* Defaults to false. When true and the list is opened, if an option's value
* matches the value in the input, it will automatically be highlighted and
@@ -744,7 +735,7 @@ export type ComboboxListProps = {
* @see Docs https://reach.tech/combobox#comboboxlist-persistselection
*/
persistSelection?: boolean;
-};
+}
if (__DEV__) {
ComboboxList.displayName = "ComboboxList";
@@ -759,70 +750,68 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/combobox#comboboxoption
*/
-export const ComboboxOption = forwardRefWithAs(
- function ComboboxOption(
- { as: Comp = "li", children, value, onClick, ...props },
- forwardedRef: React.Ref
- ) {
- const {
- onSelect,
- data: { navigationValue },
- transition,
- } = React.useContext(ComboboxContext);
-
- let ownRef = React.useRef(null);
- let ref = useForkedRef(forwardedRef, ownRef);
-
- let index = useDescendant(
- {
- element: ownRef.current!,
- value,
- },
- ComboboxDescendantContext
- );
+export const ComboboxOption = React.forwardRef(function ComboboxOption(
+ { as: Comp = "li", children, value, onClick, ...props },
+ forwardedRef
+) {
+ const {
+ onSelect,
+ data: { navigationValue },
+ transition,
+ } = React.useContext(ComboboxContext);
- const isActive = navigationValue === value;
+ let ownRef = React.useRef(null);
+ let ref = useComposedRefs(forwardedRef, ownRef);
- const handleClick = () => {
- onSelect && onSelect(value);
- transition(SELECT_WITH_CLICK, { value });
- };
+ let index = useDescendant(
+ {
+ element: ownRef.current!,
+ value,
+ },
+ ComboboxDescendantContext
+ );
- return (
-
-
- {children ? (
- isFunction(children) ? (
- children({ value, index })
- ) : (
- children
- )
+ const isActive = navigationValue === value;
+
+ const handleClick = () => {
+ onSelect && onSelect(value);
+ transition(SELECT_WITH_CLICK, { value });
+ };
+
+ return (
+
+
+ {children ? (
+ isFunction(children) ? (
+ children({ value, index })
) : (
-
- )}
-
-
- );
- }
-);
+ children
+ )
+ ) : (
+
+ )}
+
+
+ );
+}) as Polymorphic.ForwardRefComponent<"li", ComboboxOptionProps>;
/**
* @see Docs https://reach.tech/combobox#comboboxoption-props
*/
-export type ComboboxOptionProps = {
+export interface ComboboxOptionProps {
/**
* Optional. If omitted, the `value` will be used as the children like as:
* ``. But if you need
@@ -846,7 +835,7 @@ export type ComboboxOptionProps = {
* @see Docs https://reach.tech/combobox#comboboxoption-value
*/
value: string;
-};
+}
if (__DEV__) {
ComboboxOption.displayName = "ComboboxOption";
@@ -878,7 +867,7 @@ export function ComboboxOptionText() {
const results = React.useMemo(
() =>
- findAll({
+ HighlightWords.findAll({
searchWords: escapeRegexp(contextValue || "").split(/\s+/),
textToHighlight: value,
}),
@@ -915,46 +904,44 @@ if (__DEV__) {
/**
* ComboboxButton
*/
-export const ComboboxButton = forwardRefWithAs(
- function ComboboxButton(
- { as: Comp = "button", onClick, onKeyDown, ...props },
- forwardedRef
- ) {
- const {
- transition,
- state,
- buttonRef,
- listboxId,
- isExpanded,
- } = React.useContext(ComboboxContext);
- const ref = useForkedRef(buttonRef, forwardedRef);
+export const ComboboxButton = React.forwardRef(function ComboboxButton(
+ { as: Comp = "button", onClick, onKeyDown, ...props },
+ forwardedRef
+) {
+ const {
+ transition,
+ state,
+ buttonRef,
+ listboxId,
+ isExpanded,
+ } = React.useContext(ComboboxContext);
+ const ref = useComposedRefs(buttonRef, forwardedRef);
- const handleKeyDown = useKeyDown();
+ const handleKeyDown = useKeyDown();
- const handleClick = () => {
- if (state === IDLE) {
- transition(OPEN_WITH_BUTTON);
- } else {
- transition(CLOSE_WITH_BUTTON);
- }
- };
+ const handleClick = () => {
+ if (state === IDLE) {
+ transition(OPEN_WITH_BUTTON);
+ } else {
+ transition(CLOSE_WITH_BUTTON);
+ }
+ };
- return (
-
- );
- }
-);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"button", ComboboxButtonProps>;
-export type ComboboxButtonProps = {};
+export interface ComboboxButtonProps {}
if (__DEV__) {
ComboboxButton.displayName = "ComboboxButton";
@@ -972,20 +959,20 @@ if (__DEV__) {
*/
function useFocusManagement(
lastEventType: MachineEventType | undefined,
- inputRef: React.MutableRefObject
+ inputRef: React.MutableRefObject
) {
// useLayoutEffect so that the cursor goes to the end of the input instead
// of awkwardly at the beginning, unclear to me why 🤷♂️
//
// Should be safe to use here since we're just focusing an input.
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
if (
lastEventType === NAVIGATE ||
lastEventType === ESCAPE ||
lastEventType === SELECT_WITH_CLICK ||
lastEventType === OPEN_WITH_BUTTON
) {
- inputRef.current.focus();
+ inputRef.current?.focus();
}
}, [inputRef, lastEventType]);
}
@@ -1146,40 +1133,25 @@ function useBlur() {
inputRef,
buttonRef,
} = React.useContext(ComboboxContext);
- const rafIds = useLazyRef(() => new Set());
-
- React.useEffect(() => {
- return () => {
- // eslint-disable-next-line react-hooks/exhaustive-deps
- rafIds.current.forEach((id) => cancelAnimationFrame(id));
- };
- }, [rafIds]);
-
- return function handleBlur() {
- const ownerDocument = getOwnerDocument(popoverRef.current);
- if (!ownerDocument) {
- return;
- }
- let rafId = requestAnimationFrame(() => {
- // we on want to close only if focus propss outside the combobox
- if (
- ownerDocument.activeElement !== inputRef.current &&
- ownerDocument.activeElement !== buttonRef.current &&
- popoverRef.current
- ) {
- if (popoverRef.current.contains(ownerDocument.activeElement)) {
- // focus landed inside the combobox, keep it open
- if (state !== INTERACTING) {
- transition(INTERACT);
- }
- } else {
- // focus landed outside the combobox, close it.
- transition(BLUR);
+ return function handleBlur(event: React.FocusEvent) {
+ let popover = popoverRef.current;
+ let input = inputRef.current;
+ let button = buttonRef.current;
+ let activeElement = event.relatedTarget as Node;
+
+ // we on want to close only if focus propss outside the combobox
+ if (activeElement !== input && activeElement !== button && popover) {
+ if (popover.contains(activeElement)) {
+ // focus landed inside the combobox, keep it open
+ if (state !== INTERACTING) {
+ transition(INTERACT);
}
+ } else {
+ // focus landed outside the combobox, close it.
+ transition(BLUR);
}
- });
- rafIds.current.add(rafId);
+ }
};
}
@@ -1236,6 +1208,19 @@ function makeHash(str: string) {
return hash;
}
+// function getActiveElement(node: Element | null | undefined) {
+// let activeElement: Element | null = null;
+// try {
+// // If Element.getRootNode is supported, we'll retrieve either the root
+// // Document or shadow root depending on where the component is rendered.
+// // https://github.com/reach/reach-ui/issues/787
+// activeElement = (node?.getRootNode() as ShadowRoot | Document)
+// .activeElement;
+// } finally {
+// return activeElement || (node?.ownerDocument || document).activeElement;
+// }
+// }
+
function getDataState(state: State) {
return state.toLowerCase();
}
@@ -1251,7 +1236,8 @@ export function escapeRegexp(str: string) {
return String(str).replace(/([.*+?=^!:${}()|[\]/\\])/g, "\\$1");
}
-////////////////////////////////////////////////////////////////////////////////
+//////////////////////////
+//////////////////////////////////////////////////////
/**
* A hook that exposes data for a given `Combobox` component to its descendants.
@@ -1298,12 +1284,12 @@ export function useComboboxOptionContext(): ComboboxOptionContextValue {
////////////////////////////////////////////////////////////////////////////////
// Types
-export type ComboboxContextValue = {
+export interface ComboboxContextValue {
id: string | undefined;
isExpanded: boolean;
navigationValue: ComboboxValue | null;
state: State;
-};
+}
type ComboboxDescendant = Descendant & {
value: ComboboxValue;
@@ -1318,16 +1304,16 @@ interface InternalComboboxContextValue {
ariaLabel?: string;
ariaLabelledby?: string;
autocompletePropRef: React.MutableRefObject;
- buttonRef: React.MutableRefObject;
+ buttonRef: React.MutableRefObject;
comboboxId: string | undefined;
data: StateData;
- inputRef: React.MutableRefObject;
+ inputRef: React.MutableRefObject;
isExpanded: boolean;
listboxId: string;
onSelect(value?: ComboboxValue): any;
openOnFocus: boolean;
- persistSelectionRef: React.MutableRefObject;
- popoverRef: React.MutableRefObject;
+ persistSelectionRef: React.MutableRefObject;
+ popoverRef: React.MutableRefObject;
state: State;
transition: Transition;
}
@@ -1363,11 +1349,11 @@ interface StateChart {
};
}
-type StateData = {
+interface StateData {
lastEventType?: MachineEventType;
navigationValue?: ComboboxValue | null;
value?: ComboboxValue | null;
-};
+}
type MachineEvent =
| { type: "BLUR" }
@@ -1380,7 +1366,7 @@ type MachineEvent =
| { type: "INTERACT" }
| {
type: "NAVIGATE";
- persistSelection?: React.MutableRefObject;
+ persistSelection?: boolean;
value: ComboboxValue;
}
| { type: "OPEN_WITH_BUTTON" }
diff --git a/packages/combobox/src/utils.ts b/packages/combobox/src/utils.ts
new file mode 100644
index 000000000..b47bbc748
--- /dev/null
+++ b/packages/combobox/src/utils.ts
@@ -0,0 +1,181 @@
+// Forked from https://github.com/bvaughn/highlight-words-core
+
+/**
+ * Creates an array of chunk objects representing both higlightable and non
+ * highlightable pieces of text that match each search word.
+ *
+ * @return Array of "chunk" objects
+ */
+function findAll({
+ autoEscape,
+ caseSensitive = false,
+ findChunks = defaultFindChunks,
+ sanitize,
+ searchWords,
+ textToHighlight,
+}: {
+ autoEscape?: boolean;
+ caseSensitive?: boolean;
+ findChunks?: typeof defaultFindChunks;
+ sanitize?: typeof defaultSanitize;
+ searchWords: string[];
+ textToHighlight?: string | null;
+}): Chunk[] {
+ return fillInChunks({
+ chunksToHighlight: combineChunks({
+ chunks: findChunks({
+ autoEscape,
+ caseSensitive,
+ sanitize,
+ searchWords,
+ textToHighlight,
+ }),
+ }),
+ totalLength: textToHighlight ? textToHighlight.length : 0,
+ });
+}
+
+/**
+ * Takes an array of "chunk" objects and combines chunks that overlap into
+ * single chunks.
+ *
+ * @return Array of "chunk" objects
+ */
+function combineChunks({ chunks }: { chunks: Chunk[] }): Chunk[] {
+ return chunks
+ .sort((first, second) => first.start - second.start)
+ .reduce((processedChunks, nextChunk) => {
+ // First chunk just goes straight in the array...
+ if (processedChunks.length === 0) {
+ return [nextChunk];
+ } else {
+ // ... subsequent chunks get checked to see if they overlap...
+ const prevChunk = processedChunks.pop()!;
+ if (nextChunk.start <= prevChunk.end) {
+ // It may be the case that prevChunk completely surrounds nextChunk, so take the
+ // largest of the end indeces.
+ const endIndex = Math.max(prevChunk.end, nextChunk.end);
+ processedChunks.push({
+ highlight: false,
+ start: prevChunk.start,
+ end: endIndex,
+ });
+ } else {
+ processedChunks.push(prevChunk, nextChunk);
+ }
+ return processedChunks;
+ }
+ }, []);
+}
+
+/**
+ * Examine text for any matches. If we find matches, add them to the returned
+ * array as a "chunk" object.
+ *
+ * @return Array of "chunk" objects
+ */
+function defaultFindChunks({
+ autoEscape,
+ caseSensitive,
+ sanitize = defaultSanitize,
+ searchWords,
+ textToHighlight,
+}: {
+ autoEscape?: boolean;
+ caseSensitive?: boolean;
+ sanitize?: typeof defaultSanitize;
+ searchWords: string[];
+ textToHighlight?: string | null;
+}): Chunk[] {
+ textToHighlight = sanitize(textToHighlight || "");
+
+ return searchWords
+ .filter((searchWord) => searchWord) // Remove empty words
+ .reduce((chunks, searchWord) => {
+ searchWord = sanitize(searchWord);
+
+ if (autoEscape) {
+ searchWord = escapeRegExpFn(searchWord);
+ }
+
+ const regex = new RegExp(searchWord, caseSensitive ? "g" : "gi");
+
+ let match;
+ while ((match = regex.exec(textToHighlight || ""))) {
+ let start = match.index;
+ let end = regex.lastIndex;
+ // We do not return zero-length matches
+ if (end > start) {
+ chunks.push({ highlight: false, start, end });
+ }
+
+ // Prevent browsers like Firefox from getting stuck in an infinite loop
+ // See http://www.regexguru.com/2008/04/watch-out-for-zero-length-matches/
+ if (match.index === regex.lastIndex) {
+ regex.lastIndex++;
+ }
+ }
+
+ return chunks;
+ }, []);
+}
+
+/**
+ * Given a set of chunks to highlight, create an additional set of chunks
+ * to represent the bits of text between the highlighted text.
+ *
+ * @return Array of "chunk" objects
+ */
+function fillInChunks({
+ chunksToHighlight,
+ totalLength,
+}: {
+ chunksToHighlight: Chunk[];
+ totalLength: number;
+}): Chunk[] {
+ const allChunks: Chunk[] = [];
+
+ if (chunksToHighlight.length === 0) {
+ append(0, totalLength, false);
+ } else {
+ let lastIndex = 0;
+ chunksToHighlight.forEach((chunk) => {
+ append(lastIndex, chunk.start, false);
+ append(chunk.start, chunk.end, true);
+ lastIndex = chunk.end;
+ });
+ append(lastIndex, totalLength, false);
+ }
+ return allChunks;
+
+ function append(start: number, end: number, highlight: boolean) {
+ if (end - start > 0) {
+ allChunks.push({
+ start,
+ end,
+ highlight,
+ });
+ }
+ }
+}
+
+function defaultSanitize(string: string): string {
+ return string;
+}
+
+function escapeRegExpFn(string: string): string {
+ return string.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
+}
+
+export const HighlightWords = {
+ combineChunks,
+ fillInChunks,
+ findAll,
+ findChunks: defaultFindChunks,
+};
+
+export interface Chunk {
+ highlight: boolean;
+ start: number;
+ end: number;
+}
diff --git a/packages/component-component/package.json b/packages/component-component/package.json
index 5fb8b513b..1a2d5e0b3 100644
--- a/packages/component-component/package.json
+++ b/packages/component-component/package.json
@@ -1,9 +1,12 @@
{
"name": "@reach/component-component",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Declarative React Component Definitions",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
@@ -13,8 +16,8 @@
"prop-types": "^15.7.2"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.4.0 || 17.x",
diff --git a/packages/descendants/package.json b/packages/descendants/package.json
index 1ab69b672..70ce70e92 100644
--- a/packages/descendants/package.json
+++ b/packages/descendants/package.json
@@ -1,21 +1,24 @@
{
"name": "@reach/descendants",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "A descendant index solution for better accessibility support in compound components",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/descendants"
},
"dependencies": {
- "@reach/utils": "0.13.1",
- "tslib": "^2.0.0"
+ "@reach/utils": "0.15.0",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/descendants/src/index.tsx b/packages/descendants/src/index.tsx
index 09f73f633..2eb20b753 100644
--- a/packages/descendants/src/index.tsx
+++ b/packages/descendants/src/index.tsx
@@ -1,11 +1,9 @@
import * as React from "react";
-import {
- createNamedContext,
- noop,
- useForceUpdate,
- useIsomorphicLayoutEffect,
- usePrevious,
-} from "@reach/utils";
+import { useForceUpdate } from "@reach/utils/use-force-update";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { usePrevious } from "@reach/utils/use-previous";
+import { createNamedContext } from "@reach/utils/context";
+import { noop } from "@reach/utils/noop";
function createDescendantContext(
name: string,
@@ -76,7 +74,7 @@ function useDescendant(
});
// Prevent any flashing
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
if (!descendant.element) forceUpdate();
registerDescendant({
...descendant,
@@ -274,7 +272,6 @@ function useDescendantKeyDown<
rotate = true,
rtl = false,
} = options;
- let index = currentIndex ?? -1;
return function handleKeyDown(event: React.KeyboardEvent) {
if (
@@ -292,6 +289,8 @@ function useDescendantKeyDown<
return;
}
+ let index = currentIndex ?? -1;
+
// If we use a filter function, we need to re-index our descendants array
// so that filtered descendent elements aren't selected.
let selectableDescendants = filter
diff --git a/packages/dialog/__tests__/dialog.test.tsx b/packages/dialog/__tests__/dialog.test.tsx
index 07bd5895c..8739b8b7b 100644
--- a/packages/dialog/__tests__/dialog.test.tsx
+++ b/packages/dialog/__tests__/dialog.test.tsx
@@ -33,7 +33,7 @@ describe("", () => {
});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
clock.restore();
const { container } = render();
let results: AxeResults = null as any;
diff --git a/packages/dialog/package.json b/packages/dialog/package.json
index 174a8255f..380740f17 100644
--- a/packages/dialog/package.json
+++ b/packages/dialog/package.json
@@ -1,25 +1,28 @@
{
"name": "@reach/dialog",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React Modal Dialog.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/dialog"
},
"dependencies": {
- "@reach/portal": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/portal": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
"react-focus-lock": "^2.5.0",
"react-remove-scroll": "^2.4.1",
- "tslib": "^2.0.0"
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/dialog/src/index.tsx b/packages/dialog/src/index.tsx
index f6651008d..3ab8306d0 100644
--- a/packages/dialog/src/index.tsx
+++ b/packages/dialog/src/index.tsx
@@ -12,19 +12,18 @@
import * as React from "react";
import { Portal } from "@reach/portal";
-import {
- forwardRefWithAs,
- getOwnerDocument,
- isString,
- noop,
- useCheckStyles,
- useForkedRef,
- wrapEvent,
-} from "@reach/utils";
+import { getOwnerDocument } from "@reach/utils/owner-document";
+import { isString } from "@reach/utils/type-check";
+import { noop } from "@reach/utils/noop";
+import { useCheckStyles } from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import FocusLock from "react-focus-lock";
import { RemoveScroll } from "react-remove-scroll";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
+
const overlayPropTypes = {
allowPinchZoom: PropTypes.bool,
dangerouslyBypassFocusLock: PropTypes.bool,
@@ -46,36 +45,34 @@ const overlayPropTypes = {
*
* @see Docs https://reach.tech/dialog#dialogoverlay
*/
-const DialogOverlay = forwardRefWithAs(
- function DialogOverlay(
- { as: Comp = "div", isOpen = true, ...props },
- forwardedRef
- ) {
- useCheckStyles("dialog");
-
- // We want to ignore the immediate focus of a tooltip so it doesn't pop
- // up again when the menu closes, only pops up when focus returns again
- // to the tooltip (like native OS tooltips).
- React.useEffect(() => {
- if (isOpen) {
+const DialogOverlay = React.forwardRef(function DialogOverlay(
+ { as: Comp = "div", isOpen = true, ...props },
+ forwardedRef
+) {
+ useCheckStyles("dialog");
+
+ // We want to ignore the immediate focus of a tooltip so it doesn't pop
+ // up again when the menu closes, only pops up when focus returns again
+ // to the tooltip (like native OS tooltips).
+ React.useEffect(() => {
+ if (isOpen) {
+ // @ts-ignore
+ window.__REACH_DISABLE_TOOLTIPS = true;
+ } else {
+ window.requestAnimationFrame(() => {
+ // Wait a frame so that this doesn't fire before tooltip does
// @ts-ignore
- window.__REACH_DISABLE_TOOLTIPS = true;
- } else {
- window.requestAnimationFrame(() => {
- // Wait a frame so that this doesn't fire before tooltip does
- // @ts-ignore
- window.__REACH_DISABLE_TOOLTIPS = false;
- });
- }
- }, [isOpen]);
+ window.__REACH_DISABLE_TOOLTIPS = false;
+ });
+ }
+ }, [isOpen]);
- return isOpen ? (
-
-
-
- ) : null;
- }
-);
+ return isOpen ? (
+
+
+
+ ) : null;
+}) as Polymorphic.ForwardRefComponent<"div", DialogOverlayProps>;
if (__DEV__) {
DialogOverlay.displayName = "DialogOverlay";
@@ -85,7 +82,7 @@ if (__DEV__) {
};
}
-type DialogOverlayProps = DialogProps & {
+interface DialogOverlayProps extends DialogProps {
/**
* By default the dialog locks the focus inside it. Normally this is what you
* want. This prop is provided so that this feature can be disabled. This,
@@ -123,94 +120,92 @@ type DialogOverlayProps = DialogProps & {
* @see https://github.com/theKashey/react-remove-scroll
*/
dangerouslyBypassScrollLock?: boolean;
-};
+}
////////////////////////////////////////////////////////////////////////////////
/**
* DialogInner
*/
-const DialogInner = forwardRefWithAs(
- function DialogInner(
- {
- allowPinchZoom,
- as: Comp = "div",
- dangerouslyBypassFocusLock = false,
- dangerouslyBypassScrollLock = false,
- initialFocusRef,
- onClick,
- onDismiss = noop,
- onKeyDown,
- onMouseDown,
- unstable_lockFocusAcrossFrames = true,
- ...props
- },
- forwardedRef
- ) {
- const mouseDownTarget = React.useRef(null);
- const overlayNode = React.useRef(null);
- const ref = useForkedRef(overlayNode, forwardedRef);
-
- const activateFocusLock = React.useCallback(() => {
- if (initialFocusRef && initialFocusRef.current) {
- initialFocusRef.current.focus();
- }
- }, [initialFocusRef]);
+const DialogInner = React.forwardRef(function DialogInner(
+ {
+ allowPinchZoom,
+ as: Comp = "div",
+ dangerouslyBypassFocusLock = false,
+ dangerouslyBypassScrollLock = false,
+ initialFocusRef,
+ onClick,
+ onDismiss = noop,
+ onKeyDown,
+ onMouseDown,
+ unstable_lockFocusAcrossFrames = true,
+ ...props
+ },
+ forwardedRef
+) {
+ const mouseDownTarget = React.useRef(null);
+ const overlayNode = React.useRef(null);
+ const ref = useComposedRefs(overlayNode, forwardedRef);
- function handleClick(event: React.MouseEvent) {
- if (mouseDownTarget.current === event.target) {
- event.stopPropagation();
- onDismiss(event);
- }
+ const activateFocusLock = React.useCallback(() => {
+ if (initialFocusRef && initialFocusRef.current) {
+ initialFocusRef.current.focus();
}
+ }, [initialFocusRef]);
- function handleKeyDown(event: React.KeyboardEvent) {
- if (event.key === "Escape") {
- event.stopPropagation();
- onDismiss(event);
- }
+ function handleClick(event: React.MouseEvent) {
+ if (mouseDownTarget.current === event.target) {
+ event.stopPropagation();
+ onDismiss(event);
}
+ }
- function handleMouseDown(event: React.MouseEvent) {
- mouseDownTarget.current = event.target;
+ function handleKeyDown(event: React.KeyboardEvent) {
+ if (event.key === "Escape") {
+ event.stopPropagation();
+ onDismiss(event);
}
+ }
- React.useEffect(() => {
- return overlayNode.current
- ? createAriaHider(overlayNode.current)
- : void null;
- }, []);
-
- return (
-
-
-
-
-
- );
+ function handleMouseDown(event: React.MouseEvent) {
+ mouseDownTarget.current = event.target;
}
-);
+
+ React.useEffect(() => {
+ return overlayNode.current
+ ? createAriaHider(overlayNode.current)
+ : void null;
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", DialogOverlayProps>;
if (__DEV__) {
DialogOverlay.displayName = "DialogOverlay";
@@ -236,38 +231,36 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/dialog#dialogcontent
*/
-const DialogContent = forwardRefWithAs(
- function DialogContent(
- { as: Comp = "div", onClick, onKeyDown, ...props },
- forwardedRef
- ) {
- return (
- {
- event.stopPropagation();
- })}
- />
- );
- }
-);
+const DialogContent = React.forwardRef(function DialogContent(
+ { as: Comp = "div", onClick, onKeyDown, ...props },
+ forwardedRef
+) {
+ return (
+ {
+ event.stopPropagation();
+ })}
+ />
+ );
+}) as Polymorphic.ForwardRefComponent<"div", DialogContentProps>;
/**
* @see Docs https://reach.tech/dialog#dialogcontent-props
*/
-type DialogContentProps = {
+interface DialogContentProps {
/**
* Accepts any renderable content.
*
* @see Docs https://reach.tech/dialog#dialogcontent-children
*/
children?: React.ReactNode;
-};
+}
if (__DEV__) {
DialogContent.displayName = "DialogContent";
@@ -287,7 +280,7 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/dialog#dialog
*/
-const Dialog = forwardRefWithAs(function Dialog(
+const Dialog = React.forwardRef(function Dialog(
{
allowPinchZoom = false,
initialFocusRef,
@@ -307,12 +300,12 @@ const Dialog = forwardRefWithAs(function Dialog(
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", DialogProps>;
/**
* @see Docs https://reach.tech/dialog#dialog-props
*/
-type DialogProps = {
+interface DialogProps {
/**
* Handle zoom/pinch gestures on iOS devices when scroll locking is enabled.
* Defaults to `false`.
@@ -372,7 +365,7 @@ type DialogProps = {
* https://github.com/reach/reach-ui/issues/536
*/
unstable_lockFocusAcrossFrames?: boolean;
-};
+}
if (__DEV__) {
Dialog.displayName = "Dialog";
diff --git a/packages/disclosure/__tests__/disclosure.test.tsx b/packages/disclosure/__tests__/disclosure.test.tsx
index 136aa5b0c..4ef5839c3 100644
--- a/packages/disclosure/__tests__/disclosure.test.tsx
+++ b/packages/disclosure/__tests__/disclosure.test.tsx
@@ -83,7 +83,7 @@ describe("", () => {
});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
let { getByRole, container } = render(
Click Button
@@ -108,16 +108,6 @@ describe("", () => {
expect(getByText("Panel body")).toHaveAttribute("id", "panel--my-id");
});
- it("removes the panel from the navigation flow", () => {
- let { getByText } = render(
-
- Click Button
- Panel body
-
- );
- expect(getByText("Panel body")).toHaveAttribute("tabindex", "-1");
- });
-
it("sets the correct aria attributes when collapsed", () => {
let { getByText } = render(
diff --git a/packages/disclosure/package.json b/packages/disclosure/package.json
index f5b359db3..f592f0165 100644
--- a/packages/disclosure/package.json
+++ b/packages/disclosure/package.json
@@ -1,23 +1,27 @@
{
"name": "@reach/disclosure",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React disclosure component",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/disclosure"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/disclosure/src/index.tsx b/packages/disclosure/src/index.tsx
index 84cf1c222..96c2810ae 100644
--- a/packages/disclosure/src/index.tsx
+++ b/packages/disclosure/src/index.tsx
@@ -16,18 +16,17 @@
*/
import * as React from "react";
-import {
- createNamedContext,
- forwardRefWithAs,
- makeId,
- useForkedRef,
- useStableCallback,
- warning,
- wrapEvent,
-} from "@reach/utils";
+import { useStableCallback } from "@reach/utils/use-stable-callback";
+import { createNamedContext } from "@reach/utils/context";
+import { makeId } from "@reach/utils/make-id";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import { useId } from "@reach/auto-id";
+import warning from "tiny-warning";
import PropTypes from "prop-types";
+import type * as Polymorphic from "@reach/utils/polymorphic";
+
const DisclosureContext = createNamedContext(
"DisclosureContext",
{} as DisclosureContextValue
@@ -119,7 +118,7 @@ const Disclosure: React.FC = ({
);
};
-type DisclosureProps = {
+interface DisclosureProps {
/**
* `Disclosure` expects to receive accept `DisclosureButton` and
* `DisclosurePanel` components as children. It can also accept wrapper
@@ -162,7 +161,7 @@ type DisclosureProps = {
* @see Docs https://reach.tech/disclosure#disclosure-open
*/
open?: boolean;
-};
+}
if (__DEV__) {
Disclosure.displayName = "Disclosure";
@@ -183,58 +182,57 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/disclosure#disclosurebutton
*/
-const DisclosureButton = forwardRefWithAs(
- function DisclosureButton(
- {
- // The element that shows and hides the content has role `button`.
- // https://www.w3.org/TR/wai-aria-practices-1.2/#disclosure
- as: Comp = "button",
- children,
- onClick,
- onMouseDown,
- onPointerDown,
- ...props
- },
- forwardedRef
- ) {
- const { onSelect, open, panelId } = React.useContext(DisclosureContext);
- const ownRef = React.useRef(null);
-
- const ref = useForkedRef(forwardedRef, ownRef);
-
- function handleClick(event: React.MouseEvent) {
- event.preventDefault();
- ownRef.current && ownRef.current.focus();
- onSelect();
- }
-
- return (
-
- {children}
-
- );
+const DisclosureButton = React.forwardRef(function DisclosureButton(
+ {
+ // The element that shows and hides the content has role `button`.
+ // https://www.w3.org/TR/wai-aria-practices-1.2/#disclosure
+ as: Comp = "button",
+ children,
+ onClick,
+ onMouseDown,
+ onPointerDown,
+ ...props
+ },
+ forwardedRef
+) {
+ const { onSelect, open, panelId } = React.useContext(DisclosureContext);
+ const ownRef = React.useRef(null);
+
+ const ref = useComposedRefs(forwardedRef, ownRef);
+
+ function handleClick(event: React.MouseEvent) {
+ event.preventDefault();
+ ownRef.current && ownRef.current.focus();
+ onSelect();
}
-);
+
+ return (
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"button", DisclosureButtonProps>;
+
/**
* @see Docs https://reach.tech/disclosure#disclosurebutton-props
*/
-type DisclosureButtonProps = {
+interface DisclosureButtonProps {
/**
* Typically a text string that serves as a label for the disclosure button,
* though nested DOM nodes can be passed as well so long as they are valid
@@ -244,7 +242,7 @@ type DisclosureButtonProps = {
* @see Docs https://reach.tech/disclosure#disclosurebutton-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
DisclosureButton.displayName = "DisclosureButton";
@@ -264,28 +262,25 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/disclosure#disclosurepanel
*/
-const DisclosurePanel = forwardRefWithAs(
- function DisclosurePanel(
- { as: Comp = "div", children, ...props },
- forwardedRef
- ) {
- const { panelId, open } = React.useContext(DisclosureContext);
-
- return (
-
- {children}
-
- );
- }
-);
+const DisclosurePanel = React.forwardRef(function DisclosurePanel(
+ { as: Comp = "div", children, ...props },
+ forwardedRef
+) {
+ const { panelId, open } = React.useContext(DisclosureContext);
+
+ return (
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", DisclosurePanelProps>;
if (__DEV__) {
DisclosurePanel.displayName = "DisclosurePanel";
@@ -295,14 +290,14 @@ if (__DEV__) {
/**
* @see Docs https://reach.tech/disclosure#disclosurepanel-props
*/
-type DisclosurePanelProps = {
+interface DisclosurePanelProps {
/**
* Inner collapsible content for the disclosure item.
*
* @see Docs https://reach.tech/disclosure#disclosurepanel-children
*/
children: React.ReactNode;
-};
+}
////////////////////////////////////////////////////////////////////////////////
diff --git a/packages/listbox/__tests__/listbox.test.tsx b/packages/listbox/__tests__/listbox.test.tsx
index 081f2416a..386e69c11 100644
--- a/packages/listbox/__tests__/listbox.test.tsx
+++ b/packages/listbox/__tests__/listbox.test.tsx
@@ -75,7 +75,7 @@ describe("", () => {
});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
let { container } = render();
let results: AxeResults = null as any;
await act(async () => {
diff --git a/packages/listbox/examples/basic-strict-mode.example.tsx b/packages/listbox/examples/basic-strict-mode.example.tsx
index bedc97a72..0216bd32e 100644
--- a/packages/listbox/examples/basic-strict-mode.example.tsx
+++ b/packages/listbox/examples/basic-strict-mode.example.tsx
@@ -7,8 +7,6 @@ import "@reach/listbox/styles.css";
let name = "Basic (Strict Mode)";
-type Option = { value: string; label: string };
-
function Example() {
return (
diff --git a/packages/listbox/examples/common.tsx b/packages/listbox/examples/common.tsx
index de7ad37dd..48b3584a4 100644
--- a/packages/listbox/examples/common.tsx
+++ b/packages/listbox/examples/common.tsx
@@ -7,7 +7,9 @@ export const Tag: React.FC> = (
let ref = React.useRef(null);
let setInnerTextRef = React.useCallback((node: HTMLSpanElement) => {
ref.current = node;
- setInnerText(node.innerText);
+ if (node) {
+ setInnerText(node.innerText);
+ }
}, []);
return (
",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/listbox"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/descendants": "0.13.1",
- "@reach/machine": "0.13.1",
- "@reach/popover": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/descendants": "0.15.0",
+ "@reach/machine": "0.15.0",
+ "@reach/popover": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/listbox/src/index.tsx b/packages/listbox/src/index.tsx
index c286f4b7e..ed8bbdfee 100644
--- a/packages/listbox/src/index.tsx
+++ b/packages/listbox/src/index.tsx
@@ -36,22 +36,18 @@ import {
useDescendants,
useDescendantsInit,
} from "@reach/descendants";
+import { isRightClick } from "@reach/utils/is-right-click";
+import { useStableCallback } from "@reach/utils/use-stable-callback";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { createNamedContext } from "@reach/utils/context";
+import { isBoolean, isFunction, isString } from "@reach/utils/type-check";
+import { makeId } from "@reach/utils/make-id";
import {
- createNamedContext,
- forwardRefWithAs,
- isBoolean,
- isFunction,
- isRightClick,
- isString,
- makeId,
- memoWithAs,
useCheckStyles,
useControlledSwitchWarning,
- useForkedRef,
- useIsomorphicLayoutEffect as useLayoutEffect,
- useStableCallback,
- wrapEvent,
-} from "@reach/utils";
+} from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import { useCreateMachine, useMachine } from "@reach/machine";
import {
createMachineDefinition,
@@ -59,8 +55,9 @@ import {
ListboxStates,
} from "./machine";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { Descendant } from "@reach/descendants";
-import type { DistributiveOmit } from "@reach/utils";
+import type { DistributiveOmit } from "@reach/utils/types";
import type { StateMachine } from "@reach/machine";
import type {
ListboxNodeRefs,
@@ -95,10 +92,7 @@ const ListboxGroupContext = createNamedContext(
*
* @see Docs https://reach.tech/listbox#listboxinput
*/
-const ListboxInput = forwardRefWithAs<
- ListboxInputProps & { _componentName?: string },
- "div"
->(function ListboxInput(
+const ListboxInput = React.forwardRef(function ListboxInput(
{
as: Comp = "div",
"aria-labelledby": ariaLabelledBy,
@@ -113,7 +107,7 @@ const ListboxInput = forwardRefWithAs<
value: valueProp,
// We only use this prop for console warnings
- _componentName = "ListboxInput",
+ __componentName = "ListboxInput",
...props
},
forwardedRef
@@ -121,8 +115,6 @@ const ListboxInput = forwardRefWithAs<
let isControlled = React.useRef(valueProp != null);
let [options, setOptions] = useDescendantsInit();
- let stableOnChange = useStableCallback(onChange);
-
// DOM refs
let buttonRef = React.useRef(null);
let hiddenInputRef = React.useRef(null);
@@ -156,11 +148,17 @@ const ListboxInput = forwardRefWithAs<
DEBUG
);
+ let stableOnChange = useStableCallback((newValue: string) => {
+ if (newValue !== state.context.value) {
+ onChange?.(newValue);
+ }
+ });
+
// IDs for aria attributes
let _id = useId(props.id);
let id = props.id || makeId("listbox-input", _id);
- let ref = useForkedRef(inputRef, forwardedRef);
+ let ref = useComposedRefs(inputRef, forwardedRef);
// If the button has children, we just render them as the label.
// Otherwise we'll find the option with a value that matches the listbox value
@@ -234,7 +232,7 @@ const ListboxInput = forwardRefWithAs<
}
}
- useControlledSwitchWarning(valueProp, "value", _componentName);
+ useControlledSwitchWarning(valueProp, "value", __componentName);
// Even if the app controls state, we still need to update it internally to
// run the state machine transitions
@@ -335,7 +333,10 @@ const ListboxInput = forwardRefWithAs<
);
-});
+}) as Polymorphic.ForwardRefComponent<
+ "div",
+ ListboxInputProps & { __componentName?: string }
+>;
if (__DEV__) {
ListboxInput.displayName = "ListboxInput";
@@ -416,12 +417,12 @@ type ListboxInputProps = Pick<
*
* @see Docs https://reach.tech/listbox#listbox-1
*/
-const Listbox = forwardRefWithAs(function Listbox(
+const Listbox = React.forwardRef(function Listbox(
{ arrow = "▼", button, children, portal = true, ...props },
forwardedRef
) {
return (
-
+
{({ value, valueLabel }) => (
(function Listbox(
)}
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", ListboxProps>;
if (__DEV__) {
Listbox.displayName = "Listbox";
@@ -496,123 +497,119 @@ type ListboxProps = Omit &
*
* @see Docs https://reach.tech/listbox#listbox-button
*/
-const ListboxButtonImpl = forwardRefWithAs(
- function ListboxButton(
- {
- "aria-label": ariaLabel,
- arrow = false,
- as: Comp = "span",
- children,
- onKeyDown,
- onMouseDown,
- onMouseUp,
- ...props
- },
- forwardedRef
- ) {
- let {
- ariaLabelledBy,
- buttonRef,
- disabled,
- isExpanded,
- listboxId,
- stateData,
- send,
- listboxValueLabel,
- } = React.useContext(ListboxContext);
- let listboxValue = stateData.value;
+const ListboxButtonImpl = React.forwardRef(function ListboxButton(
+ {
+ "aria-label": ariaLabel,
+ arrow = false,
+ as: Comp = "span",
+ children,
+ onKeyDown,
+ onMouseDown,
+ onMouseUp,
+ ...props
+ },
+ forwardedRef
+) {
+ let {
+ ariaLabelledBy,
+ buttonRef,
+ disabled,
+ isExpanded,
+ listboxId,
+ stateData,
+ send,
+ listboxValueLabel,
+ } = React.useContext(ListboxContext);
+ let listboxValue = stateData.value;
- let ref = useForkedRef(buttonRef, forwardedRef);
+ let ref = useComposedRefs(buttonRef, forwardedRef);
- let handleKeyDown = useKeyDown();
+ let handleKeyDown = useKeyDown();
- function handleMouseDown(event: React.MouseEvent) {
- if (!isRightClick(event.nativeEvent)) {
- event.preventDefault();
- event.stopPropagation();
- send({
- type: ListboxEvents.ButtonMouseDown,
- disabled,
- });
- }
+ function handleMouseDown(event: React.MouseEvent) {
+ if (!isRightClick(event.nativeEvent)) {
+ event.preventDefault();
+ event.stopPropagation();
+ send({
+ type: ListboxEvents.ButtonMouseDown,
+ disabled,
+ });
}
+ }
- function handleMouseUp(event: React.MouseEvent) {
- if (!isRightClick(event.nativeEvent)) {
- event.preventDefault();
- event.stopPropagation();
- send({ type: ListboxEvents.ButtonMouseUp });
- }
+ function handleMouseUp(event: React.MouseEvent) {
+ if (!isRightClick(event.nativeEvent)) {
+ event.preventDefault();
+ event.stopPropagation();
+ send({ type: ListboxEvents.ButtonMouseUp });
}
+ }
- let id = makeId("button", listboxId);
-
- // If the button has children, we just render them as the label
- // If a user needs the label on the server to prevent hydration mismatch
- // errors, they need to control the state of the component and pass a label
- // directly to the button.
- let label: React.ReactNode = React.useMemo(() => {
- if (!children) {
- return listboxValueLabel;
- } else if (isFunction(children)) {
- return children({
- isExpanded,
- label: listboxValueLabel!,
- value: listboxValue,
- // TODO: Remove in 1.0
- expanded: isExpanded,
- });
- }
- return children;
- }, [children, listboxValueLabel, isExpanded, listboxValue]);
+ let id = makeId("button", listboxId);
+
+ // If the button has children, we just render them as the label
+ // If a user needs the label on the server to prevent hydration mismatch
+ // errors, they need to control the state of the component and pass a label
+ // directly to the button.
+ let label: React.ReactNode = React.useMemo(() => {
+ if (!children) {
+ return listboxValueLabel;
+ } else if (isFunction(children)) {
+ return children({
+ isExpanded,
+ label: listboxValueLabel!,
+ value: listboxValue,
+ // TODO: Remove in 1.0
+ expanded: isExpanded,
+ });
+ }
+ return children;
+ }, [children, listboxValueLabel, isExpanded, listboxValue]);
- return (
-
- {label}
- {arrow && (
- {isBoolean(arrow) ? null : arrow}
- )}
-
- );
- }
-);
+ return (
+
+ {label}
+ {arrow && {isBoolean(arrow) ? null : arrow}}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"span", ListboxButtonProps>;
if (__DEV__) {
ListboxButtonImpl.displayName = "ListboxButton";
@@ -622,12 +619,14 @@ if (__DEV__) {
};
}
-const ListboxButton = memoWithAs(ListboxButtonImpl);
+const ListboxButton = React.memo(
+ ListboxButtonImpl
+) as Polymorphic.MemoComponent<"span", ListboxButtonProps>;
/**
* @see Docs https://reach.tech/listbox#listboxbutton-props
*/
-type ListboxButtonProps = {
+interface ListboxButtonProps {
/**
* Renders a text string or React node to represent an arrow inside the
* button.
@@ -683,7 +682,7 @@ type ListboxButtonProps = {
// TODO: Remove in 1.0
expanded: boolean;
}) => React.ReactNode);
-};
+}
////////////////////////////////////////////////////////////////////////////////
@@ -694,33 +693,31 @@ type ListboxButtonProps = {
*
* @see Docs https://reach.tech/listbox#listboxarrow
*/
-const ListboxArrowImpl = forwardRefWithAs(
- function ListboxArrow(
- { as: Comp = "span", children, ...props },
- forwardedRef
- ) {
- let { isExpanded } = React.useContext(ListboxContext);
- return (
-
- {isFunction(children)
- ? children({
- isExpanded,
- // TODO: Remove in 1.0
- expanded: isExpanded,
- })
- : children || "▼"}
-
- );
- }
-);
+const ListboxArrowImpl = React.forwardRef(function ListboxArrow(
+ { as: Comp = "span", children, ...props },
+ forwardedRef
+) {
+ let { isExpanded } = React.useContext(ListboxContext);
+ return (
+
+ {isFunction(children)
+ ? children({
+ isExpanded,
+ // TODO: Remove in 1.0
+ expanded: isExpanded,
+ })
+ : children || "▼"}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"span", ListboxArrowProps>;
if (__DEV__) {
ListboxArrowImpl.displayName = "ListboxArrow";
@@ -729,12 +726,15 @@ if (__DEV__) {
};
}
-const ListboxArrow = memoWithAs(ListboxArrowImpl);
+const ListboxArrow = React.memo(ListboxArrowImpl) as Polymorphic.MemoComponent<
+ "span",
+ ListboxArrowProps
+>;
/**
* @see Docs https://reach.tech/listbox#listboxarrow-props
*/
-type ListboxArrowProps = {
+interface ListboxArrowProps {
/**
* Children to render as the listbox button's arrow. This can be a render
* function that accepts the listbox's expanded state as an argument.
@@ -746,7 +746,7 @@ type ListboxArrowProps = {
// TODO: Remove in 1.0
expanded: boolean;
}) => React.ReactNode);
-};
+}
////////////////////////////////////////////////////////////////////////////////
@@ -757,59 +757,57 @@ type ListboxArrowProps = {
*
* @see Docs https://reach.tech/listbox#listboxpopover
*/
-const ListboxPopoverImpl = forwardRefWithAs(
- function ListboxPopover(
- {
- as: Comp = "div",
- position = positionMatchWidth,
- onBlur,
- onKeyDown,
- portal = true,
- unstable_observableRefs,
- ...props
- },
- forwardedRef
- ) {
- let { buttonRef, popoverRef, send, isExpanded } = React.useContext(
- ListboxContext
- );
- let ref = useForkedRef(popoverRef, forwardedRef);
-
- let handleKeyDown = useKeyDown();
-
- let commonProps = {
- hidden: !isExpanded,
- tabIndex: -1,
- ...props,
- ref,
- "data-reach-listbox-popover": "",
- onBlur: wrapEvent(onBlur, handleBlur),
- onKeyDown: wrapEvent(onKeyDown, handleKeyDown),
- };
+const ListboxPopoverImpl = React.forwardRef(function ListboxPopover(
+ {
+ as: Comp = "div",
+ position = positionMatchWidth,
+ onBlur,
+ onKeyDown,
+ portal = true,
+ unstable_observableRefs,
+ ...props
+ },
+ forwardedRef
+) {
+ let { buttonRef, popoverRef, send, isExpanded } = React.useContext(
+ ListboxContext
+ );
+ let ref = useComposedRefs(popoverRef, forwardedRef);
+
+ let handleKeyDown = useKeyDown();
+
+ let commonProps = {
+ hidden: !isExpanded,
+ tabIndex: -1,
+ ...props,
+ ref,
+ "data-reach-listbox-popover": "",
+ onBlur: composeEventHandlers(onBlur, handleBlur),
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
+ };
- function handleBlur(event: React.FocusEvent) {
- let { nativeEvent } = event;
- requestAnimationFrame(() => {
- send({
- type: ListboxEvents.Blur,
- relatedTarget: nativeEvent.relatedTarget || nativeEvent.target,
- });
+ function handleBlur(event: React.FocusEvent) {
+ let { nativeEvent } = event;
+ requestAnimationFrame(() => {
+ send({
+ type: ListboxEvents.Blur,
+ relatedTarget: nativeEvent.relatedTarget || nativeEvent.target,
});
- }
-
- return portal ? (
-
- ) : (
-
- );
+ });
}
-);
+
+ return portal ? (
+
+ ) : (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", ListboxPopoverProps>;
if (__DEV__) {
ListboxPopoverImpl.displayName = "ListboxPopover";
@@ -820,12 +818,14 @@ if (__DEV__) {
};
}
-const ListboxPopover = memoWithAs(ListboxPopoverImpl);
+const ListboxPopover = React.memo(
+ ListboxPopoverImpl
+) as Polymorphic.MemoComponent<"div", ListboxPopoverProps>;
/**
* @see Docs https://reach.tech/listbox#listboxpopover-props
*/
-type ListboxPopoverProps = {
+interface ListboxPopoverProps {
/**
* `ListboxPopover` expects to receive `ListboxList` as its children.
*
@@ -846,7 +846,7 @@ type ListboxPopoverProps = {
*/
position?: PopoverProps["position"];
unstable_observableRefs?: PopoverProps["unstable_observableRefs"];
-};
+}
////////////////////////////////////////////////////////////////////////////////
@@ -857,52 +857,51 @@ type ListboxPopoverProps = {
*
* @see Docs https://reach.tech/listbox#listboxlist
*/
-const ListboxList = forwardRefWithAs(
- function ListboxList({ as: Comp = "ul", ...props }, forwardedRef) {
- let {
- ariaLabel,
- ariaLabelledBy,
- isExpanded,
- listboxId,
- listRef,
- stateData: { value, navigationValue },
- } = React.useContext(ListboxContext);
- let ref = useForkedRef(forwardedRef, listRef);
+const ListboxList = React.forwardRef(function ListboxList(
+ { as: Comp = "ul", ...props },
+ forwardedRef
+) {
+ let {
+ ariaLabel,
+ ariaLabelledBy,
+ isExpanded,
+ listboxId,
+ listRef,
+ stateData: { value, navigationValue },
+ } = React.useContext(ListboxContext);
+ let ref = useComposedRefs(forwardedRef, listRef);
- return (
-
- );
- }
-);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"ul", ListboxListProps>;
if (__DEV__) {
ListboxList.displayName = "ListboxList";
@@ -912,7 +911,7 @@ if (__DEV__) {
/**
* @see Docs https://reach.tech/listbox#listboxlist-props
*/
-type ListboxListProps = {};
+interface ListboxListProps {}
////////////////////////////////////////////////////////////////////////////////
@@ -923,166 +922,183 @@ type ListboxListProps = {};
*
* @see Docs https://reach.tech/listbox#listboxoption
*/
-const ListboxOption = forwardRefWithAs(
- function ListboxOption(
+const ListboxOption = React.forwardRef(function ListboxOption(
+ {
+ as: Comp = "li",
+ children,
+ disabled,
+ onClick,
+ onMouseDown,
+ onMouseEnter,
+ onMouseLeave,
+ onMouseMove,
+ onMouseUp,
+ onTouchStart,
+ value,
+ label: labelProp,
+ ...props
+ },
+ forwardedRef
+) {
+ if (__DEV__ && !value) {
+ throw Error(`A ListboxOption must have a value prop.`);
+ }
+
+ let {
+ highlightedOptionRef,
+ isExpanded,
+ onValueChange,
+ selectedOptionRef,
+ send,
+ state,
+ stateData: { value: listboxValue, navigationValue },
+ } = React.useContext(ListboxContext);
+
+ let [labelState, setLabel] = React.useState(labelProp);
+ let label = labelProp || labelState || "";
+
+ let ownRef = React.useRef(null);
+ useDescendant(
{
- as: Comp = "li",
- children,
- disabled,
- onMouseDown,
- onMouseEnter,
- onMouseLeave,
- onMouseMove,
- onMouseUp,
- onTouchStart,
+ element: ownRef.current!,
value,
- label: labelProp,
- ...props
+ label,
+ disabled: !!disabled,
},
- forwardedRef
- ) {
- if (__DEV__ && !value) {
- throw Error(`A ListboxOption must have a value prop.`);
- }
+ ListboxDescendantContext
+ );
- let {
- highlightedOptionRef,
- isExpanded,
- onValueChange,
- selectedOptionRef,
- send,
- state,
- stateData: { value: listboxValue, navigationValue },
- } = React.useContext(ListboxContext);
+ // After the ref is mounted to the DOM node, we check to see if we have an
+ // explicit label prop before looking for the node's textContent for
+ // typeahead functionality.
+ let getLabelFromDomNode = React.useCallback(
+ (node: HTMLElement) => {
+ if (!labelProp && node) {
+ setLabel((prevState) => {
+ if (node.textContent && prevState !== node.textContent) {
+ return node.textContent;
+ }
+ return prevState || "";
+ });
+ }
+ },
+ [labelProp]
+ );
- let [labelState, setLabel] = React.useState(labelProp);
- let label = labelProp || labelState || "";
+ let isHighlighted = navigationValue ? navigationValue === value : false;
+ let isSelected = listboxValue === value;
- let ownRef = React.useRef(null);
- useDescendant(
- {
- element: ownRef.current!,
- value,
- label,
- disabled: !!disabled,
- },
- ListboxDescendantContext
- );
+ let ref = useComposedRefs(
+ getLabelFromDomNode,
+ forwardedRef,
+ ownRef,
+ isSelected ? selectedOptionRef : null,
+ isHighlighted ? highlightedOptionRef : null
+ );
- // After the ref is mounted to the DOM node, we check to see if we have an
- // explicit label prop before looking for the node's textContent for
- // typeahead functionality.
- let getLabelFromDomNode = React.useCallback(
- (node: HTMLElement) => {
- if (!labelProp && node) {
- setLabel((prevState) => {
- if (node.textContent && prevState !== node.textContent) {
- return node.textContent;
- }
- return prevState || "";
- });
- }
- },
- [labelProp]
- );
+ function handleMouseEnter() {
+ send({
+ type: ListboxEvents.OptionMouseEnter,
+ value,
+ disabled: !!disabled,
+ });
+ }
- let isHighlighted = navigationValue ? navigationValue === value : false;
- let isSelected = listboxValue === value;
+ function handleTouchStart() {
+ send({
+ type: ListboxEvents.OptionTouchStart,
+ value,
+ disabled: !!disabled,
+ });
+ }
- let ref = useForkedRef(
- getLabelFromDomNode,
- forwardedRef,
- ownRef,
- isSelected ? selectedOptionRef : null,
- isHighlighted ? highlightedOptionRef : null
- );
+ function handleMouseLeave() {
+ send({ type: ListboxEvents.ClearNavSelection });
+ }
+
+ function handleMouseDown(event: React.MouseEvent) {
+ // Prevent blur event from firing and bubbling to the popover
+ if (!isRightClick(event.nativeEvent)) {
+ event.preventDefault();
+ send({ type: ListboxEvents.OptionMouseDown });
+ }
+ }
- function handleMouseEnter() {
+ function handleMouseUp(event: React.MouseEvent) {
+ if (!isRightClick(event.nativeEvent)) {
send({
- type: ListboxEvents.OptionMouseEnter,
+ type: ListboxEvents.OptionMouseUp,
value,
+ callback: onValueChange,
disabled: !!disabled,
});
}
+ }
- function handleTouchStart() {
+ function handleClick(event: React.MouseEvent) {
+ // Generally an option will be selected on mouseup, but in case this isn't
+ // handled correctly by the device (whether because it's a touch/pen or
+ // virtual click event) we want to handle selection on a full click event
+ // just in case. This should address issues with screenreader selection,
+ // but this needs more robust testing.
+ if (!isRightClick(event.nativeEvent)) {
send({
- type: ListboxEvents.OptionTouchStart,
+ type: ListboxEvents.OptionClick,
value,
+ callback: onValueChange,
disabled: !!disabled,
});
}
+ }
- function handleMouseLeave() {
- send({ type: ListboxEvents.ClearNavSelection });
- }
-
- function handleMouseDown(event: React.MouseEvent) {
- // Prevent blur event from firing and bubbling to the popover
- if (!isRightClick(event.nativeEvent)) {
- event.preventDefault();
- send({ type: ListboxEvents.OptionMouseDown });
- }
- }
-
- function handleMouseUp(event: React.MouseEvent) {
- if (!isRightClick(event.nativeEvent)) {
- send({
- type: ListboxEvents.OptionMouseUp,
- value,
- callback: onValueChange,
- disabled: !!disabled,
- });
- }
- }
-
- function handleMouseMove() {
- // We don't really *need* these guards since we put all of our transition
- // logic in the state machine, but in this case it seems wise not to
- // needlessly run our transitions every time the user's mouse moves. Seems
- // like a lot. 🙃
- if (state === ListboxStates.Open || navigationValue !== value) {
- send({
- type: ListboxEvents.OptionMouseMove,
- value,
- disabled: !!disabled,
- });
- }
+ function handleMouseMove() {
+ // We don't really *need* these guards since we put all of our transition
+ // logic in the state machine, but in this case it seems wise not to
+ // needlessly run our transitions every time the user's mouse moves. Seems
+ // like a lot. 🙃
+ if (state === ListboxStates.Open || navigationValue !== value) {
+ send({
+ type: ListboxEvents.OptionMouseMove,
+ value,
+ disabled: !!disabled,
+ });
}
-
- return (
-
- {children}
-
- );
}
-);
+
+ return (
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"li", ListboxOptionProps>;
if (__DEV__) {
ListboxOption.displayName = "ListboxOption";
@@ -1096,7 +1112,7 @@ if (__DEV__) {
/**
* @see Docs https://reach.tech/listbox#listboxoption-props
*/
-type ListboxOptionProps = {
+interface ListboxOptionProps {
/**
* The option's value. This will be passed into a hidden input field for use
* in forms.
@@ -1120,7 +1136,7 @@ type ListboxOptionProps = {
* @see Docs https://reach.tech/listbox#listboxoption-disabled
*/
disabled?: boolean;
-};
+}
////////////////////////////////////////////////////////////////////////////////
@@ -1131,33 +1147,31 @@ type ListboxOptionProps = {
*
* @see Docs https://reach.tech/listbox#listboxgroup
*/
-const ListboxGroup = forwardRefWithAs(
- function ListboxGroup(
- { as: Comp = "div", label, children, ...props },
- forwardedRef
- ) {
- let { listboxId } = React.useContext(ListboxContext);
- let labelId = makeId("label", useId(props.id), listboxId);
- return (
-
-
- {label && {label}}
- {children}
-
-
- );
- }
-);
+const ListboxGroup = React.forwardRef(function ListboxGroup(
+ { as: Comp = "div", label, children, ...props },
+ forwardedRef
+) {
+ let { listboxId } = React.useContext(ListboxContext);
+ let labelId = makeId("label", useId(props.id), listboxId);
+ return (
+
+
+ {label && {label}}
+ {children}
+
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", ListboxGroupProps>;
if (__DEV__) {
ListboxGroup.displayName = "ListboxGroup";
@@ -1169,7 +1183,7 @@ if (__DEV__) {
/**
* @see Docs https://reach.tech/listbox#listboxgroup-props
*/
-type ListboxGroupProps = {
+interface ListboxGroupProps {
/**
* The text label to use for the listbox group. This can be omitted if a
* group contains a `ListboxGroupLabel` component. The label should always
@@ -1178,7 +1192,7 @@ type ListboxGroupProps = {
* @see Docs https://reach.tech/listbox#listboxgroup-label
*/
label?: React.ReactNode;
-};
+}
////////////////////////////////////////////////////////////////////////////////
@@ -1187,22 +1201,23 @@ type ListboxGroupProps = {
*
* @see Docs https://reach.tech/listbox#listboxgrouplabel
*/
-const ListboxGroupLabel = forwardRefWithAs(
- function ListboxGroupLabel({ as: Comp = "span", ...props }, forwardedRef) {
- let { labelId } = React.useContext(ListboxGroupContext);
- return (
-
- );
- }
-);
+const ListboxGroupLabel = React.forwardRef(function ListboxGroupLabel(
+ { as: Comp = "span", ...props },
+ forwardedRef
+) {
+ let { labelId } = React.useContext(ListboxGroupContext);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"span", ListboxGroupLabelProps>;
if (__DEV__) {
ListboxGroupLabel.displayName = "ListboxGroupLabel";
@@ -1212,7 +1227,7 @@ if (__DEV__) {
/**
* @see Docs https://reach.tech/listbox#listboxgroup-props
*/
-type ListboxGroupLabelProps = {};
+interface ListboxGroupLabelProps {}
////////////////////////////////////////////////////////////////////////////////
@@ -1290,7 +1305,7 @@ function useKeyDown() {
let index = options.findIndex(({ value }) => value === navigationValue);
- let handleKeyDown = wrapEvent(
+ let handleKeyDown = composeEventHandlers(
function (event: React.KeyboardEvent) {
let { key } = event;
let isSearching = isString(key) && key.length === 1;
@@ -1379,14 +1394,14 @@ type ListboxDescendant = Descendant & {
disabled: boolean;
};
-type ListboxContextValue = {
+interface ListboxContextValue {
id: string | undefined;
isExpanded: boolean;
highlightedOptionRef: React.RefObject;
selectedOptionRef: React.RefObject;
value: ListboxValue | null;
valueLabel: string | null;
-};
+}
interface InternalListboxContextValue {
ariaLabel?: string;
diff --git a/packages/listbox/src/machine.ts b/packages/listbox/src/machine.ts
index 8c5d39996..fd736423c 100644
--- a/packages/listbox/src/machine.ts
+++ b/packages/listbox/src/machine.ts
@@ -1,5 +1,5 @@
import { assign } from "@reach/machine";
-import { getOwnerDocument } from "@reach/utils";
+import { getOwnerDocument } from "@reach/utils/owner-document";
import type { ListboxDescendant, ListboxValue } from "./index";
import type { MachineEventWithRefs, StateMachine } from "@reach/machine";
@@ -41,9 +41,18 @@ export enum ListboxEvents {
KeyDownSearch = "KEY_DOWN_SEARCH",
KeyDownTab = "KEY_DOWN_TAB",
KeyDownShiftTab = "KEY_DOWN_SHIFT_TAB",
+
OptionTouchStart = "OPTION_TOUCH_START",
OptionMouseMove = "OPTION_MOUSE_MOVE",
OptionMouseEnter = "OPTION_MOUSE_ENTER",
+ OptionMouseDown = "OPTION_MOUSE_DOWN",
+ OptionMouseUp = "OPTION_MOUSE_UP",
+ OptionClick = "OPTION_CLICK",
+
+ // WIP: Simplify and consolidate events
+ // TODO: Use a separate machine to deal with states to determine press events
+ OptionPress = "OPTION_PRESS",
+
OutsideMouseDown = "OUTSIDE_MOUSE_DOWN",
OutsideMouseUp = "OUTSIDE_MOUSE_UP",
@@ -51,8 +60,6 @@ export enum ListboxEvents {
// ValueChange > Value change may have come from somewhere else
ValueChange = "VALUE_CHANGE",
- OptionMouseDown = "OPTION_MOUSE_DOWN",
- OptionMouseUp = "OPTION_MOUSE_UP",
PopoverPointerDown = "POPOVER_POINTER_DOWN",
PopoverPointerUp = "POPOVER_POINTER_UP",
UpdateAfterTypeahead = "UPDATE_AFTER_TYPEAHEAD",
@@ -178,10 +185,13 @@ function optionIsNavigable(data: ListboxStateData, event: ListboxEvent) {
return true;
}
-function optionIsSelectable(data: ListboxStateData, event: any) {
- if (event && event.disabled) {
+function optionIsSelectable(data: ListboxStateData, event: ListboxEvent) {
+ if ("disabled" in event && event.disabled) {
return false;
}
+ if ("value" in event) {
+ return event.value != null;
+ }
return data.navigationValue != null;
}
@@ -410,6 +420,16 @@ export const createMachineDefinition = ({
actions: [navigate, clearTypeahead],
cond: optionIsNavigable,
},
+ [ListboxEvents.OptionClick]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
+ [ListboxEvents.OptionPress]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
[ListboxEvents.OptionMouseEnter]: {
target: ListboxStates.Navigating,
actions: [navigate, clearTypeahead],
@@ -502,6 +522,16 @@ export const createMachineDefinition = ({
actions: [navigate, clearTypeahead],
cond: optionIsNavigable,
},
+ [ListboxEvents.OptionClick]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
+ [ListboxEvents.OptionPress]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
[ListboxEvents.KeyDownNavigate]: {
target: ListboxStates.Navigating,
actions: [navigate, clearTypeahead, focusList],
@@ -611,6 +641,16 @@ export const createMachineDefinition = ({
actions: [navigate, clearTypeahead],
cond: optionIsNavigable,
},
+ [ListboxEvents.OptionClick]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
+ [ListboxEvents.OptionPress]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
[ListboxEvents.OptionMouseEnter]: {
target: ListboxStates.Dragging,
actions: [navigate, clearTypeahead],
@@ -728,6 +768,16 @@ export const createMachineDefinition = ({
actions: [navigate, clearTypeahead],
cond: optionIsNavigable,
},
+ [ListboxEvents.OptionClick]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
+ [ListboxEvents.OptionPress]: {
+ target: ListboxStates.Idle,
+ actions: [assignValue, clearTypeahead, focusButton, selectOption],
+ cond: optionIsSelectable,
+ },
[ListboxEvents.OptionMouseEnter]: {
target: ListboxStates.Navigating,
actions: [navigate, clearTypeahead],
@@ -890,6 +940,18 @@ export type ListboxEvent = ListboxEventBase &
callback?: ((newValue: ListboxValue) => void) | null | undefined;
disabled: boolean;
}
+ | {
+ type: ListboxEvents.OptionClick;
+ value: ListboxValue | null | undefined;
+ callback?: ((newValue: ListboxValue) => void) | null | undefined;
+ disabled: boolean;
+ }
+ | {
+ type: ListboxEvents.OptionPress;
+ value: ListboxValue | null | undefined;
+ callback?: ((newValue: ListboxValue) => void) | null | undefined;
+ disabled: boolean;
+ }
| {
type: ListboxEvents.KeyDownTab;
}
diff --git a/packages/listbox/styles.css b/packages/listbox/styles.css
index a8713cb39..c982567fa 100644
--- a/packages/listbox/styles.css
+++ b/packages/listbox/styles.css
@@ -42,16 +42,16 @@
user-select: none;
}
-[data-reach-listbox-option][aria-selected="true"] {
+[data-reach-listbox-option][data-current-nav] {
background: hsl(211, 81%, 46%);
color: hsl(0, 0%, 100%);
}
-[data-reach-listbox-option][data-current] {
+[data-reach-listbox-option][data-current-selected] {
font-weight: bolder;
}
-[data-reach-listbox-option][data-current][data-confirming] {
+[data-reach-listbox-option][data-current-selected][data-confirming] {
animation: flash 100ms;
animation-iteration-count: 1;
}
diff --git a/packages/machine/package.json b/packages/machine/package.json
index 7702ea893..6dff1405c 100644
--- a/packages/machine/package.json
+++ b/packages/machine/package.json
@@ -1,22 +1,25 @@
{
"name": "@reach/machine",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "State machine utilities for the Reach UI library.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/machine"
},
"dependencies": {
- "@reach/utils": "0.13.1",
+ "@reach/utils": "0.15.0",
"@xstate/fsm": "1.4.0",
- "tslib": "^2.0.0"
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/machine/src/index.tsx b/packages/machine/src/index.tsx
index 5c35aab82..74de919b7 100644
--- a/packages/machine/src/index.tsx
+++ b/packages/machine/src/index.tsx
@@ -5,9 +5,10 @@ import {
interpret,
InterpreterStatus,
} from "@xstate/fsm";
-import { isString, useConstant } from "@reach/utils";
+import { isString } from "@reach/utils/type-check";
+import { useConstant } from "@reach/utils/use-constant";
-import type { DistributiveOmit } from "@reach/utils";
+import type { DistributiveOmit } from "@reach/utils/types";
import type {
EventObject as MachineEvent,
StateMachine,
diff --git a/packages/menu-button/__tests__/menu-button.test.tsx b/packages/menu-button/__tests__/menu-button.test.tsx
index 5d326a80d..668bfde45 100644
--- a/packages/menu-button/__tests__/menu-button.test.tsx
+++ b/packages/menu-button/__tests__/menu-button.test.tsx
@@ -1,172 +1,342 @@
import * as React from "react";
-import { render, act, fireEvent, fireClickAndMouseEvents } from "$test/utils";
-import { AxeResults } from "$test/types";
+import {
+ render,
+ screen,
+ fireEvent,
+ simulateMouseClick,
+ simulateSpaceKeyClick,
+ simulateEnterKeyClick,
+} from "$test/utils";
import { axe } from "jest-axe";
-import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button";
+import {
+ Menu,
+ MenuList,
+ MenuButton,
+ MenuItem,
+ MenuLink,
+} from "@reach/menu-button";
-describe("", () => {
- describe("rendering", () => {
- it("should mount the component", () => {
- let { queryByRole } = render(
-
- );
- expect(queryByRole("button")).toBeTruthy();
+let noop = () => {};
+
+describe(" with ", () => {
+ describe("a11y", () => {
+ it("Should not have ARIA violations", async () => {
+ let { container, list, button } = renderTestMenu();
+
+ let results = await axe(container);
+ expect(results).toHaveNoViolations();
+
+ // Toggle the menu and check again
+ simulateMouseClick(button);
+ results = await axe(container);
+
+ // We have to check the container and list separately since the list is
+ // portaled outside of the container.
+ let listResults = await axe(list);
+
+ expect(results).toHaveNoViolations();
+ expect(listResults).toHaveNoViolations();
});
- it("should mount with render props", () => {
- let { queryByRole } = render(
-
- );
- expect(queryByRole("button")).toBeTruthy();
+ describe("ARIA attributes", () => {
+ it("`role` is set to `menu` for list element", () => {
+ let { button, list } = renderTestMenu();
+
+ // Toggle the menu so that it is rendered
+ simulateMouseClick(button);
+ expect(list).toHaveAttribute("role", "menu");
+ });
+
+ it("`aria-controls` for button element points to the list element id", () => {
+ let rendered = renderTestMenu();
+
+ // Toggle the menu so that it is rendered
+ simulateMouseClick(rendered.button);
+
+ let id = rendered.list.getAttribute("id");
+ expect(rendered.button).toHaveAttribute("aria-controls", id);
+ });
+
+ it("`aria-haspopup` for button element is present", () => {
+ let rendered = renderTestMenu();
+ expect(rendered.button).toHaveAttribute("aria-haspopup");
+ });
+
+ describe("when the list is not toggled", () => {
+ it("`aria-expanded` for the button element is not present", () => {
+ let rendered = renderTestMenu();
+ expect(rendered.button).not.toHaveAttribute("aria-expanded");
+ });
+ });
+
+ describe("when the list is toggled", () => {
+ let rendered: ReturnType;
+ beforeEach(() => {
+ rendered = renderTestMenu();
+ simulateMouseClick(rendered.button);
+ });
+
+ it("`role` is set to `menuitem` for item elements", () => {
+ expect(rendered.items[0]).toHaveAttribute("role", "menuitem");
+ expect(rendered.items[1]).toHaveAttribute("role", "menuitem");
+ });
+
+ it("`aria-expanded` for the button element is true", async () => {
+ expect(rendered.button).toHaveAttribute("aria-expanded", "true");
+ });
+
+ it("`aria-labelledby` for list element points to the button element id", () => {
+ let id = rendered.button.getAttribute("id");
+ expect(rendered.list).toHaveAttribute("aria-labelledby", id);
+ });
+
+ it("`aria-activedescendant` for list element is not present", () => {
+ expect(rendered.list).not.toHaveAttribute("aria-activedescendant");
+ });
+
+ describe("when mouse enters an item", () => {
+ it("`aria-activedescendant` for list element points to the active item", () => {
+ let item = rendered.items[0];
+ let id = item.getAttribute("id");
+ fireEvent.mouseEnter(item);
+ expect(rendered.list).toHaveAttribute("aria-activedescendant", id);
+ });
+ });
+ });
});
});
- describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
- let { container } = render(
+ describe("rendering", () => {
+ it("passes DOM props to the button", () => {
+ let { getByRole } = render(
);
- let results: AxeResults = null as any;
- await act(async () => {
- results = await axe(container);
- });
- expect(results).toHaveNoViolations();
+ let button = getByRole("button");
+ expect(button).toHaveAttribute("id", "test-id");
+ });
+
+ it("should not show the menu list by default", () => {
+ let { list } = renderTestMenu();
+ expect(list).not.toBeVisible();
});
});
describe("user events", () => {
- it("should toggle on button click", () => {
- let { getByRole, baseElement } = render(
-
- );
-
- let getPopover = () =>
- baseElement.querySelector("[data-reach-menu-popover]");
-
- expect(getPopover()).not.toBeVisible();
+ it("should show the list when the button is clicked", () => {
+ let rendered = renderTestMenu();
+ simulateMouseClick(rendered.button);
- fireClickAndMouseEvents(getByRole("button"));
- expect(getPopover()).toBeVisible();
- fireClickAndMouseEvents(getByRole("button"));
- expect(getPopover()).not.toBeVisible();
+ expect(rendered.list).toBeVisible();
+ simulateMouseClick(rendered.button);
+ expect(rendered.list).not.toBeVisible();
});
- it("should not re-focus the button when user selects an item with click", () => {
- let { getByRole, getByText } = render(
-
- );
+ it("should call `onSelect` when user selects an item", () => {
+ let rendered = renderTestMenu();
+ simulateSpaceKeyClick(rendered.button, { fireClick: true });
+ simulateSpaceKeyClick(rendered.items[0]);
+ expect(rendered.selectCallbacks[0]).toHaveBeenCalledTimes(1);
+ });
- fireClickAndMouseEvents(getByRole("button"));
- fireClickAndMouseEvents(getByText("Download"));
- expect(getByRole("button")).not.toHaveFocus();
+ it("should not focus the button when user selects an item with click", () => {
+ let rendered = renderTestMenu();
+ simulateMouseClick(rendered.button);
+ simulateMouseClick(rendered.items[0]);
+ expect(rendered.button).not.toHaveFocus();
});
it("should manage focus when user selects an item with `Space` key", () => {
- let { getByRole, getByText } = render(
-
- );
+ let rendered = renderTestMenu();
- fireEvent.keyDown(getByRole("button"), { key: " " });
- fireEvent.keyDown(getByText("Download"), { key: " " });
- expect(getByRole("button")).toHaveFocus();
+ simulateSpaceKeyClick(rendered.button, { fireClick: true });
+ simulateSpaceKeyClick(rendered.items[0]);
+ expect(rendered.button).toHaveFocus();
});
it("should manage focus when user selects an item with `Enter` key", () => {
- let { getByRole, getByText } = render(
-
- );
-
- fireEvent.keyDown(getByRole("button"), { key: "Enter" });
- fireEvent.keyDown(getByText("Download"), { key: "Enter" });
+ let rendered = renderTestMenu();
- expect(getByRole("button")).toHaveFocus();
+ simulateSpaceKeyClick(rendered.button, { fireClick: true });
+ simulateEnterKeyClick(rendered.items[0]);
+ expect(rendered.button).toHaveFocus();
});
it("should manage focus when user dismisses with the `Escape` key", () => {
- let { getByRole, getByText } = render(
-
- );
-
- fireClickAndMouseEvents(getByRole("button"));
- fireEvent.keyDown(getByText("Download"), { key: "Escape" });
-
- expect(getByRole("button")).toHaveFocus();
+ let rendered = renderTestMenu();
+ simulateMouseClick(rendered.button);
+ fireEvent.keyDown(rendered.list, { key: "Escape" });
+ expect(rendered.button).toHaveFocus();
});
it("should not manage focus when user clicks outside element", () => {
- let { getByRole, getByTestId } = render(
+ let { getByRole } = render(
<>
-
+
>
);
+ let button = getByRole("button");
+ let input = getByRole("textbox");
- fireClickAndMouseEvents(getByRole("button"));
- fireEvent.click(getByTestId("input"));
- expect(getByRole("button")).not.toHaveFocus();
+ simulateMouseClick(button);
+ fireEvent.click(input);
+ expect(button).not.toHaveFocus();
});
});
});
+
+describe(" with ", () => {
+ describe("a11y", () => {
+ it("Should not have ARIA violations", async () => {
+ let { container, list, button } = renderTestMenuWithLinks();
+
+ let results = await axe(container);
+ expect(results).toHaveNoViolations();
+
+ // Toggle the menu and check again
+ simulateMouseClick(button);
+ results = await axe(container);
+
+ // We have to check the container and list separately since the list is
+ // portaled outside of the container.
+ let listResults = await axe(list);
+
+ expect(results).toHaveNoViolations();
+ expect(listResults).toHaveNoViolations();
+ });
+ });
+});
+
+describe(" with and ", () => {
+ describe("a11y", () => {
+ it("Should not have ARIA violations", async () => {
+ let { container, list, button } = renderTestMenuWithLinksAndItems();
+
+ let results = await axe(container);
+ expect(results).toHaveNoViolations();
+
+ // Toggle the menu and check again
+ simulateMouseClick(button);
+ results = await axe(container);
+
+ // We have to check the container and list separately since the list is
+ // portaled outside of the container.
+ let listResults = await axe(list);
+
+ expect(results).toHaveNoViolations();
+ expect(listResults).toHaveNoViolations();
+ });
+ });
+});
+
+function renderTestMenu() {
+ let cb1 = jest.fn();
+ let cb2 = jest.fn();
+ let { getByRole, container } = render(
+
+ );
+ return {
+ container,
+ get root() {
+ return document.body;
+ },
+ get button() {
+ return getByRole("button");
+ },
+ get list() {
+ return screen.getByTestId("list");
+ },
+ get items() {
+ return [screen.getByText("Download"), screen.getByText("Create a Copy")];
+ },
+ selectCallbacks: [cb1, cb2],
+ };
+}
+
+function renderTestMenuWithLinks() {
+ let cb1 = jest.fn();
+ let cb2 = jest.fn();
+ let { getByRole, container } = render(
+
+ );
+ return {
+ container,
+ get root() {
+ return document.body;
+ },
+ get button() {
+ return getByRole("button");
+ },
+ get list() {
+ return screen.getByTestId("list");
+ },
+ get items() {
+ return [screen.getByText("Home"), screen.getByText("About")];
+ },
+ selectCallbacks: [cb1, cb2],
+ };
+}
+
+function renderTestMenuWithLinksAndItems() {
+ let cb1 = jest.fn();
+ let cb2 = jest.fn();
+ let { getByRole, container } = render(
+
+ );
+ return {
+ container,
+ get root() {
+ return document.body;
+ },
+ get button() {
+ return getByRole("button");
+ },
+ get list() {
+ return screen.getByTestId("list");
+ },
+ get items() {
+ return [screen.getByText("Home"), screen.getByText("About")];
+ },
+ selectCallbacks: [cb1, cb2],
+ };
+}
diff --git a/packages/menu-button/examples/basic.example.js b/packages/menu-button/examples/basic.example.js
index d7b8e731f..0d11cf9b9 100644
--- a/packages/menu-button/examples/basic.example.js
+++ b/packages/menu-button/examples/basic.example.js
@@ -1,5 +1,11 @@
import * as React from "react";
-import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button";
+import {
+ Menu,
+ MenuList,
+ MenuLink,
+ MenuButton,
+ MenuItem,
+} from "@reach/menu-button";
import { action } from "@storybook/addon-actions";
import "@reach/menu-button/styles.css";
@@ -7,17 +13,34 @@ let name = "Basic";
function Example() {
return (
-
+
+
+
+
);
}
diff --git a/packages/menu-button/examples/basic.example.tsx b/packages/menu-button/examples/basic.example.tsx
index 1241df13a..df143740c 100644
--- a/packages/menu-button/examples/basic.example.tsx
+++ b/packages/menu-button/examples/basic.example.tsx
@@ -1,23 +1,46 @@
import * as React from "react";
import { action } from "@storybook/addon-actions";
-import { Menu, MenuList, MenuButton, MenuItem } from "@reach/menu-button";
+import {
+ Menu,
+ MenuList,
+ MenuButton,
+ MenuItem,
+ MenuLink,
+} from "@reach/menu-button";
import "@reach/menu-button/styles.css";
let name = "Basic (TS)";
function Example() {
return (
-
+
+
+
+
);
}
diff --git a/packages/menu-button/examples/with-links.example.js b/packages/menu-button/examples/with-links.example.js
deleted file mode 100644
index 628180505..000000000
--- a/packages/menu-button/examples/with-links.example.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as React from "react";
-import { action } from "@storybook/addon-actions";
-import {
- Menu,
- MenuList,
- MenuButton,
- MenuLink,
- MenuItem,
-} from "@reach/menu-button";
-import {
- Router,
- Link,
- createMemorySource,
- createHistory,
- LocationProvider,
-} from "@reach/router";
-import "@reach/menu-button/styles.css";
-
-let name = "With Links";
-
-function Example() {
- return (
-
-
-
-
-
-
- );
-}
-
-Example.story = { name };
-export const Comp = Example;
-export default { title: "MenuButton" };
-
-////////////////////////////////////////////////////////////////////////////////
-
-// this is because we're in an iframe and not a
-// pushState server inside of storybook
-let memoryHistory = createHistory(createMemorySource("/"));
-
-function Home() {
- return (
-
-
Home
-
-
- );
-}
-
-function Settings() {
- return (
-
-
Settings
-
- Go Home
-
-
- );
-}
diff --git a/packages/menu-button/package.json b/packages/menu-button/package.json
index 94273fc3f..bafeb5bde 100644
--- a/packages/menu-button/package.json
+++ b/packages/menu-button/package.json
@@ -1,25 +1,29 @@
{
"name": "@reach/menu-button",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React button dropdown menu.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/menu-button"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/descendants": "0.13.1",
- "@reach/popover": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/descendants": "0.15.0",
+ "@reach/popover": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/menu-button/src/index.tsx b/packages/menu-button/src/index.tsx
index 0b2fc4917..de0f68869 100644
--- a/packages/menu-button/src/index.tsx
+++ b/packages/menu-button/src/index.tsx
@@ -13,8 +13,9 @@
import * as React from "react";
import PropTypes from "prop-types";
+import warning from "tiny-warning";
import { useId } from "@reach/auto-id";
-import { Popover, Position } from "@reach/popover";
+import { Popover } from "@reach/popover";
import {
createDescendantContext,
DescendantProvider,
@@ -23,20 +24,19 @@ import {
useDescendantsInit,
useDescendantKeyDown,
} from "@reach/descendants";
-import {
- createNamedContext,
- forwardRefWithAs,
- getOwnerDocument,
- isFunction,
- isString,
- makeId,
- noop,
- useCheckStyles,
- useForkedRef,
- usePrevious,
- wrapEvent,
-} from "@reach/utils";
-
+import { isRightClick } from "@reach/utils/is-right-click";
+import { usePrevious } from "@reach/utils/use-previous";
+import { getOwnerDocument } from "@reach/utils/owner-document";
+import { createNamedContext } from "@reach/utils/context";
+import { isFunction, isString } from "@reach/utils/type-check";
+import { makeId } from "@reach/utils/make-id";
+import { noop } from "@reach/utils/noop";
+import { useCheckStyles } from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
+
+import type { Position } from "@reach/popover";
+import type * as Polymorphic from "@reach/utils/polymorphic";
import type { Descendant } from "@reach/descendants";
////////////////////////////////////////////////////////////////////////////////
@@ -55,9 +55,13 @@ const SET_BUTTON_ID = "SET_BUTTON_ID";
const MenuDescendantContext = createDescendantContext(
"MenuDescendantContext"
);
-const MenuContext = createNamedContext(
- "MenuContext",
- {} as InternalMenuContextValue
+const StableMenuContext = createNamedContext(
+ "StableMenuContext",
+ {} as StableMenuContextValue
+);
+const UnstableMenuContext = createNamedContext(
+ "UnstableMenuContext",
+ {} as UnstableMenuContextValue
);
const initialState: MenuButtonState = {
@@ -118,15 +122,22 @@ const Menu: React.FC = ({ id, children }) => {
// on most platforms, and our menu button popover works similarly.
let readyToSelect = React.useRef(false);
- let context: InternalMenuContextValue = {
- buttonRef,
- dispatch,
+ // Trying a new approach for splitting up contexts by stable/unstable
+ // references. We'll see how it goes!
+ let stableContext = React.useMemo(() => {
+ return {
+ buttonRef,
+ dispatch,
+ menuRef,
+ popoverRef,
+ buttonClickedRef,
+ readyToSelect,
+ selectCallbacks,
+ };
+ }, []);
+
+ let unstableContext: UnstableMenuContextValue = {
menuId,
- menuRef,
- popoverRef,
- buttonClickedRef,
- readyToSelect,
- selectCallbacks,
state,
};
@@ -156,15 +167,17 @@ const Menu: React.FC = ({ id, children }) => {
items={descendants}
set={setDescendants}
>
-
- {isFunction(children)
- ? children({
- isExpanded: state.isExpanded,
- // TODO: Remove in 1.0
- isOpen: state.isExpanded,
- })
- : children}
-
+
+
+ {isFunction(children)
+ ? children({
+ isExpanded: state.isExpanded,
+ // TODO: Remove in 1.0
+ isOpen: state.isExpanded,
+ })
+ : children}
+
+
);
};
@@ -206,112 +219,106 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/menu-button#menubutton
*/
-const MenuButton = forwardRefWithAs(
- function MenuButton(
- { as: Comp = "button", onKeyDown, onMouseDown, id, ...props },
- forwardedRef
- ) {
- let {
- buttonRef,
- buttonClickedRef,
- menuId,
- state: { buttonId, isExpanded },
- dispatch,
- } = React.useContext(MenuContext);
- let ref = useForkedRef(buttonRef, forwardedRef);
- let items = useDescendants(MenuDescendantContext);
- let firstNonDisabledIndex = React.useMemo(
- () => items.findIndex((item) => !item.disabled),
- [items]
- );
- React.useEffect(() => {
- let newButtonId =
- id != null
- ? id
- : menuId
- ? makeId("menu-button", menuId)
- : "menu-button";
- if (buttonId !== newButtonId) {
+const MenuButton = React.forwardRef(function MenuButton(
+ { as: Comp = "button", onKeyDown, onMouseDown, id, ...props },
+ forwardedRef
+) {
+ let { buttonRef, buttonClickedRef, dispatch } = React.useContext(
+ StableMenuContext
+ );
+ let {
+ menuId,
+ state: { buttonId, isExpanded },
+ } = React.useContext(UnstableMenuContext);
+ let ref = useComposedRefs(buttonRef, forwardedRef);
+ let items = useDescendants(MenuDescendantContext);
+ let firstNonDisabledIndex = React.useMemo(
+ () => items.findIndex((item) => !item.disabled),
+ [items]
+ );
+ React.useEffect(() => {
+ let newButtonId =
+ id != null ? id : menuId ? makeId("menu-button", menuId) : "menu-button";
+ if (buttonId !== newButtonId) {
+ dispatch({
+ type: SET_BUTTON_ID,
+ payload: newButtonId,
+ });
+ }
+ }, [buttonId, dispatch, id, menuId]);
+
+ function handleKeyDown(event: React.KeyboardEvent) {
+ switch (event.key) {
+ case "ArrowDown":
+ case "ArrowUp":
+ event.preventDefault(); // prevent scroll
dispatch({
- type: SET_BUTTON_ID,
- payload: newButtonId,
+ type: OPEN_MENU_AT_INDEX,
+ payload: { index: firstNonDisabledIndex },
});
- }
- }, [buttonId, dispatch, id, menuId]);
-
- function handleKeyDown(event: React.KeyboardEvent) {
- switch (event.key) {
- case "ArrowDown":
- case "ArrowUp":
- event.preventDefault(); // prevent scroll
- dispatch({
- type: OPEN_MENU_AT_INDEX,
- payload: { index: firstNonDisabledIndex },
- });
- break;
- case "Enter":
- case " ":
- dispatch({
- type: OPEN_MENU_AT_INDEX,
- payload: { index: firstNonDisabledIndex },
- });
- break;
- default:
- break;
- }
+ break;
+ case "Enter":
+ case " ":
+ dispatch({
+ type: OPEN_MENU_AT_INDEX,
+ payload: { index: firstNonDisabledIndex },
+ });
+ break;
+ default:
+ break;
}
+ }
- function handleMouseDown(event: React.MouseEvent) {
- if (!isExpanded) {
- buttonClickedRef.current = true;
- }
- if (isRightClick(event.nativeEvent)) {
- return;
- } else if (isExpanded) {
- dispatch({ type: CLOSE_MENU, payload: { buttonRef } });
- } else {
- dispatch({ type: OPEN_MENU_CLEARED });
- }
+ function handleMouseDown(event: React.MouseEvent) {
+ if (!isExpanded) {
+ buttonClickedRef.current = true;
+ }
+ if (isRightClick(event.nativeEvent)) {
+ return;
+ } else if (isExpanded) {
+ dispatch({ type: CLOSE_MENU });
+ } else {
+ dispatch({ type: OPEN_MENU_CLEARED });
}
-
- return (
-
- );
}
-);
+
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"button", MenuButtonProps>;
/**
* @see Docs https://reach.tech/menu-button#menubutton-props
*/
-type MenuButtonProps = {
+interface MenuButtonProps {
/**
* Accepts any renderable content.
*
* @see Docs https://reach.tech/menu-button#menubutton-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
MenuButton.displayName = "MenuButton";
@@ -327,190 +334,192 @@ if (__DEV__) {
*
* MenuItem and MenuLink share most of the same functionality captured here.
*/
-const MenuItemImpl = forwardRefWithAs(
- function MenuItemImpl(
- {
- as: Comp,
- index: indexProp,
- isLink = false,
- onClick,
- onDragStart,
- onMouseDown,
- onMouseEnter,
- onMouseLeave,
- onMouseMove,
- onMouseUp,
- onSelect,
- disabled,
- valueText: valueTextProp,
- ...props
+const MenuItemImpl = React.forwardRef(function MenuItemImpl(
+ {
+ as: Comp = "div",
+ index: indexProp,
+ isLink = false,
+ onClick,
+ onDragStart,
+ onMouseDown,
+ onMouseEnter,
+ onMouseLeave,
+ onMouseMove,
+ onMouseUp,
+ onSelect,
+ disabled,
+ valueText: valueTextProp,
+ ...props
+ },
+ forwardedRef
+) {
+ let {
+ buttonRef,
+ dispatch,
+ readyToSelect,
+ selectCallbacks,
+ } = React.useContext(StableMenuContext);
+ let {
+ state: { selectionIndex, isExpanded },
+ } = React.useContext(UnstableMenuContext);
+ let ownRef = React.useRef(null);
+ // After the ref is mounted to the DOM node, we check to see if we have an
+ // explicit valueText prop before looking for the node's textContent for
+ // typeahead functionality.
+ let [valueText, setValueText] = React.useState(valueTextProp || "");
+
+ let setValueTextFromDOM = React.useCallback(
+ (node: HTMLElement) => {
+ if (!valueTextProp && node?.textContent) {
+ setValueText(node.textContent);
+ }
},
- forwardedRef
- ) {
- let {
- buttonRef,
- dispatch,
- readyToSelect,
- selectCallbacks,
- state: { selectionIndex, isExpanded },
- } = React.useContext(MenuContext);
- let ownRef = React.useRef(null);
- // After the ref is mounted to the DOM node, we check to see if we have an
- // explicit valueText prop before looking for the node's textContent for
- // typeahead functionality.
- let [valueText, setValueText] = React.useState(valueTextProp || "");
- let setValueTextFromDom = React.useCallback(
- (node) => {
- if (node) {
- ownRef.current = node;
- if (
- !valueTextProp ||
- (node.textContent && valueText !== node.textContent)
- ) {
- setValueText(node.textContent);
- }
- }
- },
- [valueText, valueTextProp]
- );
+ [valueTextProp]
+ );
- let ref = useForkedRef(forwardedRef, setValueTextFromDom);
+ let ref = useComposedRefs(forwardedRef, ownRef, setValueTextFromDOM);
- let mouseEventStarted = React.useRef(false);
+ let mouseEventStarted = React.useRef(false);
- let index = useDescendant(
- {
- element: ownRef.current!,
- key: valueText,
- disabled,
- isLink,
- },
- MenuDescendantContext,
- indexProp
- );
- let isSelected = index === selectionIndex && !disabled;
+ let index = useDescendant(
+ {
+ element: ownRef.current!,
+ key: valueText,
+ disabled,
+ isLink,
+ },
+ MenuDescendantContext,
+ indexProp
+ );
+ let isSelected = index === selectionIndex && !disabled;
- // Update the callback ref array on every render
- selectCallbacks.current[index] = onSelect;
+ // Update the callback ref array on every render
+ selectCallbacks.current[index] = onSelect;
- function select() {
- focus(buttonRef.current);
- onSelect && onSelect();
- dispatch({ type: CLICK_MENU_ITEM });
- }
+ function select() {
+ focus(buttonRef.current);
+ onSelect && onSelect();
+ dispatch({ type: CLICK_MENU_ITEM });
+ }
- function handleClick(event: React.MouseEvent) {
- if (isLink && !isRightClick(event.nativeEvent)) {
- if (disabled) {
- event.preventDefault();
- } else {
- select();
- }
+ function handleClick(event: React.MouseEvent) {
+ if (isLink && !isRightClick(event.nativeEvent)) {
+ if (disabled) {
+ event.preventDefault();
+ } else {
+ select();
}
}
+ }
- function handleDragStart(event: React.MouseEvent) {
- // Because we don't preventDefault on mousedown for links (we need the
- // native click event), clicking and holding on a link triggers a
- // dragstart which we don't want.
- if (isLink) {
- event.preventDefault();
- }
+ function handleDragStart(event: React.MouseEvent) {
+ // Because we don't preventDefault on mousedown for links (we need the
+ // native click event), clicking and holding on a link triggers a
+ // dragstart which we don't want.
+ if (isLink) {
+ event.preventDefault();
}
+ }
- function handleMouseDown(event: React.MouseEvent) {
- if (isRightClick(event.nativeEvent)) return;
+ function handleMouseDown(event: React.MouseEvent) {
+ if (isRightClick(event.nativeEvent)) {
+ return;
+ }
- if (isLink) {
- // Signal that the mouse is down so we can react call the right function
- // if the user is clicking on a link.
- mouseEventStarted.current = true;
- } else {
- event.preventDefault();
- }
+ if (isLink) {
+ // Signal that the mouse is down so we can call the right function if the
+ // user is clicking on a link.
+ mouseEventStarted.current = true;
+ } else {
+ event.preventDefault();
}
+ }
- function handleMouseEnter(event: React.MouseEvent) {
- if (!isSelected && index != null && !disabled) {
- dispatch({ type: SELECT_ITEM_AT_INDEX, payload: { index } });
- }
+ function handleMouseEnter(event: React.MouseEvent) {
+ if (!isSelected && index != null && !disabled) {
+ dispatch({ type: SELECT_ITEM_AT_INDEX, payload: { index } });
}
+ }
+
+ function handleMouseLeave(event: React.MouseEvent) {
+ // Clear out selection when mouse over a non-menu item child.
+ dispatch({ type: CLEAR_SELECTION_INDEX });
+ }
- function handleMouseLeave(event: React.MouseEvent) {
- // Clear out selection when mouse over a non-menu item child.
- dispatch({ type: CLEAR_SELECTION_INDEX });
+ function handleMouseMove() {
+ readyToSelect.current = true;
+ if (!isSelected && index != null && !disabled) {
+ dispatch({ type: SELECT_ITEM_AT_INDEX, payload: { index } });
}
+ }
- function handleMouseMove() {
+ function handleMouseUp(event: React.MouseEvent) {
+ if (!readyToSelect.current) {
readyToSelect.current = true;
- if (!isSelected && index != null && !disabled) {
- dispatch({ type: SELECT_ITEM_AT_INDEX, payload: { index } });
- }
+ return;
}
-
- function handleMouseUp(event: React.MouseEvent) {
- if (!readyToSelect.current) {
- readyToSelect.current = true;
- return;
+ if (isRightClick(event.nativeEvent)) return;
+
+ if (isLink) {
+ // If a mousedown event was initiated on a menu link followed by a
+ // mouseup event on the same link, we do nothing; a click event will
+ // come next and handle selection. Otherwise, we trigger a click event.
+ if (mouseEventStarted.current) {
+ mouseEventStarted.current = false;
+ } else if (ownRef.current) {
+ ownRef.current.click();
}
- if (isRightClick(event.nativeEvent)) return;
-
- if (isLink) {
- // If a mousedown event was initiated on a menu link followed by a
- // mouseup event on the same link, we do nothing; a click event will
- // come next and handle selection. Otherwise, we trigger a click event.
- if (mouseEventStarted.current) {
- mouseEventStarted.current = false;
- } else if (ownRef.current) {
- ownRef.current.click();
- }
- } else {
- if (!disabled) {
- select();
- }
+ } else {
+ if (!disabled) {
+ select();
}
}
+ }
- // When the menu closes, reset readyToSelect for the next interaction.
- React.useEffect(() => {
- if (!isExpanded) {
- readyToSelect.current = false;
- }
- }, [isExpanded, readyToSelect]);
+ // When the menu closes, reset readyToSelect for the next interaction.
+ React.useEffect(() => {
+ if (!isExpanded) {
+ readyToSelect.current = false;
+ }
+ }, [isExpanded, readyToSelect]);
- // Any time a mouseup event occurs anywhere in the document, we reset the
- // mouseEventStarted ref so we can check it again when needed.
- React.useEffect(() => {
- let ownerDocument = getOwnerDocument(ownRef.current)!;
- let listener = () => (mouseEventStarted.current = false);
- ownerDocument.addEventListener("mouseup", listener);
- return () => ownerDocument.removeEventListener("mouseup", listener);
- }, []);
-
- return (
-
- );
- }
-);
+ // Any time a mouseup event occurs anywhere in the document, we reset the
+ // mouseEventStarted ref so we can check it again when needed.
+ React.useEffect(() => {
+ let ownerDocument = getOwnerDocument(ownRef.current)!;
+ ownerDocument.addEventListener("mouseup", listener);
+ return () => {
+ ownerDocument.removeEventListener("mouseup", listener);
+ };
-type MenuItemImplProps = {
+ function listener() {
+ mouseEventStarted.current = false;
+ }
+ }, []);
+
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", MenuItemImplProps>;
+
+interface MenuItemImplProps {
/**
* You can put any type of content inside of a `
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", MenuItemsProps>;
/**
* @see Docs https://reach.tech/menu-button#menuitems-props
*/
-type MenuItemsProps = {
+interface MenuItemsProps {
/**
* Can contain only `MenuItem` or a `MenuLink`.
*
* @see Docs https://reach.tech/menu-button#menuitems-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
MenuItems.displayName = "MenuItems";
@@ -776,28 +784,32 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/menu-button#menulink
*/
-const MenuLink = forwardRefWithAs(
- function MenuLink({ as = "a", component, onSelect, ...props }, forwardedRef) {
- if (component) {
- console.warn(
- "[@reach/menu-button]: Please use the `as` prop instead of `component`."
- );
- }
+const MenuLink = React.forwardRef(function MenuLink(
+ {
+ as = "a",
+ // @ts-ignore
+ component,
+ onSelect,
+ ...props
+ },
+ forwardedRef
+) {
+ useDevWarning(
+ !component,
+ "[@reach/menu-button]: Please use the `as` prop instead of `component`"
+ );
- return (
-
-
-
- );
- }
-);
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"a", MenuLinkProps>;
/**
* @see Docs https://reach.tech/menu-button#menulink-props
@@ -810,7 +822,6 @@ if (__DEV__) {
MenuLink.displayName = "MenuLink";
MenuLink.propTypes = {
as: PropTypes.any,
- component: PropTypes.any,
};
}
@@ -824,7 +835,7 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/menu-button#menulist
*/
-const MenuList = forwardRefWithAs(function MenuList(
+const MenuList = React.forwardRef(function MenuList(
{ portal = true, ...props },
forwardedRef
) {
@@ -833,12 +844,12 @@ const MenuList = forwardRefWithAs(function MenuList(
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", MenuListProps>;
/**
* @see Docs https://reach.tech/menu-button#menulist-props
*/
-type MenuListProps = {
+interface MenuListProps {
/**
* Whether or not the popover should be rendered inside a portal. Defaults to
* `true`.
@@ -852,7 +863,7 @@ type MenuListProps = {
* @see Docs https://reach.tech/menu-button#menulist-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
MenuList.displayName = "MenuList";
@@ -864,90 +875,90 @@ if (__DEV__) {
////////////////////////////////////////////////////////////////////////////////
/**
- * MenuPopover
- *
- * A low-level wrapper for the popover that appears when a menu button is open.
- * You can compose it with `MenuItems` for more control over the nested
- * components and their rendered DOM nodes, or if you need to nest arbitrary
- * components between the outer wrapper and your list.
- *
- * @see Docs https://reach.tech/menu-button#menupopover
- */
-const MenuPopover = forwardRefWithAs(
- function MenuPopover(
- { as: Comp = "div", children, portal = true, position, ...props },
- forwardedRef
- ) {
- const {
- buttonRef,
- buttonClickedRef,
- dispatch,
- menuRef,
- popoverRef,
- state: { isExpanded },
- } = React.useContext(MenuContext);
+ *
+
+ *
+ * A low-level wrapper for the popover that appears when a menu button is open.
+ * You can compose it with `MenuItems` for more control over the nested
+ * components and their rendered DOM nodes, or if you need to nest arbitrary
+ * components between the outer wrapper and your list.
+ *
+ * @see Docs https://reach.tech/menu-button#menupopover
+ */
+const MenuPopover = React.forwardRef(function MenuPopover(
+ { as: Comp = "div", children, onBlur, portal = true, position, ...props },
+ forwardedRef
+) {
+ const {
+ buttonRef,
+ buttonClickedRef,
+ dispatch,
+ menuRef,
+ popoverRef,
+ } = React.useContext(StableMenuContext);
+ const {
+ state: { isExpanded },
+ } = React.useContext(UnstableMenuContext);
- const ref = useForkedRef(popoverRef, forwardedRef);
+ const ref = useComposedRefs(popoverRef, forwardedRef);
- React.useEffect(() => {
- if (!isExpanded) {
- return;
- }
+ React.useEffect(() => {
+ if (!isExpanded) {
+ return;
+ }
- let ownerDocument = getOwnerDocument(popoverRef.current)!;
- function listener(event: MouseEvent | TouchEvent) {
- if (buttonClickedRef.current) {
- buttonClickedRef.current = false;
- } else if (
- !popoverContainsEventTarget(popoverRef.current, event.target)
- ) {
- // We on want to close only if focus rests outside the menu
- dispatch({ type: CLOSE_MENU, payload: { buttonRef } });
- }
+ let ownerDocument = getOwnerDocument(popoverRef.current)!;
+ function listener(event: MouseEvent | TouchEvent) {
+ if (buttonClickedRef.current) {
+ buttonClickedRef.current = false;
+ } else if (
+ !popoverContainsEventTarget(popoverRef.current, event.target)
+ ) {
+ // We on want to close only if focus rests outside the menu
+ dispatch({ type: CLOSE_MENU });
}
- ownerDocument.addEventListener("mousedown", listener);
- // see https://github.com/reach/reach-ui/pull/700#discussion_r530369265
- // ownerDocument.addEventListener("touchstart", listener);
- return () => {
- ownerDocument.removeEventListener("mousedown", listener);
- // ownerDocument.removeEventListener("touchstart", listener);
- };
- }, [
- buttonClickedRef,
- buttonRef,
- dispatch,
- menuRef,
- popoverRef,
- isExpanded,
- ]);
-
- let commonProps = {
- ref,
- // TODO: remove in 1.0
- "data-reach-menu": "",
- "data-reach-menu-popover": "",
- hidden: !isExpanded,
- children,
- ...props,
+ }
+ ownerDocument.addEventListener("mousedown", listener);
+ // see https://github.com/reach/reach-ui/pull/700#discussion_r530369265
+ // ownerDocument.addEventListener("touchstart", listener);
+ return () => {
+ ownerDocument.removeEventListener("mousedown", listener);
+ // ownerDocument.removeEventListener("touchstart", listener);
};
+ }, [buttonClickedRef, buttonRef, dispatch, menuRef, popoverRef, isExpanded]);
+
+ let commonProps = {
+ ref,
+ // TODO: remove in 1.0
+ "data-reach-menu": "",
+ "data-reach-menu-popover": "",
+ hidden: !isExpanded,
+ children,
+ onBlur: composeEventHandlers(onBlur, (event) => {
+ if (event.currentTarget.contains(event.relatedTarget as Node)) {
+ return;
+ }
+ dispatch({ type: CLOSE_MENU });
+ }),
+ ...props,
+ };
- return portal ? (
-
- ) : (
-
- );
- }
-);
+ return portal ? (
+
+ ) : (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", MenuPopoverProps>;
/**
* @see Docs https://reach.tech/menu-button#menupopover-props
*/
-type MenuPopoverProps = {
+interface MenuPopoverProps {
/**
* Must contain a `MenuItems`
*
@@ -971,7 +982,7 @@ type MenuPopoverProps = {
* @see Docs https://reach.tech/menu-button#menupopover-position
*/
position?: Position;
-};
+}
if (__DEV__) {
MenuPopover.displayName = "MenuPopover";
@@ -990,7 +1001,7 @@ if (__DEV__) {
function useMenuButtonContext(): MenuContextValue {
let {
state: { isExpanded },
- } = React.useContext(MenuContext);
+ } = React.useContext(UnstableMenuContext);
return React.useMemo(() => ({ isExpanded }), [isExpanded]);
}
@@ -1018,7 +1029,7 @@ function findItemFromTypeahead(
}
function useMenuItemId(index: number | null) {
- let { menuId } = React.useContext(MenuContext);
+ let { menuId } = React.useContext(UnstableMenuContext);
return index != null && index > -1
? makeId(`option-${index}`, menuId)
: undefined;
@@ -1033,7 +1044,7 @@ interface MenuButtonState {
type MenuButtonAction =
| { type: "CLICK_MENU_ITEM" }
- | { type: "CLOSE_MENU"; payload: { buttonRef: ButtonRef } }
+ | { type: "CLOSE_MENU" }
| { type: "OPEN_MENU_AT_FIRST_ITEM" }
| { type: "OPEN_MENU_AT_INDEX"; payload: { index: number } }
| { type: "OPEN_MENU_CLEARED" }
@@ -1045,10 +1056,6 @@ type MenuButtonAction =
| { type: "SET_BUTTON_ID"; payload: string }
| { type: "SEARCH_FOR_ITEM"; payload: string };
-function isRightClick(nativeEvent: MouseEvent) {
- return nativeEvent.which === 3 || nativeEvent.button === 2;
-}
-
function focus(
element: T | undefined | null
) {
@@ -1131,6 +1138,20 @@ function reducer(
}
}
+function useDevWarning(condition: any, message: string) {
+ if (__DEV__) {
+ /* eslint-disable react-hooks/rules-of-hooks */
+ let messageRef = React.useRef(message);
+ React.useEffect(() => {
+ messageRef.current = message;
+ }, [message]);
+ React.useEffect(() => {
+ warning(condition, messageRef.current);
+ }, [condition]);
+ /* eslint-enable react-hooks/rules-of-hooks */
+ }
+}
+
////////////////////////////////////////////////////////////////////////////////
// Types
@@ -1144,22 +1165,25 @@ type ButtonRef = React.RefObject;
type MenuRef = React.RefObject;
type PopoverRef = React.RefObject;
-interface InternalMenuContextValue {
+interface UnstableMenuContextValue {
+ menuId: string | undefined;
+ state: MenuButtonState;
+}
+
+interface StableMenuContextValue {
buttonRef: ButtonRef;
buttonClickedRef: React.MutableRefObject;
dispatch: React.Dispatch;
- menuId: string | undefined;
menuRef: MenuRef;
popoverRef: PopoverRef;
readyToSelect: React.MutableRefObject;
selectCallbacks: React.MutableRefObject<(() => void)[]>;
- state: MenuButtonState;
}
-type MenuContextValue = {
+interface MenuContextValue {
isExpanded: boolean;
// id: string | undefined;
-};
+}
////////////////////////////////////////////////////////////////////////////////
// Exports
diff --git a/packages/popover/package.json b/packages/popover/package.json
index 61c535e49..fcdc163dc 100644
--- a/packages/popover/package.json
+++ b/packages/popover/package.json
@@ -1,24 +1,27 @@
{
"name": "@reach/popover",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Render a portal positioned relative to another element.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/popover"
},
"dependencies": {
- "@reach/portal": "0.13.1",
- "@reach/rect": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/portal": "0.15.0",
+ "@reach/rect": "0.15.0",
+ "@reach/utils": "0.15.0",
"tabbable": "^4.0.0",
- "tslib": "^2.0.0"
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/popover/src/index.tsx b/packages/popover/src/index.tsx
index 2406c637a..8290c29ac 100644
--- a/packages/popover/src/index.tsx
+++ b/packages/popover/src/index.tsx
@@ -5,28 +5,28 @@
import * as React from "react";
import { Portal } from "@reach/portal";
import { useRect, PRect } from "@reach/rect";
-import { forwardRefWithAs, getOwnerDocument, useForkedRef } from "@reach/utils";
+import { getOwnerDocument } from "@reach/utils/owner-document";
+import { useComposedRefs } from "@reach/utils/compose-refs";
import tabbable from "tabbable";
+import type * as Polymorphic from "@reach/utils/polymorphic";
+
////////////////////////////////////////////////////////////////////////////////
/**
* Popover
*/
-const Popover = forwardRefWithAs(function Popover(
- props,
- ref
-) {
+const Popover = React.forwardRef(function Popover(props, ref) {
return (
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", PopoverProps>;
-type PopoverProps = {
+interface PopoverProps {
children: React.ReactNode;
- targetRef: React.RefObject;
+ targetRef: React.RefObject;
position?: Position;
/**
* Render the popover markup, but hide it – used by MenuButton so that it
@@ -43,7 +43,7 @@ type PopoverProps = {
* anywhere in public yet!
*/
unstable_observableRefs?: React.RefObject[];
-};
+}
if (__DEV__) {
Popover.displayName = "Popover";
@@ -57,7 +57,7 @@ if (__DEV__) {
* Popover is conditionally rendered so we can't start measuring until it shows
* up, so useRect needs to live down here not up in Popover
*/
-const PopoverImpl = forwardRefWithAs(function PopoverImpl(
+const PopoverImpl = React.forwardRef(function PopoverImpl(
{
as: Comp = "div",
targetRef,
@@ -70,9 +70,9 @@ const PopoverImpl = forwardRefWithAs(function PopoverImpl(
const popoverRef = React.useRef(null);
const popoverRect = useRect(popoverRef, { observe: !props.hidden });
const targetRect = useRect(targetRef, { observe: !props.hidden });
- const ref = useForkedRef(popoverRef, forwardedRef);
+ const ref = useComposedRefs(popoverRef, forwardedRef);
- useSimulateTabNavigationForReactTree(targetRef, popoverRef);
+ useSimulateTabNavigationForReactTree(targetRef as any, popoverRef);
return (
(function PopoverImpl(
}}
/>
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", PopoverProps>;
if (__DEV__) {
PopoverImpl.displayName = "PopoverImpl";
@@ -114,10 +114,13 @@ function getStyles(
: { visibility: "hidden" };
}
-function getTopPosition(targetRect: PRect, popoverRect: PRect) {
- const { directionUp } = getCollisions(targetRect, popoverRect);
+function getTopPosition(
+ targetRect: PRect,
+ popoverRect: PRect,
+ isDirectionUp: boolean
+) {
return {
- top: directionUp
+ top: isDirectionUp
? `${targetRect.top - popoverRect.height + window.pageYOffset}px`
: `${targetRect.top + targetRect.height + window.pageYOffset}px`,
};
@@ -128,12 +131,15 @@ const positionDefault: Position = (targetRect, popoverRect) => {
return {};
}
- const { directionRight } = getCollisions(targetRect, popoverRect);
+ const { directionRight, directionUp } = getCollisions(
+ targetRect,
+ popoverRect
+ );
return {
left: directionRight
? `${targetRect.right - popoverRect.width + window.pageXOffset}px`
: `${targetRect.left + window.pageXOffset}px`,
- ...getTopPosition(targetRect, popoverRect),
+ ...getTopPosition(targetRect, popoverRect, directionUp),
};
};
@@ -142,12 +148,12 @@ const positionRight: Position = (targetRect, popoverRect) => {
return {};
}
- const { directionLeft } = getCollisions(targetRect, popoverRect);
+ const { directionLeft, directionUp } = getCollisions(targetRect, popoverRect);
return {
left: directionLeft
? `${targetRect.left + window.pageXOffset}px`
: `${targetRect.right - popoverRect.width + window.pageXOffset}px`,
- ...getTopPosition(targetRect, popoverRect),
+ ...getTopPosition(targetRect, popoverRect, directionUp),
};
};
@@ -156,10 +162,11 @@ const positionMatchWidth: Position = (targetRect, popoverRect) => {
return {};
}
+ const { directionUp } = getCollisions(targetRect, popoverRect);
return {
width: targetRect.width,
left: targetRect.left,
- ...getTopPosition(targetRect, popoverRect),
+ ...getTopPosition(targetRect, popoverRect, directionUp),
};
};
diff --git a/packages/portal/package.json b/packages/portal/package.json
index 86019f441..d21614f73 100644
--- a/packages/portal/package.json
+++ b/packages/portal/package.json
@@ -1,21 +1,24 @@
{
"name": "@reach/portal",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Declarative portals for React",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/portal"
},
"dependencies": {
- "@reach/utils": "0.13.1",
- "tslib": "^2.0.0"
+ "@reach/utils": "0.15.0",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/portal/src/index.tsx b/packages/portal/src/index.tsx
index 9fe314242..447634789 100644
--- a/packages/portal/src/index.tsx
+++ b/packages/portal/src/index.tsx
@@ -12,7 +12,8 @@
*/
import * as React from "react";
-import { useIsomorphicLayoutEffect, useForceUpdate } from "@reach/utils";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { useForceUpdate } from "@reach/utils/use-force-update";
import { createPortal } from "react-dom";
/**
@@ -25,7 +26,7 @@ const Portal: React.FC = ({ children, type = "reach-portal" }) => {
let portalNode = React.useRef(null);
let forceUpdate = useForceUpdate();
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
// This ref may be null when a hot-loader replaces components on the page
if (!mountNode.current) return;
// It's possible that the content of the portal has, itself, been portaled.
diff --git a/packages/rect/package.json b/packages/rect/package.json
index e30b3e115..7bc53d4fb 100644
--- a/packages/rect/package.json
+++ b/packages/rect/package.json
@@ -1,9 +1,12 @@
{
"name": "@reach/rect",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Measure React elements position in the DOM",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
@@ -11,13 +14,14 @@
},
"dependencies": {
"@reach/observe-rect": "1.2.0",
- "@reach/utils": "0.13.1",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/rect/src/index.tsx b/packages/rect/src/index.tsx
index b3d0374ff..461f62486 100644
--- a/packages/rect/src/index.tsx
+++ b/packages/rect/src/index.tsx
@@ -11,12 +11,9 @@
import * as React from "react";
import PropTypes from "prop-types";
import observeRect from "@reach/observe-rect";
-import {
- isBoolean,
- isFunction,
- useIsomorphicLayoutEffect as useLayoutEffect,
- warning,
-} from "@reach/utils";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { isBoolean, isFunction } from "@reach/utils/type-check";
+import warning from "tiny-warning";
////////////////////////////////////////////////////////////////////////////////
diff --git a/packages/skip-nav/__tests__/skip-nav.test.tsx b/packages/skip-nav/__tests__/skip-nav.test.tsx
index 06e1fed49..81599ede2 100644
--- a/packages/skip-nav/__tests__/skip-nav.test.tsx
+++ b/packages/skip-nav/__tests__/skip-nav.test.tsx
@@ -5,7 +5,7 @@ import { SkipNavLink, SkipNavContent } from "@reach/skip-nav";
describe("", () => {
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
let { container } = render();
expect(await axe(container)).toHaveNoViolations();
});
diff --git a/packages/skip-nav/package.json b/packages/skip-nav/package.json
index 43148ba5f..2eff83335 100644
--- a/packages/skip-nav/package.json
+++ b/packages/skip-nav/package.json
@@ -1,21 +1,24 @@
{
"name": "@reach/skip-nav",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Skip navigation links for screen reader and keyboard users.",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/skip-nav"
},
"dependencies": {
- "@reach/utils": "0.13.1",
- "tslib": "^2.0.0"
+ "@reach/utils": "0.15.0",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || 17.x",
diff --git a/packages/skip-nav/src/index.tsx b/packages/skip-nav/src/index.tsx
index 7531ed042..d50a85ea2 100644
--- a/packages/skip-nav/src/index.tsx
+++ b/packages/skip-nav/src/index.tsx
@@ -1,5 +1,7 @@
import * as React from "react";
-import { forwardRefWithAs, useCheckStyles } from "@reach/utils";
+import { useCheckStyles } from "@reach/utils/dev-utils";
+
+import type * as Polymorphic from "@reach/utils/polymorphic";
// The user may want to provide their own ID (maybe there are multiple nav
// menus on a page a use might want to skip at various points in tabbing?).
@@ -14,32 +16,30 @@ let defaultId = "reach-skip-nav";
*
* @see Docs https://reach.tech/skip-nav#skipnavlink
*/
-const SkipNavLink = forwardRefWithAs(
- function SkipNavLink(
- { as: Comp = "a", children = "Skip to content", contentId, ...props },
- forwardedRef
- ) {
- let id = contentId || defaultId;
- useCheckStyles("skip-nav");
- return (
-
- {children}
-
- );
- }
-);
+const SkipNavLink = React.forwardRef(function SkipNavLink(
+ { as: Comp = "a", children = "Skip to content", contentId, ...props },
+ forwardedRef
+) {
+ let id = contentId || defaultId;
+ useCheckStyles("skip-nav");
+ return (
+
+ {children}
+
+ );
+}) as Polymorphic.ForwardRefComponent<"a", SkipNavLinkProps>;
/**
* @see Docs https://reach.tech/skip-nav#skipnavlink-props
*/
-type SkipNavLinkProps = {
+interface SkipNavLinkProps {
/**
* Allows you to change the text for your preferred phrase or localization.
*
@@ -53,7 +53,7 @@ type SkipNavLinkProps = {
* @see Docs https://reach.tech/skip-nav#skipnavlink-contentid
*/
contentId?: string;
-};
+}
if (__DEV__) {
SkipNavLink.displayName = "SkipNavLink";
@@ -68,27 +68,25 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/skip-nav#skipnavcontent
*/
-const SkipNavContent = forwardRefWithAs(
- function SkipNavContent(
- { as: Comp = "div", id: idProp, ...props },
- forwardedRef
- ) {
- let id = idProp || defaultId;
- return (
-
- );
- }
-);
+const SkipNavContent = React.forwardRef(function SkipNavContent(
+ { as: Comp = "div", id: idProp, ...props },
+ forwardedRef
+) {
+ let id = idProp || defaultId;
+ return (
+
+ );
+}) as Polymorphic.ForwardRefComponent<"div", SkipNavContentProps>;
/**
* @see Docs https://reach.tech/skip-nav#skipnavcontent-props
*/
-type SkipNavContentProps = {
+interface SkipNavContentProps {
/**
* You can place the `SkipNavContent` element as a sibling to your main
* content or as a wrapper.
@@ -114,7 +112,7 @@ type SkipNavContentProps = {
* @see Docs https://reach.tech/skip-nav#skipnavcontent-id
*/
id?: string;
-};
+}
if (__DEV__) {
SkipNavContent.displayName = "SkipNavContent";
diff --git a/packages/slider/__tests__/slider.test.tsx b/packages/slider/__tests__/slider.test.tsx
index 8fa494a56..40000076a 100644
--- a/packages/slider/__tests__/slider.test.tsx
+++ b/packages/slider/__tests__/slider.test.tsx
@@ -23,7 +23,7 @@ describe("", () => {
describe("rendering", () => {});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
const { container } = render();
const results = await axe(container);
expect(results).toHaveNoViolations();
diff --git a/packages/slider/examples/with-tooltip.example.js b/packages/slider/examples/with-tooltip.example.js
index 855ad4512..f58f7ba0e 100644
--- a/packages/slider/examples/with-tooltip.example.js
+++ b/packages/slider/examples/with-tooltip.example.js
@@ -1,5 +1,5 @@
import * as React from "react";
-import { wrapEvent } from "@reach/utils";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
import { useTooltip, TooltipPopup } from "@reach/tooltip";
import {
SliderInput,
@@ -35,7 +35,7 @@ function Example() {
}, []);
const getEventHandler = (handler) =>
- wrapEvent(preventDefaultWhenFocused, handler);
+ composeEventHandlers(preventDefaultWhenFocused, handler);
return (
diff --git a/packages/slider/package.json b/packages/slider/package.json
index 4cd55b3b4..b1e15dfa5 100644
--- a/packages/slider/package.json
+++ b/packages/slider/package.json
@@ -1,23 +1,27 @@
{
"name": "@reach/slider",
- "version": "0.13.1",
+ "version": "0.15.0",
"description": "Accessible React Slider Component",
"author": "React Training ",
"license": "MIT",
+ "sideEffects": [
+ "*.css"
+ ],
"repository": {
"type": "git",
"url": "git+https://github.com/reach/reach-ui.git",
"directory": "packages/slider"
},
"dependencies": {
- "@reach/auto-id": "0.13.1",
- "@reach/utils": "0.13.1",
+ "@reach/auto-id": "0.15.0",
+ "@reach/utils": "0.15.0",
"prop-types": "^15.7.2",
- "tslib": "^2.0.0"
+ "tiny-warning": "^1.0.3",
+ "tslib": "^2.1.0"
},
"devDependencies": {
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
},
"peerDependencies": {
"react": "^16.9.0 || 17.x",
diff --git a/packages/slider/src/index.tsx b/packages/slider/src/index.tsx
index fd6044468..90c2d5547 100644
--- a/packages/slider/src/index.tsx
+++ b/packages/slider/src/index.tsx
@@ -25,24 +25,24 @@
import * as React from "react";
import PropTypes from "prop-types";
import { useId } from "@reach/auto-id";
+import { useControlledState } from "@reach/utils/use-controlled-state";
+import { isRightClick } from "@reach/utils/is-right-click";
+import { useStableLayoutCallback } from "@reach/utils/use-stable-callback";
+import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils/use-isomorphic-layout-effect";
+import { getOwnerDocument } from "@reach/utils/owner-document";
+import { createNamedContext } from "@reach/utils/context";
+import { isFunction } from "@reach/utils/type-check";
+import { makeId } from "@reach/utils/make-id";
+import { noop } from "@reach/utils/noop";
import {
- createNamedContext,
- forwardRefWithAs,
- getOwnerDocument,
- isFunction,
- isRightClick,
- makeId,
- memoWithAs,
- noop,
useCheckStyles,
- useControlledState,
useControlledSwitchWarning,
- useEventCallback,
- useForkedRef,
- useIsomorphicLayoutEffect,
- warning,
- wrapEvent,
-} from "@reach/utils";
+} from "@reach/utils/dev-utils";
+import { useComposedRefs } from "@reach/utils/compose-refs";
+import { composeEventHandlers } from "@reach/utils/compose-event-handlers";
+import warning from "tiny-warning";
+
+import type * as Polymorphic from "@reach/utils/polymorphic";
// TODO: Remove in 1.0
type SliderAlignment = "center" | "contain";
@@ -105,7 +105,7 @@ const sliderPropTypes = {
*
* @see Docs https://reach.tech/slider#slider
*/
-const Slider = forwardRefWithAs(function Slider(
+const Slider = React.forwardRef(function Slider(
{ children, ...props },
forwardedRef
) {
@@ -114,7 +114,7 @@ const Slider = forwardRefWithAs(function Slider(
{...props}
ref={forwardedRef}
data-reach-slider=""
- _componentName="Slider"
+ __componentName="Slider"
>
@@ -123,12 +123,12 @@ const Slider = forwardRefWithAs(function Slider(
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", SliderProps>;
/**
* @see Docs https://reach.tech/slider#slider-props
*/
-type SliderProps = {
+interface SliderProps {
/**
* `Slider` can accept `SliderMarker` children to enhance display of specific
* values along the track.
@@ -244,7 +244,7 @@ type SliderProps = {
* @see Docs https://reach.tech/slider#slider-step
*/
step?: number;
-};
+}
if (__DEV__) {
Slider.displayName = "Slider";
@@ -265,9 +265,7 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/slider#sliderinput
*/
-const SliderInput = forwardRefWithAs<
- SliderInputProps & { _componentName?: string }
->(function SliderInput(
+const SliderInput = React.forwardRef(function SliderInput(
{
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledBy,
@@ -296,12 +294,12 @@ const SliderInput = forwardRefWithAs<
orientation = SliderOrientation.Horizontal,
step = 1,
children,
- _componentName = "SliderInput",
+ __componentName = "SliderInput",
...rest
},
forwardedRef
) {
- useControlledSwitchWarning(controlledValue, "value", _componentName);
+ useControlledSwitchWarning(controlledValue, "value", __componentName);
warning(
!DEPRECATED_getValueText,
@@ -318,7 +316,7 @@ const SliderInput = forwardRefWithAs<
let trackRef: TrackRef = React.useRef(null);
let handleRef: HandleRef = React.useRef(null);
let sliderRef: SliderRef = React.useRef(null);
- let ref = useForkedRef(sliderRef, forwardedRef);
+ let ref = useComposedRefs(sliderRef, forwardedRef);
let [hasFocus, setHasFocus] = React.useState(false);
@@ -345,12 +343,12 @@ const SliderInput = forwardRefWithAs<
: `${handleSize}px * ${trackPercent * 0.01}`
})`;
let handlePositionRef = React.useRef(handlePosition);
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
handlePositionRef.current = handlePosition;
}, [handlePosition]);
let onChangeRef = React.useRef(onChange);
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
let updateValue = React.useCallback(
@@ -383,7 +381,7 @@ const SliderInput = forwardRefWithAs<
);
// https://www.w3.org/TR/wai-aria-practices-1.2/#slider_kbd_interaction
- let handleKeyDown = useEventCallback((event: React.KeyboardEvent) => {
+ let handleKeyDown = useStableLayoutCallback((event: React.KeyboardEvent) => {
if (disabled) {
return;
}
@@ -487,7 +485,7 @@ const SliderInput = forwardRefWithAs<
onPointerDown,
onPointerUp,
});
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
appEvents.current.onMouseMove = onMouseMove;
appEvents.current.onMouseDown = onMouseDown;
appEvents.current.onMouseUp = onMouseUp;
@@ -498,7 +496,7 @@ const SliderInput = forwardRefWithAs<
appEvents.current.onPointerUp = onPointerUp;
}, [onMouseMove, onMouseDown, onMouseUp, onTouchStart, onTouchEnd, onTouchMove, onPointerDown, onPointerUp]);
- let handleSlideStart = useEventCallback((event: SomePointerEvent) => {
+ let handleSlideStart = useStableLayoutCallback((event: SomePointerEvent) => {
if (isRightClick(event)) return;
if (disabled) {
@@ -530,7 +528,7 @@ const SliderInput = forwardRefWithAs<
removeEndEvents.current = addEndListener();
});
- let setPointerCapture = useEventCallback((event: PointerEvent) => {
+ let setPointerCapture = useStableLayoutCallback((event: PointerEvent) => {
if (isRightClick(event)) return;
if (disabled) {
pointerDownRef.current = false;
@@ -540,13 +538,13 @@ const SliderInput = forwardRefWithAs<
sliderRef.current?.setPointerCapture(event.pointerId);
});
- let releasePointerCapture = useEventCallback((event: PointerEvent) => {
+ let releasePointerCapture = useStableLayoutCallback((event: PointerEvent) => {
if (isRightClick(event)) return;
sliderRef.current?.releasePointerCapture(event.pointerId);
pointerDownRef.current = false;
});
- let handlePointerMove = useEventCallback((event: SomePointerEvent) => {
+ let handlePointerMove = useStableLayoutCallback((event: SomePointerEvent) => {
if (disabled || !pointerDownRef.current) {
pointerDownRef.current = false;
return;
@@ -559,7 +557,7 @@ const SliderInput = forwardRefWithAs<
updateValue(newValue);
});
- let handleSlideStop = useEventCallback((event: SomePointerEvent) => {
+ let handleSlideStop = useStableLayoutCallback((event: SomePointerEvent) => {
if (isRightClick(event)) return;
pointerDownRef.current = false;
@@ -577,11 +575,11 @@ const SliderInput = forwardRefWithAs<
let addMoveListener = React.useCallback(() => {
let ownerDocument = getOwnerDocument(sliderRef.current)!;
- let touchListener = wrapEvent(
+ let touchListener = composeEventHandlers(
appEvents.current.onTouchMove,
handlePointerMove
);
- let mouseListener = wrapEvent(
+ let mouseListener = composeEventHandlers(
appEvents.current.onMouseMove,
handlePointerMove
);
@@ -596,15 +594,18 @@ const SliderInput = forwardRefWithAs<
let addEndListener = React.useCallback(() => {
let ownerDocument = getOwnerDocument(sliderRef.current)!;
let ownerWindow = ownerDocument.defaultView || window;
- let pointerListener = wrapEvent(
+ let pointerListener = composeEventHandlers(
appEvents.current.onPointerUp,
releasePointerCapture
);
- let touchListener = wrapEvent(
+ let touchListener = composeEventHandlers(
appEvents.current.onTouchEnd,
handleSlideStop
);
- let mouseListener = wrapEvent(appEvents.current.onMouseUp, handleSlideStop);
+ let mouseListener = composeEventHandlers(
+ appEvents.current.onMouseUp,
+ handleSlideStop
+ );
if ("PointerEvent" in ownerWindow) {
ownerDocument.addEventListener("pointerup", pointerListener);
}
@@ -630,15 +631,15 @@ const SliderInput = forwardRefWithAs<
let ownerDocument = getOwnerDocument(sliderElement)!;
let ownerWindow = ownerDocument.defaultView || window;
- let touchListener = wrapEvent(
+ let touchListener = composeEventHandlers(
appEvents.current.onTouchStart,
handleSlideStart
);
- let mouseListener = wrapEvent(
+ let mouseListener = composeEventHandlers(
appEvents.current.onMouseDown,
handleSlideStart
);
- let pointerListener = wrapEvent(
+ let pointerListener = composeEventHandlers(
appEvents.current.onPointerDown,
setPointerCapture
);
@@ -704,7 +705,10 @@ const SliderInput = forwardRefWithAs<
);
-});
+}) as Polymorphic.ForwardRefComponent<
+ "div",
+ SliderInputProps & { __componentName?: string }
+>;
/**
* @see Docs https://reach.tech/slider#sliderinput-props
@@ -736,12 +740,12 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/slider#slidertrack
*/
-const SliderTrackImpl = forwardRefWithAs(function SliderTrack(
+const SliderTrackImpl = React.forwardRef(function SliderTrack(
{ as: Comp = "div", children, style = {}, ...props },
forwardedRef
) {
const { disabled, orientation, trackRef } = useSliderContext();
- const ref = useForkedRef(trackRef, forwardedRef);
+ const ref = useComposedRefs(trackRef, forwardedRef);
return (
(function SliderTrack(
{children}
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", SliderTrackProps>;
if (__DEV__) {
SliderTrackImpl.displayName = "SliderTrack";
@@ -764,12 +768,15 @@ if (__DEV__) {
};
}
-const SliderTrack = memoWithAs(SliderTrackImpl);
+const SliderTrack = React.memo(SliderTrackImpl) as Polymorphic.MemoComponent<
+ "div",
+ SliderTrackProps
+>;
/**
* @see Docs https://reach.tech/slider#slidertrack-props
*/
-type SliderTrackProps = {
+interface SliderTrackProps {
/**
* `SliderTrack` expects ``, at minimum, for the Slider to
* function. All other Slider subcomponents should be passed as children
@@ -778,7 +785,7 @@ type SliderTrackProps = {
* @see Docs https://reach.tech/slider#slidertrack-children
*/
children: React.ReactNode;
-};
+}
if (__DEV__) {
SliderTrack.displayName = "SliderTrack";
@@ -794,7 +801,7 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/slider#sliderrange
*/
-const SliderRangeImpl = forwardRefWithAs(function SliderRange(
+const SliderRangeImpl = React.forwardRef(function SliderRange(
{ as: Comp = "div", children, style = {}, ...props },
forwardedRef
) {
@@ -809,17 +816,20 @@ const SliderRangeImpl = forwardRefWithAs(function SliderRange(
data-orientation={orientation}
/>
);
-});
+}) as Polymorphic.ForwardRefComponent<"div", SliderRangeProps>;
if (__DEV__) {
SliderRangeImpl.displayName = "SliderRange";
SliderRangeImpl.propTypes = {};
}
-const SliderRange = memoWithAs(SliderRangeImpl);
+const SliderRange = React.memo(SliderRangeImpl) as Polymorphic.MemoComponent<
+ "div",
+ SliderRangeProps
+>;
// TODO: Remove in 1.0
-const SliderTrackHighlightImpl = forwardRefWithAs(
+const SliderTrackHighlightImpl = React.forwardRef(
function SliderTrackHighlightImpl(props, ref) {
if (__DEV__) {
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -838,11 +848,13 @@ const SliderTrackHighlightImpl = forwardRefWithAs(
/>
);
}
-);
+) as Polymorphic.ForwardRefComponent<"div", SliderRangeProps>;
+
if (__DEV__) {
SliderTrackHighlightImpl.displayName = "SliderTrackHighlight";
SliderTrackHighlightImpl.propTypes = SliderRangeImpl.propTypes;
}
+
export interface SliderTrackHighlightProps extends SliderRangeProps {}
/**
@@ -852,7 +864,9 @@ export interface SliderTrackHighlightProps extends SliderRangeProps {}
*
* @alias SliderRange
*/
-export const SliderTrackHighlight = memoWithAs(SliderTrackHighlightImpl);
+export const SliderTrackHighlight = React.memo(
+ SliderTrackHighlightImpl
+) as Polymorphic.MemoComponent<"div", SliderRangeProps>;
/**
* `SliderRange` accepts any props that a HTML div component accepts.
@@ -860,7 +874,7 @@ export const SliderTrackHighlight = memoWithAs(SliderTrackHighlightImpl);
*
* @see Docs https://reach.tech/slider#sliderrange-props
*/
-type SliderRangeProps = {};
+interface SliderRangeProps {}
if (__DEV__) {
SliderRange.displayName = "SliderRange";
@@ -875,110 +889,109 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/slider#sliderhandle
*/
-const SliderHandleImpl = forwardRefWithAs(
- function SliderHandle(
- {
- // min,
- // max,
- as: Comp = "div",
- onBlur,
- onFocus,
- style = {},
- onKeyDown,
- ...props
- },
- forwardedRef
- ) {
- const {
- ariaLabel,
- ariaLabelledBy,
- ariaValueText,
- disabled,
- handlePosition,
- handleRef,
- isVertical,
- handleKeyDown,
- orientation,
- setHasFocus,
- sliderMin,
- sliderMax,
- value,
- } = useSliderContext();
-
- const ref = useForkedRef(handleRef, forwardedRef);
+const SliderHandleImpl = React.forwardRef(function SliderHandle(
+ {
+ // min,
+ // max,
+ as: Comp = "div",
+ onBlur,
+ onFocus,
+ style = {},
+ onKeyDown,
+ ...props
+ },
+ forwardedRef
+) {
+ const {
+ ariaLabel,
+ ariaLabelledBy,
+ ariaValueText,
+ disabled,
+ handlePosition,
+ handleRef,
+ isVertical,
+ handleKeyDown,
+ orientation,
+ setHasFocus,
+ sliderMin,
+ sliderMax,
+ value,
+ } = useSliderContext();
- return (
- {
- setHasFocus(false);
- })}
- onFocus={wrapEvent(onFocus, () => {
- setHasFocus(true);
- })}
- onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
- style={{
- position: "absolute",
- ...(isVertical
- ? { bottom: handlePosition }
- : { left: handlePosition }),
- ...style,
- }}
- />
- );
- }
-);
+ const ref = useComposedRefs(handleRef, forwardedRef);
+
+ return (
+ {
+ setHasFocus(false);
+ })}
+ onFocus={composeEventHandlers(onFocus, () => {
+ setHasFocus(true);
+ })}
+ onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
+ style={{
+ position: "absolute",
+ ...(isVertical ? { bottom: handlePosition } : { left: handlePosition }),
+ ...style,
+ }}
+ />
+ );
+}) as Polymorphic.ForwardRefComponent<"div", SliderHandleProps>;
if (__DEV__) {
SliderHandleImpl.displayName = "SliderHandle";
SliderHandleImpl.propTypes = {};
}
-const SliderHandle = memoWithAs(SliderHandleImpl);
+const SliderHandle = React.memo(SliderHandleImpl) as Polymorphic.MemoComponent<
+ "div",
+ SliderHandleProps
+>;
/**
* `SliderRange` accepts any props that a HTML div component accepts.
*
* @see Docs https://reach.tech/slider#sliderhandle-props
*/
-type SliderHandleProps = {};
+interface SliderHandleProps {}
if (__DEV__) {
SliderHandle.displayName = "SliderHandle";
@@ -994,55 +1007,49 @@ if (__DEV__) {
*
* @see Docs https://reach.tech/slider#slidermarker
*/
-const SliderMarkerImpl = forwardRefWithAs(
- function SliderMarker(
- { as: Comp = "div", children, style = {}, value, ...props },
- forwardedRef
- ) {
- const {
- disabled,
- isVertical,
- orientation,
- sliderMin,
- sliderMax,
- value: sliderValue,
- } = useSliderContext();
-
- let inRange = !(value < sliderMin || value > sliderMax);
- let absoluteStartPosition = `${valueToPercent(
- value,
- sliderMin,
- sliderMax
- )}%`;
-
- let state =
- value < sliderValue
- ? "under-value"
- : value === sliderValue
- ? "at-value"
- : "over-value";
-
- return inRange ? (
-
- ) : null;
- }
-);
+const SliderMarkerImpl = React.forwardRef(function SliderMarker(
+ { as: Comp = "div", children, style = {}, value, ...props },
+ forwardedRef
+) {
+ const {
+ disabled,
+ isVertical,
+ orientation,
+ sliderMin,
+ sliderMax,
+ value: sliderValue,
+ } = useSliderContext();
+
+ let inRange = !(value < sliderMin || value > sliderMax);
+ let absoluteStartPosition = `${valueToPercent(value, sliderMin, sliderMax)}%`;
+
+ let state =
+ value < sliderValue
+ ? "under-value"
+ : value === sliderValue
+ ? "at-value"
+ : "over-value";
+
+ return inRange ? (
+
+ ) : null;
+}) as Polymorphic.ForwardRefComponent<"div", SliderMarkerProps>;
if (__DEV__) {
SliderMarkerImpl.displayName = "SliderMarker";
@@ -1051,19 +1058,22 @@ if (__DEV__) {
};
}
-const SliderMarker = memoWithAs(SliderMarkerImpl);
+const SliderMarker = React.memo(SliderMarkerImpl) as Polymorphic.MemoComponent<
+ "div",
+ SliderMarkerProps
+>;
/**
* @see Docs https://reach.tech/slider#slidermarker-props
*/
-type SliderMarkerProps = {
+interface SliderMarkerProps {
/**
* The value to denote where the marker should appear along the track.
*
* @see Docs https://reach.tech/slider#slidermarker-value
*/
value: number;
-};
+}
if (__DEV__) {
SliderMarker.displayName = "SliderMarker";
@@ -1175,7 +1185,7 @@ function useDimensions(ref: React.RefObject) {
? ref.current.getBoundingClientRect()
: 0; */
- useIsomorphicLayoutEffect(() => {
+ useLayoutEffect(() => {
let ownerDocument = getOwnerDocument(ref.current)!;
let ownerWindow = ownerDocument.defaultView || window;
if (ref.current) {
diff --git a/packages/tabs/__tests__/tabs.test.tsx b/packages/tabs/__tests__/tabs.test.tsx
index 8b9760aca..b3bbc67ab 100644
--- a/packages/tabs/__tests__/tabs.test.tsx
+++ b/packages/tabs/__tests__/tabs.test.tsx
@@ -355,7 +355,7 @@ describe("", () => {
});
describe("a11y", () => {
- it("should not have basic a11y issues", async () => {
+ it("Should not have ARIA violations", async () => {
const { container } = render(