Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@cambly/syntax-floating-components: Tooltip + Popover v2.0.0, built on top of AriaKit #247

Closed
wants to merge 20 commits into from

Conversation

benjroy
Copy link
Contributor

@benjroy benjroy commented Sep 11, 2023

Changes

This PR rewrites Tooltip and Popover on top of Ariakit. This will introduce a breaking change to this repository

Why?

We have bugs in our current Tooltip + Popover, as well as issues in mixing our components from this repo with similar components from material-ui. @christianvuerings showed me a new one just this morning

This PR is here to simplify the interfaces to these components + introduce something we can build on top of to save us a ton of development time.


The main breaking change is the shape of the components themselves.

Instead of three components that must be used together, e.g. Tooltip / TooltipTrigger / TooltipContent there is only one:

<Tooltip content="some tooltip content">
  <Link href="https://news.ycombinator.com" text="hover or focus me to see tooltip, click to use the link to navigate" />
</Tooltip>

In this PR, the thing I am most concerned about is the interfaces we ship to these components. We should make them as minimal and stable as possible. That will allow us to continue making improvements or adjusting behavior internally in these comopnents without having a large impact to the callsites where they exist in our app.

Take the Tooltip + Popover for a spin at the storybook build for this PR

About AriaKit

Ariakit is an open-source project with deep development history whose goal is to make it easy to develop fully accessible applications. It can hit two birds with one stone for us:

  1. reduce the time to develop the interactive components we have been discussing in our Syntax working group
  2. ensure that all of the interactivity we build into our app will be accessible to our blind or differently-abled users. This means screen readers and the standard tools that those users rely on.

The difference to user experience is immense and also the abstractions will allow us to write more immediately usable components with smaller prop interfaces.

Browser accessibility quirks

AriaKit has deep knowledge and handling of the quirks that exist in different environments. For example, here is some quirks handling for safari re: tabbing & focus behavior in Safari/VoiceOver:

If we choose not to rely on a library like this, we will absolutely run into a subset of all the edge cases that AriaKit is already handling. These quirks and issues will take us weeks of development time to research/understand/fix, and often through our research we will eventually find the fix in the ariakit repo itself.

SSR

Ariakit (and Reakit before it) were designed from the start with ServerSideRendering in mind. That's why in 2019 we chose to rely on it at my previous job. The repo is not stale, and is still very active and responsive to issues and community PRs.

Here's an example describing an SSR flow with NextJS, where a dialog is rendered on the the server and immediately interactable on the client

Bundle size comparisons:

We'll ship a few extra kilobytes compared to what we have right now, but the difference is small.
AriaKit builds on top of floating-ui, just like the current implementations here in @cambly/syntax-floating-components

Tooltip v1 bundle:
Bundle size is 185 kB -> 60.7 kB (gzip)

Popover v1 bundle:
Bundle size is 185 kB -> 60.7 kB (gzip)

Tooltip v1 + Popover v1 bundle:
Bundle size is 186 kB -> 60.7 kB (gzip)

Popover components from AriaKit:
Bundle size is 202 kB -> 66.9 kB (gzip)

Tooltip Components from Ariakit:
Bundle size is 207 kB -> 68.4 kB (gzip)

Tooltip + Popover components from aria kit:
Bundle size is 211 kB -> 69.5 kB (gzip)

The following is a little GPT output from phind.com on some quick selling points for the library:

  • Accessibility: Ariakit is built with accessibility in mind, following WAI-ARIA recommendations. This ensures your interfaces will be accessible to all users, including those with disabilities 231314.

  • Composability and Customizability: Ariakit components can be easily composed to create new components, and they are fully customizable. This allows you to integrate them into any design or styling framework 39.

  • Adaptive: Ariakit supports various input modes, making your UI components adaptable to different user needs and assistive technologies. It ensures consistent behavior across different UIs and has been tested across a variety of browsers, devices, and platforms 39.

  • React Hooks: Ariakit provides a set of React Hooks designed to help developers build accessible UI components. These hooks include features like focus management and accessible labels 3.

  • Convenience and Efficiency: Ariakit handles many common accessibility considerations, allowing developers to focus more on the design and functionality of their applications. This can save time and effort in the development process 310.

  • Open and Community Support: Ariakit is an open-project, benefiting from the contributions and support of a community of developers. This can lead to regular updates, improvements, and a wealth of resources and solutions for common issues 214.

Let's look at some code examples of using Ariakit:

Creating an Accessible Button

import { Button } from "@ariakit/button";

function MyButton() {
  return <Button>Click me!</Button>;
}

Creating an Accessible Dialog **

import "./style.css";
import * as Ariakit from "@ariakit/react";

