From 492cbdc609b1c2f318bc432e49f7926419988bc2 Mon Sep 17 00:00:00 2001 From: Haz Date: Sat, 13 May 2023 08:36:47 +0200 Subject: [PATCH] Use Floating UI within the component instead of store (#2279) Closes #887 Closes #1595 Closes #2228 The primary purpose of this PR is to move the `@floating-ui/dom` dependency from the `usePopoverStore` hook to the `Popover` component so it can be lazy loaded. In addition, we moved most of the Floating UI-related props to the `Popover` component, which was the original idea, but wasn't viable before without hacks. The only exception is the `placement` prop, to which we need access on components other than `Popover`. This introduces breaking changes on `Popover` and derived components. Those are described in the changesets. --- Finally, the `Tooltip` component has been updated, so it composes `Hovercard` now, which also benefited from the `Popover` changes. The specific `Tooltip` updates are related to the closed issues above. --- .changeset/__breaking-1.md | 57 +++ .changeset/curvy-taxis-shave-2.md | 12 + .changeset/curvy-taxis-shave.md | 8 + .changeset/mighty-cougars-rule.md | 6 + .github/workflows/main.yml | 4 +- examples/combobox-animated/index.tsx | 13 +- examples/combobox-cancel/index.tsx | 9 +- examples/combobox-disclosure/index.tsx | 9 +- examples/combobox-group/index.tsx | 9 +- .../combobox-inline-autocomplete/index.tsx | 9 +- examples/combobox-item-value/index.tsx | 9 +- examples/combobox-links/index.tsx | 13 +- examples/combobox-matches/index.tsx | 9 +- examples/combobox-min-length/index.tsx | 14 +- .../combobox-multiple/combobox-multiple.tsx | 9 +- examples/combobox-textarea/index.tsx | 15 +- examples/combobox/index.tsx | 9 +- examples/form-select/select.tsx | 4 +- examples/hovercard-disclosure/index.tsx | 9 +- examples/hovercard/index.tsx | 4 +- examples/menu-bar/menu.tsx | 17 +- examples/menu-context-menu/index.tsx | 9 +- examples/menu-context-menu/readme.md | 2 +- examples/menu-item-radio/index.tsx | 3 +- examples/menu-nested/menu.tsx | 12 +- examples/menu-slide/menu.tsx | 20 +- .../show-hide-with-click-1-chrome.png | Bin 21199 -> 21044 bytes .../show-hide-with-click-1-safari.png | Bin 50736 -> 50906 bytes examples/menu/index.tsx | 4 +- examples/popover-flip/index.tsx | 7 +- examples/popover-focus-within/popover.tsx | 33 +- examples/popover-lazy/index.tsx | 51 ++ examples/popover-lazy/popover.ts | 1 + examples/popover-lazy/readme.md | 18 + examples/popover-lazy/spinner.tsx | 11 + examples/popover-lazy/style.css | 9 + examples/popover-lazy/test.ts | 51 ++ .../popover-lazy/use-perceptible-value.ts | 22 + examples/popover-responsive/index.tsx | 21 +- examples/popover-responsive/readme.md | 2 +- examples/popover-selection/index.tsx | 16 +- examples/popover-selection/readme.md | 2 +- examples/popover-standalone/popover.tsx | 33 +- examples/popover-standalone/readme.md | 2 +- examples/popover-standalone/test-browser.ts | 7 +- examples/select-animated/index.tsx | 10 +- examples/select-autofill/index.tsx | 7 +- .../select-combobox.tsx | 4 +- examples/select-combobox/index.tsx | 9 +- examples/select-group/index.tsx | 13 +- examples/select-item-custom/index.tsx | 9 +- examples/select-multiple/index.tsx | 13 +- examples/select/index.tsx | 13 +- examples/toolbar-select/toolbar.tsx | 3 +- .../index.tsx | 4 +- .../style.css | 0 examples/tooltip-instant/test.ts | 29 ++ examples/tooltip-label/index.tsx | 33 ++ .../style.css | 0 examples/tooltip-label/test.ts | 18 + examples/tooltip-placement/test.ts | 17 - examples/tooltip-timeout/index.tsx | 20 - examples/tooltip-timeout/test.ts | 23 - examples/tooltip/index.tsx | 4 +- examples/tooltip/style.css | 1 + examples/tooltip/test-mobile.ts | 12 + examples/tooltip/test.ts | 88 +++- package-lock.json | 5 +- package.json | 9 + .../src/hovercard/hovercard-store.ts | 5 +- .../ariakit-core/src/popover/popover-store.ts | 402 +--------------- .../ariakit-core/src/tooltip/tooltip-store.ts | 148 ++---- packages/ariakit-react-core/package.json | 1 + .../src/popover/popover-store.ts | 19 +- .../src/popover/popover.tsx | 442 ++++++++++++++++-- .../ariakit-react-core/src/portal/portal.tsx | 52 ++- .../src/tooltip/tooltip-anchor.ts | 122 +++-- .../src/tooltip/tooltip-store.ts | 24 +- .../src/tooltip/tooltip.tsx | 154 ++---- packages/ariakit-react/package.json | 4 +- playwright.config.ts | 14 +- .../[category]/[page]/table-of-contents.tsx | 11 +- website/app/previews/[page]/page.tsx | 2 +- website/components/header-menu.tsx | 25 +- website/components/header-version-select.tsx | 14 +- website/components/playground-toolbar.tsx | 19 +- website/components/tooltip-button.tsx | 5 +- 87 files changed, 1377 insertions(+), 1014 deletions(-) create mode 100644 .changeset/__breaking-1.md create mode 100644 .changeset/curvy-taxis-shave-2.md create mode 100644 .changeset/curvy-taxis-shave.md create mode 100644 .changeset/mighty-cougars-rule.md create mode 100644 examples/popover-lazy/index.tsx create mode 100644 examples/popover-lazy/popover.ts create mode 100644 examples/popover-lazy/readme.md create mode 100644 examples/popover-lazy/spinner.tsx create mode 100644 examples/popover-lazy/style.css create mode 100644 examples/popover-lazy/test.ts create mode 100644 examples/popover-lazy/use-perceptible-value.ts rename examples/{tooltip-placement => tooltip-instant}/index.tsx (72%) rename examples/{tooltip-placement => tooltip-instant}/style.css (100%) create mode 100644 examples/tooltip-instant/test.ts create mode 100644 examples/tooltip-label/index.tsx rename examples/{tooltip-timeout => tooltip-label}/style.css (100%) create mode 100644 examples/tooltip-label/test.ts delete mode 100644 examples/tooltip-placement/test.ts delete mode 100644 examples/tooltip-timeout/index.tsx delete mode 100644 examples/tooltip-timeout/test.ts create mode 100644 examples/tooltip/test-mobile.ts diff --git a/.changeset/__breaking-1.md b/.changeset/__breaking-1.md new file mode 100644 index 0000000000..5aeff8f1bb --- /dev/null +++ b/.changeset/__breaking-1.md @@ -0,0 +1,57 @@ +--- +"@ariakit/core": minor +"@ariakit/react-core": minor +"@ariakit/react": minor +--- + +**BREAKING**: Moved props from the `usePopoverStore` hook to the `Popover` component: `fixed`, `gutter`, `shift`, `flip`, `slide`, `overlap`, `sameWidth`, `fitViewport`, `arrowPadding`, `overflowPadding`, `getAnchorRect`, `renderCallback` (renamed to `updatePosition`). ([#2279](https://github.com/ariakit/ariakit/pull/2279)) + +The exception is the `placement` prop that should still be passed to the store. + +**Before**: + +```jsx +const popover = usePopoverStore({ + placement: "bottom", + fixed: true, + gutter: 8, + shift: 8, + flip: true, + slide: true, + overlap: true, + sameWidth: true, + fitViewport: true, + arrowPadding: 8, + overflowPadding: 8, + getAnchorRect: (anchor) => anchor?.getBoundingClientRect(), + renderCallback: (props) => props.defaultRenderCallback(), +}); + +; +``` + +**After**: + +```jsx +const popover = usePopoverStore({ placement: "bottom" }); + + anchor?.getBoundingClientRect()} + updatePosition={(props) => props.updatePosition()} +/>; +``` + +This change affects all the hooks and components that use `usePopoverStore` and `Popover` underneath: `useComboboxStore`, `ComboboxPopover`, `useHovercardStore`, `Hovercard`, `useMenuStore`, `Menu`, `useSelectStore`, `SelectPopover`, `useTooltipStore`, `Tooltip`. + +With this change, the underlying `@floating-ui/dom` dependency has been also moved to the `Popover` component, which means it can be lazy loaded. See the [Lazy Popover](https://ariakit.org/examples/popover-lazy) example. diff --git a/.changeset/curvy-taxis-shave-2.md b/.changeset/curvy-taxis-shave-2.md new file mode 100644 index 0000000000..8575c3eceb --- /dev/null +++ b/.changeset/curvy-taxis-shave-2.md @@ -0,0 +1,12 @@ +--- +"@ariakit/react-core": patch +"@ariakit/react": patch +--- + +The `Tooltip` component now defaults to use `aria-describedby` instead of `aria-labelledby`. ([#2279](https://github.com/ariakit/ariakit/pull/2279)) + +If you want to use the tooltip as a label for an anchor element, you can use the `type` prop on `useTooltipStore`: + +```jsx +useTooltipStore({ type: "label" }); +``` diff --git a/.changeset/curvy-taxis-shave.md b/.changeset/curvy-taxis-shave.md new file mode 100644 index 0000000000..0c56028b3f --- /dev/null +++ b/.changeset/curvy-taxis-shave.md @@ -0,0 +1,8 @@ +--- +"@ariakit/react-core": patch +"@ariakit/react": patch +--- + +The `Tooltip` component now supports mouse events. ([#2279](https://github.com/ariakit/ariakit/pull/2279)) + +It's now possible to hover over the tooltip without it disappearing, which makes it compliant with [WCAG 1.4.13](https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html). diff --git a/.changeset/mighty-cougars-rule.md b/.changeset/mighty-cougars-rule.md new file mode 100644 index 0000000000..31a1693b88 --- /dev/null +++ b/.changeset/mighty-cougars-rule.md @@ -0,0 +1,6 @@ +--- +"@ariakit/react-core": patch +"@ariakit/react": patch +--- + +Fixed infinite loop on `Portal` with the `preserveTabOrder` prop set to `true` when the portalled element is placed right after its original position in the React tree. ([#2279](https://github.com/ariakit/ariakit/pull/2279)) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a7aa06c349..8603e170eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -287,7 +287,7 @@ jobs: - name: Test id: test - run: npm run test-chrome + run: npm run playwright -- --project chrome --project android - name: Upload test results to GitHub if: failure() && steps.test.outcome == 'failure' @@ -341,7 +341,7 @@ jobs: - name: Test id: test - run: npm run playwright -- --project firefox --project safari + run: npm run playwright -- --project firefox --project safari --project ios - name: Upload test results to GitHub if: failure() && steps.test.outcome == 'failure' diff --git a/examples/combobox-animated/index.tsx b/examples/combobox-animated/index.tsx index 5f7d3df8b7..7407c29ad8 100644 --- a/examples/combobox-animated/index.tsx +++ b/examples/combobox-animated/index.tsx @@ -2,11 +2,7 @@ import * as Ariakit from "@ariakit/react"; import "./style.css"; export default function Example() { - const combobox = Ariakit.useComboboxStore({ - gutter: 4, - sameWidth: true, - animated: true, - }); + const combobox = Ariakit.useComboboxStore({ animated: true }); return (
- + 🍎 Apple diff --git a/examples/combobox-cancel/index.tsx b/examples/combobox-cancel/index.tsx index f0a8c995f4..6a804061ce 100644 --- a/examples/combobox-cancel/index.tsx +++ b/examples/combobox-cancel/index.tsx @@ -12,7 +12,7 @@ const list = [ ]; export default function Example() { - const combobox = Ariakit.useComboboxStore({ gutter: 4, sameWidth: true }); + const combobox = Ariakit.useComboboxStore(); return (
- + {list.map((value) => (
- + 🍕 Pizza diff --git a/examples/combobox-group/index.tsx b/examples/combobox-group/index.tsx index a816e1a42e..9c67a9b8e0 100644 --- a/examples/combobox-group/index.tsx +++ b/examples/combobox-group/index.tsx @@ -6,7 +6,7 @@ import food from "./food.js"; import "./style.css"; export default function Example() { - const combobox = Ariakit.useComboboxStore({ gutter: 4, sameWidth: true }); + const combobox = Ariakit.useComboboxStore(); const value = combobox.useState("value"); const deferredValue = React.useDeferredValue(value); @@ -27,7 +27,12 @@ export default function Example() { autoSelect /> - + {!!matches.length ? ( matches.map(([type, items], i) => ( diff --git a/examples/combobox-inline-autocomplete/index.tsx b/examples/combobox-inline-autocomplete/index.tsx index 6f0b7a89a3..c87d9f4be0 100644 --- a/examples/combobox-inline-autocomplete/index.tsx +++ b/examples/combobox-inline-autocomplete/index.tsx @@ -2,7 +2,7 @@ import * as Ariakit from "@ariakit/react"; import "./style.css"; export default function Example() { - const combobox = Ariakit.useComboboxStore({ gutter: 8, sameWidth: true }); + const combobox = Ariakit.useComboboxStore(); return (
- + 🍎 Apple diff --git a/examples/combobox-item-value/index.tsx b/examples/combobox-item-value/index.tsx index 78fc9b79eb..8cb5100dd9 100644 --- a/examples/combobox-item-value/index.tsx +++ b/examples/combobox-item-value/index.tsx @@ -4,7 +4,7 @@ import "./style.css"; const fruits = ["Apple", "Grape", "Orange", "Strawberry", "Watermelon"]; export default function Example() { - const combobox = Ariakit.useComboboxStore({ gutter: 8, sameWidth: true }); + const combobox = Ariakit.useComboboxStore(); return (
- + {fruits.map((f) => ( diff --git a/examples/combobox-links/index.tsx b/examples/combobox-links/index.tsx index 132113d666..5b0dabee67 100644 --- a/examples/combobox-links/index.tsx +++ b/examples/combobox-links/index.tsx @@ -24,11 +24,7 @@ const links = [ ]; export default function Example() { - const combobox = Ariakit.useComboboxStore({ - gutter: 4, - sameWidth: true, - }); - + const combobox = Ariakit.useComboboxStore(); const mounted = combobox.useState("mounted"); const value = combobox.useState("value"); const deferredValue = useDeferredValue(value); @@ -72,7 +68,12 @@ export default function Example() { /> {mounted && ( - + {matches.length ? ( matches.map(renderItem) ) : ( diff --git a/examples/combobox-matches/index.tsx b/examples/combobox-matches/index.tsx index cb38f5bb27..4eca48663f 100644 --- a/examples/combobox-matches/index.tsx +++ b/examples/combobox-matches/index.tsx @@ -5,7 +5,7 @@ import list from "./list.js"; import "./style.css"; export default function Example() { - const combobox = Ariakit.useComboboxStore({ gutter: 8, sameWidth: true }); + const combobox = Ariakit.useComboboxStore(); const value = combobox.useState("value"); const deferredValue = useDeferredValue(value); @@ -24,7 +24,12 @@ export default function Example() { className="combobox" /> - + {matches.length ? ( matches.map((value) => ( - + 🍎 Apple diff --git a/examples/combobox-multiple/combobox-multiple.tsx b/examples/combobox-multiple/combobox-multiple.tsx index 8c7afa2985..4fb5db7417 100644 --- a/examples/combobox-multiple/combobox-multiple.tsx +++ b/examples/combobox-multiple/combobox-multiple.tsx @@ -35,8 +35,6 @@ export const ComboboxMultiple = React.forwardRef< // is on the combobox input, so we set `virtualFocus` to `false` to disable // this behavior and put DOM focus on the items. virtualFocus: false, - sameWidth: true, - gutter: 8, defaultValue, value, setValue: onChange, @@ -76,7 +74,12 @@ export const ComboboxMultiple = React.forwardRef< ) : ( element )} - + {(popoverProps) => ( (null); const [caretOffset, setCaretOffset] = React.useState(null); - const combobox = Ariakit.useComboboxStore({ - fitViewport: true, - getAnchorRect: () => { - const textarea = ref.current; - if (!textarea) return null; - return getAnchorRect(textarea); - }, - }); + const combobox = Ariakit.useComboboxStore(); const searchValue = combobox.useState("value"); const mounted = combobox.useState("mounted"); @@ -127,6 +120,12 @@ export default function Example() {