diff --git a/.changeset/grumpy-news-happen.md b/.changeset/grumpy-news-happen.md new file mode 100644 index 000000000000..7172ba997e30 --- /dev/null +++ b/.changeset/grumpy-news-happen.md @@ -0,0 +1,5 @@ +--- +'@ag.ds-next/react': minor +--- + +Removed dependency to Reach UI as it is no longer being maintained. The logic from `@reach/auto-id` has been forked and copied into the repo. \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json index 566046b79847..9cbcd2fd8e78 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -7,7 +7,6 @@ "dependencies": { "@babel/runtime": "^7.4.5", "@popperjs/core": "^2.11.4", - "@reach/auto-id": "^0.18.0", "@react-spring/web": "^9.4.5", "date-fns": "^2.28.0", "downshift": "^6.1.7", diff --git a/packages/react/src/core/utils/useId.ts b/packages/react/src/core/utils/useId.ts deleted file mode 100644 index d309d1a06964..000000000000 --- a/packages/react/src/core/utils/useId.ts +++ /dev/null @@ -1 +0,0 @@ -export { useId } from '@reach/auto-id'; diff --git a/packages/react/src/core/utils/useId/canUseDom.ts b/packages/react/src/core/utils/useId/canUseDom.ts new file mode 100644 index 000000000000..9f69e308abc8 --- /dev/null +++ b/packages/react/src/core/utils/useId/canUseDom.ts @@ -0,0 +1,9 @@ +// The contents of this file has been copied from https://github.com/reach/reach-ui/blob/dev/packages/utils/src/can-use-dom.ts + +export function canUseDOM() { + return !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement + ); +} diff --git a/packages/react/src/core/utils/useId/index.ts b/packages/react/src/core/utils/useId/index.ts new file mode 100644 index 000000000000..d9387a007fe9 --- /dev/null +++ b/packages/react/src/core/utils/useId/index.ts @@ -0,0 +1 @@ +export { useId } from './useId'; diff --git a/packages/react/src/core/utils/useId/useId.test.tsx b/packages/react/src/core/utils/useId/useId.test.tsx new file mode 100644 index 000000000000..449419bfa161 --- /dev/null +++ b/packages/react/src/core/utils/useId/useId.test.tsx @@ -0,0 +1,35 @@ +// The contents of this file has been copied from https://github.com/reach/reach-ui/blob/dev/packages/auto-id/__tests__/auto-id.test.tsx + +import { render, cleanup } from '../../../../../../test-utils'; +import { useId } from './useId'; + +afterEach(cleanup); + +describe('useId', () => { + it('should generate a unique ID value', () => { + function Comp() { + const justNull = null; + const randId = useId(justNull); + const randId2 = useId(); + return ( +
+
Wow
+
Ok
+
+ ); + } + const { getByText } = render(); + const id1 = getByText('Wow').id; + const id2 = getByText('Ok').id; + expect(id2).not.toEqual(id1); + }); + + it('uses a fallback ID', () => { + function Comp() { + const newId = useId('awesome'); + return
Ok
; + } + const { getByText } = render(); + expect(getByText('Ok').id).toEqual('awesome'); + }); +}); diff --git a/packages/react/src/core/utils/useId/useId.ts b/packages/react/src/core/utils/useId/useId.ts new file mode 100644 index 000000000000..b1ef0ede267f --- /dev/null +++ b/packages/react/src/core/utils/useId/useId.ts @@ -0,0 +1,139 @@ +/** + * This file use to just be a simple re-export of `useId` from @reach/auto-id + * We can not just use `useId` from React as we need to provide support for React 16, 17 and 18 + * + * As Reach is no longer being maintained and does not support React 18, the contents of this file have been copied from: + * https://github.com/reach/reach-ui/blob/dev/packages/auto-id/src/reach-auto-id.ts + * + * See Github issue: https://github.com/reach/reach-ui/issues/972 + */ + +/** + * Welcome to @reach/auto-id! + * Let's see if we can make sense of why this hook exists and its + * implementation. + * + * Some background: + * 1. Accessibility APIs rely heavily on element IDs + * 2. Requiring developers to put IDs on every element in Reach UI is both + * cumbersome and error-prone + * 3. With a component model, we can generate IDs for them! + * + * Solution 1: Generate random IDs. + * + * This works great as long as you don't server render your app. When React (in + * the client) tries to reuse the markup from the server, the IDs won't match + * and React will then recreate the entire DOM tree. + * + * Solution 2: Increment an integer + * + * This sounds great. Since we're rendering the exact same tree on the server + * and client, we can increment a counter and get a deterministic result between + * client and server. Also, JS integers can go up to nine-quadrillion. I'm + * pretty sure the tab will be closed before an app never needs + * 10 quadrillion IDs! + * + * Problem solved, right? + * + * Ah, but there's a catch! React's concurrent rendering makes this approach + * non-deterministic. While the client and server will end up with the same + * elements in the end, depending on suspense boundaries (and possibly some user + * input during the initial render) the incrementing integers won't always match + * up. + * + * Solution 3: Don't use IDs at all on the server; patch after first render. + * + * What we've done here is solution 2 with some tricks. With this approach, the + * ID returned is an empty string on the first render. This way the server and + * client have the same markup no matter how wild the concurrent rendering may + * have gotten. + * + * After the render, we patch up the components with an incremented ID. This + * causes a double render on any components with `useId`. Shouldn't be a problem + * since the components using this hook should be small, and we're only updating + * the ID attribute on the DOM, nothing big is happening. + * + * It doesn't have to be an incremented number, though--we could do generate + * random strings instead, but incrementing a number is probably the cheapest + * thing we can do. + * + * Additionally, we only do this patchup on the very first client render ever. + * Any calls to `useId` that happen dynamically in the client will be + * populated immediately with a value. So, we only get the double render after + * server hydration and never again, SO BACK OFF ALRIGHT? + */ + +/* eslint-disable react-hooks/rules-of-hooks */ + +import * as React from 'react'; +import { useIsomorphicLayoutEffect as useLayoutEffect } from './useIsomorphicLayoutEffect'; + +let serverHandoffComplete = false; +let id = 0; +function genId() { + return ++id; +} + +// Workaround for https://github.com/webpack/webpack/issues/14814 +// https://github.com/eps1lon/material-ui/blob/8d5f135b4d7a58253a99ab56dce4ac8de61f5dc1/packages/mui-utils/src/useId.ts#L21 +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const maybeReactUseId: undefined | (() => string) = (React as any)[ + 'useId'.toString() +]; + +/** + * useId + * + * Autogenerate IDs to facilitate WAI-ARIA and server rendering. + * + * Note: The returned ID will initially be `null` and will update after a + * component mounts. Users may need to supply their own ID if they need + * consistent values for SSR. + * + * @see Docs https://reach.tech/auto-id + */ +function useId(idFromProps: string): string; +function useId(idFromProps: number): number; +function useId(idFromProps: string | number): string | number; +function useId(idFromProps: string | undefined | null): string | undefined; +function useId(idFromProps: number | undefined | null): number | undefined; +function useId( + idFromProps: string | number | undefined | null +): string | number | undefined; +function useId(): string | undefined; + +function useId(providedId?: number | string | undefined | null) { + if (maybeReactUseId !== undefined) { + const generatedId = maybeReactUseId(); + return providedId ?? generatedId; + } + + // If this instance isn't part of the initial render, we don't have to do the + // double render/patch-up dance. We can just generate the ID and return it. + const initialId = providedId ?? (serverHandoffComplete ? genId() : null); + const [id, setId] = React.useState(initialId); + + useLayoutEffect(() => { + if (id === null) { + // Patch the ID after render. We do this in `useLayoutEffect` to avoid any + // rendering flicker, though it'll make the first render slower (unlikely + // to matter, but you're welcome to measure your app and let us know if + // it's a problem). + setId(genId()); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + if (serverHandoffComplete === false) { + // Flag all future uses of `useId` to skip the update dance. This is in + // `useEffect` because it goes after `useLayoutEffect`, ensuring we don't + // accidentally bail out of the patch-up dance prematurely. + serverHandoffComplete = true; + } + }, []); + + return providedId ?? id ?? undefined; +} + +export { useId }; diff --git a/packages/react/src/core/utils/useId/useIsomorphicLayoutEffect.ts b/packages/react/src/core/utils/useId/useIsomorphicLayoutEffect.ts new file mode 100644 index 000000000000..400084db265b --- /dev/null +++ b/packages/react/src/core/utils/useId/useIsomorphicLayoutEffect.ts @@ -0,0 +1,32 @@ +// The contents of this file has been copied from https://github.com/reach/reach-ui/blob/dev/packages/utils/src/use-isomorphic-layout-effect.ts + +import { useEffect, useLayoutEffect } from 'react'; +import { canUseDOM } from './canUseDom'; + +/** + * React currently throws a warning when using useLayoutEffect on the server. To + * get around it, we can conditionally useEffect on the server (no-op) and + * useLayoutEffect in the browser. We occasionally need useLayoutEffect to + * ensure we don't get a render flash for certain operations, but we may also + * need affected components to render on the server. One example is when setting + * a component's descendants to retrieve their index values. + * + * Important to note that using this hook as an escape hatch will break the + * eslint dependency warnings unless you rename the import to `useLayoutEffect`. + * Use sparingly only when the effect won't effect the rendered HTML to avoid + * any server/client mismatch. + * + * If a useLayoutEffect is needed and the result would create a mismatch, it's + * likely that the component in question shouldn't be rendered on the server at + * all, so a better approach would be to lazily render those in a parent + * component after client-side hydration. + * + * https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 + * https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.js + * + * @param effect + * @param deps + */ +export const useIsomorphicLayoutEffect = canUseDOM() + ? useLayoutEffect + : useEffect; diff --git a/yarn.lock b/yarn.lock index f64447af6b5c..24ec50c2e861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2253,18 +2253,6 @@ resolved "https://registry.yarnpkg.com/@preconstruct/next/-/next-4.0.0.tgz#4d9c64ed68bb7cdc72d35d79d1dbe26ba2cae61e" integrity sha512-vSrc8wFQgBErU7dKTKSQtr/DLWPHcN9jMoiWOAQodB1+B4Kpqqry6QhGYoRm0DQU5gNL+Rcp+Xb350O1E/gjsg== -"@reach/auto-id@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.18.0.tgz#4b97085cd1cf1360a9bedc6e9c78e97824014f0d" - integrity sha512-XwY1IwhM7mkHZFghhjiqjQ6dstbOdpbFLdggeke75u8/8icT8uEHLbovFUgzKjy9qPvYwZIB87rLiR8WdtOXCg== - dependencies: - "@reach/utils" "0.18.0" - -"@reach/utils@0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" - integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A== - "@react-spring/animated@~9.4.5": version "9.4.5" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.4.5.tgz#dd9921c716a4f4a3ed29491e0c0c9f8ca0eb1a54"