export default function Example() {
  const dialog = Ariakit.useDialogStore();
  return (
    <>
      <Ariakit.Button onClick={dialog.show} className="button">
        Show modal
      </Ariakit.Button>
      <Ariakit.Dialog store={dialog} className="dialog">
        <Ariakit.DialogHeading className="heading">
          Success
        </Ariakit.DialogHeading>
        <p className="description">
          Your payment has been successfully processed. We have emailed your
          receipt.
        </p>
        <div>
          <Ariakit.DialogDismiss className="button">OK</Ariakit.DialogDismiss>
        </div>
      </Ariakit.Dialog>
    </>
  );
}

Creating an Accessible Menu

import "./style.css";
import * as Ariakit from "@ariakit/react";

export default function Example() {
  const menu = Ariakit.useMenuStore();
  return (
    <>
      <Ariakit.MenuButton store={menu} className="button">
        Actions
        <Ariakit.MenuButtonArrow />
      </Ariakit.MenuButton>
      <Ariakit.Menu store={menu} gutter={8} className="menu">
        <Ariakit.MenuItem className="menu-item" onClick={() => alert("Edit")}>
          Edit
        </Ariakit.MenuItem>
        <Ariakit.MenuItem className="menu-item">Share</Ariakit.MenuItem>
        <Ariakit.MenuItem className="menu-item" disabled>
          Delete
        </Ariakit.MenuItem>
        <Ariakit.MenuSeparator className="separator" />
        <Ariakit.MenuItem className="menu-item">Report</Ariakit.MenuItem>
      </Ariakit.Menu>
    </>
  );
}

@changeset-bot
Copy link

changeset-bot bot commented Sep 11, 2023

🦋 Changeset detected

Latest commit: 83b9215

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@cambly/syntax-floating-components Minor
@cambly/syntax-core Patch
@syntax/storybook Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Sep 11, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
syntax ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 12, 2023 7:02pm

…s rather than package requires in the test files
@benjroy benjroy changed the title 20230907 floating components v2 built on ariakit @cambly/syntax-floating-components: Tooltip + Popover v2.0.0, built on top of AriaKit Sep 12, 2023
@benjroy benjroy marked this pull request as ready for review September 12, 2023 17:58
@@ -21,6 +21,8 @@
"test:watch": "vitest"
},
"dependencies": {
"@ariakit/react": "^0.3.0",
"@cambly/syntax-core": "workspace:^",
"@cambly/syntax-design-tokens": "workspace:0.9.0",
"@floating-ui/react": "^0.24.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy can we make sure to remove the @floating-ui/react dependency now?

Copy link
Contributor

@christianvuerings christianvuerings left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy thanks for putting this PR together! I like the simplified API and improved accessibility features.

Ariakit vs other libraries

Curious why you picked Ariakit vs something like React Aria or Radix UI.

Number of weekly downloads:

https://npmtrends.com/@ariakit/react-vs-@radix-ui/react-tooltip-vs-@react-aria/tooltip
image

Maintenance

https://npmcompare.com/compare/@ariakit/react,@radix-ui/primitive,react-aria

Ariakit React Aria Radix UI
Owned & maintained by 1 person - Diego Daz Adobe WorkOS

What is going to happen to Ariakit if Diego no longer works on it?

Other comparisons

Todo's for this PR if we want to go ahead with Ariakit

  • Add SSR test files for both Popover and Tooltip - to avoid issues like we had with Layer: Layer: fix document is not defined on SSR #255
  • Address comments in this PR
  • Figure out root cause as to why hovering / focussing doesn’t keep a Popover or Tooltip open in Storybook
tooltip-tooltip-does-not-stay-open-on-hover-storybook.mp4

Action items after this PR lands:

  • Update all Popover / Tooltip components in Cambly-Frontend

@@ -0,0 +1,8 @@
---
"@cambly/syntax-floating-components": minor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy can we make this a major release for syntax-floating-components?


_Breaking_: @cambly/syntax-floating-components: Popover + Tooltip v2.0.0, built on AriaKit

Patch: @cambly/syntax-core/Typography: `inline` renders as `"span"` by default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy could we move this out to a separate PR?

// so use globby to exclude stories from this workspace matched in node_modules
// (this happens because syntax-floating components has a dependency on syntax-core + pnpm linking behavior for workspace management)
//
// see: https://github.com/storybookjs/storybook/issues/19446#issuecomment-1276067149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy would a Storybook upgrade not fix this for us? Looks like they fixed it in Storybook itself:
storybookjs/storybook#19446 (comment)
storybookjs/storybook#22873

If so, I would rather upgrade Storybook than having this workaround.

@@ -6,7 +6,7 @@
"changeset": "changeset",
"clean": "turbo run clean && rm -rf node_modules",
"dev": "turbo run dev --no-cache --continue",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"format": "prettier --write \"**/*.{ts,tsx,md,css}\"",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy thanks for adding this in!

@@ -113,7 +113,8 @@ const Typography = ({
*/
weight?: "regular" | "interactive" | "semiBold" | "bold" | "heavy";
}): ReactElement => {
const Tag = as;
const defaultTag = inline ? "span" : "div";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy as mentioned above, please

  • put this in a separate PR
  • add tests around this change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy also, I'm curious what happens if we set inline={false} + as="span" - won't that render a span which is still inline?

* Wires a handler to fire when visibility changes to an ariakit DisclosureStore store (tooltip, hovercard, popover, etc...)
* Use for analytics and controlling open state externally
*/
export default function useChangeContentVisibility(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy could you add some tests for these 2 utility methods we added?


type OnFocusHandler = FocusEventHandler<HTMLElement>;

export default function useForwardFocus(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy could you also add docs for this default export?

anchorElement === anchorRef.current
)
return;
// ariakit Tooltip uses html data attr to determine if mouse moves closes the tooltip or not
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy this doesn't feel great... seems like we need to know about Ariakit internals to make this work. Let's definitely add tests around this.

TooltipContent,
TooltipTrigger,
};
export { Popover, Tooltip };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy way cleaner, thank you

<Story />
</Box>
),
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy let's make sure all the properties listed here are still available on the component - e.g. onOpenChange / strategy

@benjroy
Copy link
Contributor Author

benjroy commented Sep 27, 2023

@christianvuerings I haven't checked out radix yet, but from these results alone in a two way matchup b/w react-aria and ariakit, react-aria is the clear choice.

Bundle size is 33.4 kB -> 11.2 kB (gzip):
react-aria tooltip dependencies (used in an example from their docs)

Bundle size is 154 kB -> 49.7 kB (gzip)
ariakit tooltip dependencies (used in an example from their docs)

*edit:

Bundle size is 184 kB -> 60.5 kB (gzip)
radix-ui tooltip primitives (from an example in their docs)

react-aria remains on top here. These measurements are rudimentary, but wow that's a clear standout. The bundle sizes might start to even out if we use and ship more individual parts of the library, (like Tooltip + Popover + Dialog + other parts an average page would be likely to have.

Another point in react-aria's favor: they have a date-picker.

it's still pending in ariakit, and has been since it's reakit days.

  • edit 2:

Okay, more comparisons: from the DatePicker example in react-aria, here is bundle size for what look to be most of the react-aria exports needed to implement the example: Bundle size is 287 kB -> 87.8 kB (gzip)

for just the datepicker hooks from that example:
Bundle size is 77.8 kB -> 23.8 kB (gzip)

I think our team compared all three of these back at nerdwallet (~2020), and reakit won out at the time for a few reasons: bundle-size, level of opinionated-ness (we needed something we could make work with as few breaking changes as possible to legacy code), ssr support, and state of the projects themselves. At the time reakit came out on top in every column that we needed, especially the bundle size.

I was the one pushing for us to adopt and use react-aria throughout that process, but it was just a bit early in that project's history so there were problems with ssr rendering & bundle size implications were not nearly as optimized as what these are showing.

I'm happy to take another pass at it now! Especially with those numbers from the tooltip bundle size. But also, that 87.8kb DatePicker bundle says that we need to consider multiple permutations to really get confidence in bundle-size numbers.

Anyway, as long as we focus a lot of energy on the external props interface that we ship from syntax at this initial stage, I think we should be able to change out whatever library we build on top of in the future without having to ship a breaking change to our syntax packages in order to achieve that.

@diegohaz
Copy link

Hi! I happened upon this PR during my weekly search. As this project is open source, I want to mention that if you end up using Ariakit, please feel free to mention me on PRs/issues for reviews or any questions you might have.

A few notes:

  • The entire Ariakit library is approximately 50 kB gzip, excluding the react and react-dom peer dependencies. The Tooltip is roughly 23 kB gzip, mainly because it incorporates numerous accessibility enhancements and addresses many edge cases. In fact, it's powered by Hovercard and Dialog. Tooltip shares a good amount of code with other modules in the library, so the increase in bundle size should be offset as the project uses other modules.

  • Regarding maintenance, I suggest this read: Ariakit is antifragile

  • Here's an example of building the Radix Dialog API using Ariakit underneath: https://ariakit.org/examples/dialog-radix. The document delves deeper into some of the edge cases that Ariakit resolves.

@@ -21,6 +21,8 @@
"test:watch": "vitest"
},
"dependencies": {
"@ariakit/react": "^0.3.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjroy could we make sure that we use the latest version of @ariakit/react v0.3.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants