From c2e9fe4661799975d61ba8d56431563faf707b91 Mon Sep 17 00:00:00 2001 From: Christopher Woolum Date: Thu, 11 Jul 2024 12:45:31 -0700 Subject: [PATCH 01/10] feat(tooltip): implement tooltip to beta phase --- apps/website/src/_state/component-statuses.ts | 2 +- .../src/components/showcase/showcase.tsx | 19 +- .../headless/tooltip/examples/animation.tsx | 13 + .../tooltip/examples/arrow-styling.tsx | 58 ++++ .../docs/headless/tooltip/examples/auto.tsx | 11 + .../docs/headless/tooltip/examples/basic.tsx | 14 + .../headless/tooltip/examples/complex.tsx | 22 ++ .../docs/headless/tooltip/examples/flip.tsx | 13 + .../headless/tooltip/examples/floating.tsx | 11 + .../docs/headless/tooltip/examples/gutter.tsx | 11 + .../docs/headless/tooltip/examples/hero.tsx | 13 +- .../headless/tooltip/examples/placement.tsx | 38 +++ .../headless/tooltip/examples/styling.tsx | 12 + .../headless/tooltip/examples/transition.tsx | 13 + .../routes/docs/headless/tooltip/index.mdx | 293 ++++++++++++++++-- .../headless/tooltip/snippets/animation.css | 29 ++ .../headless/tooltip/snippets/styling.css | 14 + .../headless/tooltip/snippets/tooltip.css | 47 +++ .../headless/tooltip/snippets/transition.css | 16 + .../src/components/tooltip/index.ts | 3 +- .../src/components/tooltip/tooltip-arrow.tsx | 38 +++ .../components/tooltip/tooltip-content.tsx | 10 - .../src/components/tooltip/tooltip-context.ts | 16 + .../src/components/tooltip/tooltip-panel.tsx | 23 ++ .../src/components/tooltip/tooltip-root.tsx | 96 +++++- .../components/tooltip/tooltip-trigger.tsx | 107 ++++++- .../src/components/tooltip/tooltip.driver.ts | 69 +++++ .../src/components/tooltip/tooltip.test.ts | 223 +++++++++++++ packages/kit-headless/src/utils/test-utils.ts | 8 + 29 files changed, 1183 insertions(+), 59 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/auto.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/basic.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/complex.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/flip.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/floating.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/gutter.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/styling.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx create mode 100644 apps/website/src/routes/docs/headless/tooltip/snippets/animation.css create mode 100644 apps/website/src/routes/docs/headless/tooltip/snippets/styling.css create mode 100644 apps/website/src/routes/docs/headless/tooltip/snippets/transition.css create mode 100644 packages/kit-headless/src/components/tooltip/tooltip-arrow.tsx delete mode 100644 packages/kit-headless/src/components/tooltip/tooltip-content.tsx create mode 100644 packages/kit-headless/src/components/tooltip/tooltip-context.ts create mode 100644 packages/kit-headless/src/components/tooltip/tooltip-panel.tsx create mode 100644 packages/kit-headless/src/components/tooltip/tooltip.driver.ts create mode 100644 packages/kit-headless/src/components/tooltip/tooltip.test.ts create mode 100644 packages/kit-headless/src/utils/test-utils.ts diff --git a/apps/website/src/_state/component-statuses.ts b/apps/website/src/_state/component-statuses.ts index 17e7479a4..bff04db40 100644 --- a/apps/website/src/_state/component-statuses.ts +++ b/apps/website/src/_state/component-statuses.ts @@ -49,6 +49,6 @@ export const statusByComponent: ComponentKitsStatuses = { Select: ComponentStatus.Beta, Separator: ComponentStatus.Beta, Tabs: ComponentStatus.Beta, - Tooltip: ComponentStatus.Draft, + Tooltip: ComponentStatus.Beta, }, }; diff --git a/apps/website/src/components/showcase/showcase.tsx b/apps/website/src/components/showcase/showcase.tsx index 5a58b15e9..4cfdd2dd3 100644 --- a/apps/website/src/components/showcase/showcase.tsx +++ b/apps/website/src/components/showcase/showcase.tsx @@ -19,13 +19,18 @@ export const Showcase = component$(({ name, ...props }) => { const componentCodeSig = useSignal(); useTask$(async () => { - // eslint-disable-next-line qwik/valid-lexical-scope - MetaGlobComponentSig.value = isDev - ? await metaGlobComponents[componentPath]() // We need to call `await metaGlobComponents[componentPath]()` in development as it is `eager:false` - : metaGlobComponents[componentPath]; // We need to directly access the `metaGlobComponents[componentPath]` expression in preview/production as it is `eager:true` - componentCodeSig.value = isDev - ? await rawComponents[componentPath]() - : rawComponents[componentPath]; + try { + // eslint-disable-next-line qwik/valid-lexical-scope + MetaGlobComponentSig.value = isDev + ? await metaGlobComponents[componentPath]() // We need to call `await metaGlobComponents[componentPath]()` in development as it is `eager:false` + : metaGlobComponents[componentPath]; // We need to directly access the `metaGlobComponents[componentPath]` expression in preview/production as it is `eager:true` + componentCodeSig.value = isDev + ? await rawComponents[componentPath]() + : rawComponents[componentPath]; + } catch (e) { + console.error('Unable to load path %s', componentPath, e); + throw e; + } }); return ( diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx new file mode 100644 index 000000000..94a17a8f3 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + + Animated tooltip content here + + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx new file mode 100644 index 000000000..f653f2626 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx @@ -0,0 +1,58 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + useStyles$(` + .tooltip-panel { + background-color: #222; + color: #fff; + padding: 15px; + border-radius: 8px; + position: relative; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + transition: opacity 0.3s ease; + opacity: 0; + } + + .tooltip-panel[data-state="open"] { + opacity: 1; + } + + .tooltip-arrow { + position: absolute; + width: 20px; + height: 10px; + overflow: hidden; + } + + .tooltip-arrow::before { + content: ''; + position: absolute; + width: 10px; + height: 10px; + background-color: #222; + top: -5px; + left: calc(50% - 5px); + transform: rotate(45deg); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: top 0.3s ease; + } + + .tooltip-panel[data-state="open"] .tooltip-arrow::before { + top: -8px; + } + `); + + return ( + + Hover or Focus me + + +
+

Tooltip Title

+

This tooltip has a snazzy styled arrow!

+
+
+
+ ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/auto.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/auto.tsx new file mode 100644 index 000000000..f67d12917 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/auto.tsx @@ -0,0 +1,11 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + Tooltip content here + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/basic.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/basic.tsx new file mode 100644 index 000000000..0896bafa6 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/basic.tsx @@ -0,0 +1,14 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + + + Tooltip content here + + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/complex.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/complex.tsx new file mode 100644 index 000000000..8f43ad4ef --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/complex.tsx @@ -0,0 +1,22 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + + +
+

Tooltip Title

+

This is a tooltip with complex HTML content, including:

+
    +
  • List item 1
  • +
  • List item 2
  • +
  • List item 3
  • +
+
+
+
+ ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/flip.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/flip.tsx new file mode 100644 index 000000000..555a163ed --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/flip.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + + Tooltip content with flip enabled + + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/floating.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/floating.tsx new file mode 100644 index 000000000..fc23d9c32 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/floating.tsx @@ -0,0 +1,11 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + Floating Tooltip content here + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/gutter.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/gutter.tsx new file mode 100644 index 000000000..8cebab26c --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/gutter.tsx @@ -0,0 +1,11 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + Tooltip content with gutter + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/hero.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/hero.tsx index 1e7853bdc..f67d12917 100644 --- a/apps/website/src/routes/docs/headless/tooltip/examples/hero.tsx +++ b/apps/website/src/routes/docs/headless/tooltip/examples/hero.tsx @@ -1,16 +1,11 @@ -import { component$, useStyles$ } from '@builder.io/qwik'; +import { component$ } from '@builder.io/qwik'; import { Tooltip } from '@qwik-ui/headless'; export default component$(() => { - useStyles$(styles); - return ( - - Hover over me - I'm a tooltip! + + Hover or Focus me + Tooltip content here ); }); - -// internal -import styles from '../snippets/tooltip.css?inline'; diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx new file mode 100644 index 000000000..e6bc9a3e2 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx @@ -0,0 +1,38 @@ +import { component$, useSignal } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + const placement = useSignal<'top' | 'right' | 'bottom' | 'left'>('top'); + + return ( +
+ + + + + Hover or Focus me + + Tooltip content on the {placement.value} + + +
+ ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/styling.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/styling.tsx new file mode 100644 index 000000000..c236753e8 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/styling.tsx @@ -0,0 +1,12 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; +import '../snippets/styling.css'; + +export default component$(() => { + return ( + + Hover or Focus me + Tooltip content here + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx new file mode 100644 index 000000000..ac88f3e92 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + return ( + + Hover or Focus me + + Tooltip content with transition + + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/index.mdx b/apps/website/src/routes/docs/headless/tooltip/index.mdx index 1b604b32d..0b89dc17a 100644 --- a/apps/website/src/routes/docs/headless/tooltip/index.mdx +++ b/apps/website/src/routes/docs/headless/tooltip/index.mdx @@ -2,22 +2,32 @@ title: Qwik UI | Tooltip --- +import { statusByComponent } from '~/_state/component-statuses'; +import styles from './snippets/tooltip.css'; + + + # Tooltip -A popup that shows information when an element is focused or hovered over. +A text label that appears when a user hovers, focuses, or touches an element. -## ✨ Features - +The Qwik UI Tooltip component provides additional information or context on hover, focus, or touch. It ensures accessibility and positioning with built-in ARIA roles and automatic placement adjustments. + ## Building blocks ```tsx @@ -26,15 +36,20 @@ import { Tooltip } from '@qwik-ui/headless'; export default component$(() => { return ( - - Trigger - Panel + + + + + + + Tooltip content here + ); }); ``` -### 🎨 Anatomy +### Anatomy Table { }, { name: 'Tooltip.Trigger', - description: 'A button that opens the tooltip when interacted with.', + description: 'An element that opens the tooltip when interacted with.', }, { name: 'Tooltip.Panel', - description: `An HTML Element that is above other content on the page.`, + description: `An HTML element that contains the tooltip content.`, + }, + { + name: 'Tooltip.Arrow', + description: `An optional arrow component to point to the trigger element.`, }, ]} /> -## API +## What is a Tooltip? + +A tooltip is a small text label that appears when a user hovers over, focuses on, or touches an element. It provides additional information or context. + +### When would I use a tooltip? + +Tooltips are useful for displaying contextual information or additional details about an element without cluttering the UI. They enhance user experience by providing necessary information only when needed. + +## Use case examples + + + +## Caveats + + + While we handle most of the hard stuff, there are some details that should be + considered. + + +### Styling open tooltips + +```tsx +.tooltip-panel[data-state="open"] { + background: lightblue; +} +``` + +Use the data-state attribute on the `` component to specifically style the tooltip when it's open. + +### Cross-browser Animations + +Entry and exit animations have often been a frustrating experience in web development, especially trying to animate between `display: none`, a discrete property. + +Luckily, the Qwik UI team has done an excellent job of managing animations on polyfill browsers for you using the `data-state` attributes. + +```css +.tooltip-animation { + transform: scale(0); +} + +.tooltip-animation[data-state='open'] { + animation: tooltip-grow 0.5s ease-in-out forwards; +} + +.tooltip-animation[data-state='closing'] { + animation: tooltip-shrink 0.4s ease-in-out forwards; +} + +@keyframes tooltip-shrink { + from { + transform: scale(1); + } + to { + transform: scale(0); + } +} + +@keyframes tooltip-grow { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} +``` + +## Tooltip Behavior + +Tooltips show when hovering over or focusing on the trigger element and dismiss when moving the mouse away or losing focus. + +## Floating Behavior + +By default, the Qwik UI Tooltip will float above the trigger component. + + + +To make a tooltip float, we use JavaScript to choose where the tooltip should be positioned. + +### Custom Floating Position + +By default, tooltips will float above the trigger component. + +When setting `placement` on the root, you can customize the position of the tooltip. + + + +Above we have set the `placement` prop to `right`, so the `` will be positioned to the right of the trigger. + +### Flip + +Enabled by default, we can use the `flip` prop to flip its position based on the available space in the viewport. + + + +To disable flipping, set `flip={false}` on the ``. + +### Gutter + +The gutter property defines the space between the anchor element and the floating element. + + + +## Styling + +Styles can be added normally like any other component in Qwik UI, such as adding a class. + +If Tailwind is the framework of choice, then styles can be added using the [arbitrary variant syntax](https://tailwindcss.com/docs/hover-focus-and-other-states#using-arbitrary-variants) or [@apply](https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply) command. + + + +> The arbitrary variant can be customized/abstracted even further by [adding a variant](https://tailwindcss.com/docs/plugins#adding-variants) as a plugin in the tailwind config. + +## Animations + + + +To use an animation, add the following CSS classes to the component. + +- The `data-state="open"` attribute determines the animation that happens when it is first opened. + +- The `data-state="closing"` class determines what class is added when the tooltip is **closed**. + +Here's the CSS imported from the example: + + + +### Transition declarations + +Transitions use the same classes for entry and exit animations. Those being `data-state="opening"` and `data-state="closing"`. They are explained more in the `Caveats` section. + + + +> The [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) is another native solution that aims to solve animating between states. Support is currently in around **~71%** of browsers. + +CSS from the example: + +```css +.tooltip-transition { + opacity: 0; + transition: + opacity 0.5s, + display 0.5s, + overlay 0.5s; + transition-behavior: allow-discrete; +} + +.tooltip-transition[data-state='open'] { + opacity: 1; +} + +.tooltip-transition[data-state='closing'] { + opacity: 0; +} +``` + +## Additional References + +Qwik UI aims to be in line with the standard whenever possible. Our goal is to empower Qwik developers to create amazing experiences for their users. + +To read more about tooltips you can check it out on: + +- [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role) +- [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices-1.1/#tooltip) ### Tooltip Root { { name: 'gutter', type: 'number', - description: 'The space between the floating element and the anchored element.', + description: 'The space between the trigger element and the tooltip.', }, { - name: 'floating', - type: 'string', - description: 'The floating position of the tooltip.', + name: 'data-state="closing"', + type: 'selector', + description: 'Style the element when the tooltip is closing. This occurs when the popover has a delay set.', + }, + { + name: 'data-state="closed"', + type: 'selector', + description: 'Style the element when the tooltip is closed.', + }, + + { + name: 'data-state="opening"', + type: 'selector', + description: 'Style the element when the tooltip is in the process of opening. This occurs when the popover has a delay set.', + }, + { + name: 'data-state="open"', + type: 'selector', + description: 'Style the element when the tooltip is open.', + }, + +]} +/> + +### Tooltip Components + + + +### Example Usages + +#### Basic: + + + +#### Complex HTML: + + + +#### Arrow Styling: + + diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css b/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css new file mode 100644 index 000000000..d450b4c2f --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css @@ -0,0 +1,29 @@ +.tooltip-animation { + transform: scale(0); +} + +.tooltip-animation[data-state='open'] { + animation: tooltip-grow 0.5s ease-in-out forwards; +} + +.tooltip-animation[data-state='closing'] { + animation: tooltip-shrink 0.4s ease-in-out forwards; +} + +@keyframes tooltip-shrink { + from { + transform: scale(1); + } + to { + transform: scale(0); + } +} + +@keyframes tooltip-grow { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/styling.css b/apps/website/src/routes/docs/headless/tooltip/snippets/styling.css new file mode 100644 index 000000000..bca2b77f8 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/styling.css @@ -0,0 +1,14 @@ +.custom-trigger { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; +} + +.custom-tooltip-panel { + background-color: #333; + color: white; + padding: 10px; + border-radius: 4px; +} diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css b/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css index b0a6ae097..ba53fa158 100644 --- a/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css @@ -21,3 +21,50 @@ 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } + +.tooltip-animation { + transform: scale(0); +} + +.tooltip-animation[data-state='open'] { + animation: tooltip-grow 0.5s ease-in-out forwards; +} + +.tooltip-animation[data-state='closing'] { + animation: tooltip-shrink 0.4s ease-in-out forwards; +} + +@keyframes tooltip-shrink { + from { + transform: scale(1); + } + to { + transform: scale(0); + } +} + +@keyframes tooltip-grow { + from { + transform: scale(0); + } + to { + transform: scale(1); + } +} + +.tooltip-transition { + opacity: 0; + transition: + opacity 0.5s, + display 0.5s, + overlay 0.5s; + transition-behavior: allow-discrete; +} + +.tooltip-transition[data-state='open'] { + opacity: 1; +} + +.tooltip-transition[data-state='closing'] { + opacity: 0; +} diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css b/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css new file mode 100644 index 000000000..225be8bd1 --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css @@ -0,0 +1,16 @@ +.tooltip-transition { + opacity: 0; + transition: + opacity 0.5s, + display 0.5s, + overlay 0.5s; + transition-behavior: allow-discrete; +} + +.tooltip-transition[data-state='opening'] { + opacity: 1; +} + +.tooltip-transition[data-state='closing'] { + opacity: 0; +} diff --git a/packages/kit-headless/src/components/tooltip/index.ts b/packages/kit-headless/src/components/tooltip/index.ts index 9f7759a87..2f0dbf448 100644 --- a/packages/kit-headless/src/components/tooltip/index.ts +++ b/packages/kit-headless/src/components/tooltip/index.ts @@ -1,3 +1,4 @@ export { HTooltipRoot as Root } from './tooltip-root'; -export { HTooltipContent as Content } from './tooltip-content'; +export { HTooltipPanel as Panel } from './tooltip-panel'; export { HTooltipTrigger as Trigger } from './tooltip-trigger'; +export { HTooltipArrow as Arrow } from './tooltip-arrow'; diff --git a/packages/kit-headless/src/components/tooltip/tooltip-arrow.tsx b/packages/kit-headless/src/components/tooltip/tooltip-arrow.tsx new file mode 100644 index 000000000..0fa9f1e12 --- /dev/null +++ b/packages/kit-headless/src/components/tooltip/tooltip-arrow.tsx @@ -0,0 +1,38 @@ +import { component$ } from '@builder.io/qwik'; + +/** + * TooltipArrowProps defines the properties for the Tooltip Arrow component. + */ +export type TooltipArrowProps = { + /** + * The width of the arrow. + */ + width?: number; + + /** + * The height of the arrow. + */ + height?: number; + + /** + * Additional class names for styling. + */ + class?: string; +}; + +/** + * HTooltipArrow is the arrow component for the Tooltip. + */ +export const HTooltipArrow = component$((props: TooltipArrowProps) => { + const { width = 10, height = 5, class: className } = props; + + return ( +
+ ); +}); diff --git a/packages/kit-headless/src/components/tooltip/tooltip-content.tsx b/packages/kit-headless/src/components/tooltip/tooltip-content.tsx deleted file mode 100644 index 3f78a47d9..000000000 --- a/packages/kit-headless/src/components/tooltip/tooltip-content.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { PropsOf, Slot, component$ } from '@builder.io/qwik'; -import { HPopoverPanel } from '../popover/popover-panel'; - -export const HTooltipContent = component$((props: PropsOf) => { - return ( - - - - ); -}); diff --git a/packages/kit-headless/src/components/tooltip/tooltip-context.ts b/packages/kit-headless/src/components/tooltip/tooltip-context.ts new file mode 100644 index 000000000..160aa1d75 --- /dev/null +++ b/packages/kit-headless/src/components/tooltip/tooltip-context.ts @@ -0,0 +1,16 @@ +import { createContextId, Signal } from '@builder.io/qwik'; + +export const TooltipContextId = createContextId('Tooltip'); + +export type TooltipContext = { + compId: string; + localId: string; + + delayDuration: number; + + triggerRef: Signal; + + state: Signal; +}; + +export type TriggerDataState = 'closing' | 'closed' | 'opening' | 'open'; diff --git a/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx b/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx new file mode 100644 index 000000000..9dad5d7d6 --- /dev/null +++ b/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx @@ -0,0 +1,23 @@ +import { PropsOf, Slot, component$, useContext } from '@builder.io/qwik'; +import { HPopoverPanel } from '../popover/popover-panel'; +import { TooltipContextId } from './tooltip-context'; + +export type HTooltipPanelProps = PropsOf; + +/** + * HTooltipPanel is the panel component for the Tooltip. + */ +export const HTooltipPanel = component$((props: HTooltipPanelProps) => { + const context = useContext(TooltipContextId); + + return ( + + + + ); +}); diff --git a/packages/kit-headless/src/components/tooltip/tooltip-root.tsx b/packages/kit-headless/src/components/tooltip/tooltip-root.tsx index f8ae7a685..738020dc0 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-root.tsx +++ b/packages/kit-headless/src/components/tooltip/tooltip-root.tsx @@ -1,12 +1,96 @@ -import { PropsOf, Slot, component$ } from '@builder.io/qwik'; -import { HPopoverRoot } from '../popover/popover-root'; +import { + PropsOf, + QRL, + Signal, + Slot, + component$, + useContextProvider, + useId, + useSignal, +} from '@builder.io/qwik'; +import { FloatingProps, HPopoverRoot } from '../popover/popover-root'; +import { TooltipContext, TooltipContextId, TriggerDataState } from './tooltip-context'; -export const HTooltipRoot = component$((props: PropsOf) => { - const { hover = true, floating = 'top', ...rest } = props; +/** + * TooltipRootProps defines the properties for the Tooltip Root component. + */ +export type TooltipRootProps = { + /** + * A value that determines whether the tooltip is open. + */ + open?: boolean; + + /** A signal that controls the current open state (controlled). */ + 'bind:open'?: Signal; + + /** + * QRL handler that runs when the tooltip opens or closes. + * @param open The new state of the tooltip. + */ + onOpenChange$?: QRL<(state: TriggerDataState) => void>; + + /** + * A value that determines how long before the tooltip will + * be opened once triggered in milliseconds. + */ + delayDuration?: number; + + /** + * The default position of the tooltip. + */ + placement?: Parameters['0']['floating']; + + id?: string; +} & Pick; + +/** + * TooltipProps combines TooltipRootProps and the properties of a div element. + */ +export type TooltipProps = TooltipRootProps & Exclude, 'ref'>; + +/** + * HTooltipRoot is the root component for the Tooltip. + */ +export const HTooltipRoot = component$((props: TooltipProps) => { + const { + placement = 'top', + id, + gutter, + delayDuration = 0, + flip, + onOpenChange$: _, + ...rest + } = props; + + const triggerRef = useSignal(); + const tooltipState = useSignal('closed'); + + const localId = useId(); + const compId = id ?? localId; + + const context: TooltipContext = { + compId, + localId, + triggerRef, + delayDuration, + state: tooltipState, + }; + + useContextProvider(TooltipContextId, context); return ( - - + +
+ +
); }); diff --git a/packages/kit-headless/src/components/tooltip/tooltip-trigger.tsx b/packages/kit-headless/src/components/tooltip/tooltip-trigger.tsx index e6521a6ca..7577417a9 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-trigger.tsx +++ b/packages/kit-headless/src/components/tooltip/tooltip-trigger.tsx @@ -1,10 +1,107 @@ -import { PropsOf, Slot, component$ } from '@builder.io/qwik'; -import { HPopoverTrigger } from '../popover/popover-trigger'; +import { + Slot, + component$, + sync$, + useContext, + $, + PropsOf, + useSignal, + Signal, + useTask$, +} from '@builder.io/qwik'; +import { TooltipContextId, TriggerDataState } from './tooltip-context'; +import { isServer } from '@builder.io/qwik/build'; +import { usePopover } from '../popover/use-popover'; + +/** + * HTooltipTrigger is the trigger component for the Tooltip. + */ +export const HTooltipTrigger = component$((props: PropsOf<'button'>) => { + const context = useContext(TooltipContextId); + + const openTimeout = useSignal(); + const closeTimeout = useSignal(); + + const { showPopover, hidePopover } = usePopover(context.localId); + + const clearTimeoutIfExists = $((timeoutRef: Signal) => { + if (timeoutRef.value) { + window.clearTimeout(timeoutRef.value); + timeoutRef.value = undefined; + } + }); + + const setTooltipState = $( + (open: boolean, state: TriggerDataState, timeoutRef: Signal) => { + context.state.value = state; + + if (context.delayDuration > 0) { + timeoutRef.value = window.setTimeout(() => { + context.state.value = open ? 'open' : 'closed'; + }, context.delayDuration); + } else { + context.state.value = open ? 'open' : 'closed'; + } + }, + ); + + const setTooltipOpen$ = $(() => { + clearTimeoutIfExists(closeTimeout); + showPopover(); + setTooltipState(true, 'opening', openTimeout); + }); + + const setTooltipClosed$ = $(() => { + clearTimeoutIfExists(openTimeout); + hidePopover(); + setTooltipState(false, 'closing', closeTimeout); + }); + + const preventDefaultSync$ = sync$((e: Event) => { + e.preventDefault(); + }); + + const handleKeyDown$ = $(async (e: KeyboardEvent) => { + if (context.state.value === 'open' && e.key === 'Escape') { + e.preventDefault(); + setTooltipClosed$(); + } + }); + + useTask$(({ track, cleanup }) => { + track(() => context.state.value); + + if (isServer) return; + + if (context.state.value === 'open') { + document.addEventListener('keydown', handleKeyDown$); + + cleanup(() => { + document.removeEventListener('keydown', handleKeyDown$); + }); + } else if (context.state.value === 'closed') { + document.removeEventListener('keydown', handleKeyDown$); + } + + // Cleanup function to ensure the event listener is removed + cleanup(() => { + document.removeEventListener('keydown', handleKeyDown$); + }); + }); -export const HTooltipTrigger = component$((props: PropsOf) => { return ( - + ); }); diff --git a/packages/kit-headless/src/components/tooltip/tooltip.driver.ts b/packages/kit-headless/src/components/tooltip/tooltip.driver.ts new file mode 100644 index 000000000..4c753a3d2 --- /dev/null +++ b/packages/kit-headless/src/components/tooltip/tooltip.driver.ts @@ -0,0 +1,69 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +export type DriverLocator = Locator | Page; + +export type TooltipOpenKeys = 'Enter' | 'Space'; + +export function createTooltipDriver(rootLocator: T) { + const getTooltip = () => { + return rootLocator.locator('[role="tooltip"]'); + }; + + const getTooltipByTextContent = (tooltipContent: string) => { + return rootLocator.locator('.tooltip-panel').getByText(tooltipContent); + }; + + const getTrigger = () => { + return rootLocator.locator('[aria-describedby]'); + }; + + const openTooltip = async ( + key: TooltipOpenKeys | 'hover' | 'click', + index?: number, + ) => { + const action = key === 'click' ? 'click' : key === 'hover' ? 'hover' : 'press'; + const trigger = index !== undefined ? getTrigger().nth(index) : getTrigger(); + + const tooltip = + index !== undefined + ? getTooltipByTextContent(`Tooltip ${index + 1}`) + : getTooltip(); + + if (action === 'click') { + await trigger.click({ position: { x: 1, y: 1 } }); + } else if (action === 'hover') { + await trigger.hover(); + } else { + await trigger.press(key); + } + + // Needed because Playwright doesn't wait for the tooltip to be visible + await expect(tooltip).toBeVisible(); + + return { trigger, tooltip }; + }; + + const getAllTooltips = () => { + return getTooltip().all(); + }; + + const getAllTriggers = () => { + return getTrigger().all(); + }; + + const getProgrammaticButtonTrigger = () => { + return rootLocator.locator('button'); + }; + + return { + ...rootLocator, + locator: rootLocator, + getTooltip, + getAllTooltips, + getTrigger, + getAllTriggers, + openTooltip, + getProgrammaticButtonTrigger, + getTooltipByTextContent, + }; +} diff --git a/packages/kit-headless/src/components/tooltip/tooltip.test.ts b/packages/kit-headless/src/components/tooltip/tooltip.test.ts new file mode 100644 index 000000000..b7b49dc95 --- /dev/null +++ b/packages/kit-headless/src/components/tooltip/tooltip.test.ts @@ -0,0 +1,223 @@ +import { expect, test, type Page } from '@playwright/test'; +import { createTooltipDriver } from './tooltip.driver'; +import { assertBoundingBoxExists } from '../../utils/test-utils'; + +async function setup(page: Page, exampleName: string) { + await page.goto(`/headless/tooltip/${exampleName}`); + + const driver = createTooltipDriver(page); + + return { + driver, + }; +} + +test('@Visual diff', async ({ page }) => { + const { driver: d } = await setup(page, 'basic'); + await expect(page).toHaveScreenshot('closed tooltip.png'); + + await d.getTrigger().hover(); + + await expect(page).toHaveScreenshot('opened tooltip.png'); +}); + +test.describe('Mouse Behavior', () => { + test(`GIVEN a closed tooltip + WHEN hovering over the trigger + THEN the tooltip should open`, async ({ page }) => { + const { driver: d } = await setup(page, 'basic'); + await expect(d.getTooltip()).toBeHidden(); + + await d.getTrigger().hover(); + + await expect(d.getTooltip()).toBeVisible(); + }); + + test(`GIVEN an open tooltip + WHEN moving the mouse away from the trigger + THEN the tooltip should close`, async ({ page }) => { + const { driver: d } = await setup(page, 'basic'); + + await d.getTrigger().hover(); + await expect(d.getTooltip()).toBeVisible(); + + await page.mouse.move(0, 0); + await expect(d.getTooltip()).toBeHidden(); + }); + + test(`GIVEN an open tooltip + WHEN clicking on the trigger + THEN the tooltip should remain open`, async ({ page }) => { + const { driver: d } = await setup(page, 'basic'); + + await d.getTrigger().hover(); + await expect(d.getTooltip()).toBeVisible(); + + await d.getTrigger().click(); + await expect(d.getTooltip()).toBeVisible(); + }); +}); + +test.describe('Keyboard Behavior', () => { + test(`GIVEN an open tooltip + WHEN focusing on the trigger and pressing the 'Escape' key + THEN the tooltip should close`, async ({ page }) => { + const { driver: d } = await setup(page, 'basic'); + await expect(d.getTooltip()).toBeHidden(); + + await d.getTrigger().focus(); + await expect(d.getTooltip()).toBeVisible(); + + await d.getTrigger().press('Escape'); + await expect(d.getTooltip()).not.toBeVisible(); + }); +}); + +test.describe('Placement and Positioning', () => { + const placements = ['top', 'right', 'bottom', 'left'] as const; + + test.describe('Tooltip Placements', () => { + placements.forEach((placement) => { + test(`GIVEN a tooltip with placement set to ${placement} + WHEN hovering over the trigger + THEN the tooltip should appear at the ${placement} of the trigger`, async ({ + page, + }) => { + const { driver: d } = await setup(page, 'placement'); + + const dropdown = page.getByLabel('Select Tooltip Placement:'); + await dropdown.selectOption(placement); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + + await expect(tooltip).toBeVisible(); + + const triggerBoundingBox = await trigger.boundingBox(); + const tooltipBoundingBox = await tooltip.boundingBox(); + + switch (placement) { + case 'top': + expect(tooltipBoundingBox?.y).toBeLessThan(triggerBoundingBox?.y ?? 0); + break; + case 'right': + expect(tooltipBoundingBox?.x).toBeGreaterThan( + (triggerBoundingBox?.x ?? 0) + (triggerBoundingBox?.width ?? 0), + ); + break; + case 'bottom': + expect(tooltipBoundingBox?.y).toBeGreaterThan( + (triggerBoundingBox?.y ?? 0) + (triggerBoundingBox?.height ?? 0), + ); + break; + case 'left': + expect(tooltipBoundingBox?.x).toBeLessThan(triggerBoundingBox?.x ?? 0); + break; + } + }); + }); + }); + + test(`GIVEN a tooltip with a gutter configured + WHEN opening the tooltip + THEN the tooltip should be spaced correctly from the trigger`, async ({ + page, + }) => { + const { driver: d } = await setup(page, 'gutter'); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + await expect(tooltip).toBeVisible(); + + const tooltipBoundingBox = await tooltip.boundingBox(); + const triggerBoundingBox = await trigger.boundingBox(); + + assertBoundingBoxExists(tooltipBoundingBox); + assertBoundingBoxExists(triggerBoundingBox); + + const gutterSpace = triggerBoundingBox.y - tooltipBoundingBox.y; + expect(gutterSpace).toBe(44); + }); + + test(`GIVEN a tooltip with flip configured + WHEN scrolling the page + THEN the tooltip should flip to the opposite end once space runs out`, async ({ + page, + }) => { + const { driver: d } = await setup(page, 'flip'); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + async function calculateYDiff() { + const tooltipBoundingBox = await tooltip.boundingBox(); + const triggerBoundingBox = await trigger.boundingBox(); + + assertBoundingBoxExists(tooltipBoundingBox); + assertBoundingBoxExists(triggerBoundingBox); + + return tooltipBoundingBox.y - triggerBoundingBox.y; + } + + // Introduce artificial spacing + await trigger.evaluate((element) => (element.style.marginTop = '2000px')); + await trigger.evaluate((element) => (element.style.marginBottom = '1000px')); + + await trigger.hover(); + await expect(tooltip).toBeVisible(); + + let yDiff = await calculateYDiff(); + expect(yDiff).toBeLessThan(0); + + await page.evaluate(() => window.scrollBy(0, 340)); + + await page.waitForTimeout(1000); + + await trigger.hover(); + await expect(tooltip).toBeVisible(); + + yDiff = await calculateYDiff(); + expect(yDiff).toBeGreaterThan(0); + }); +}); + +test.describe('Tooltip Animations', () => { + test(`GIVEN a tooltip with an animation + WHEN hovering over the trigger + THEN the tooltip should open with an animation`, async ({ page }) => { + const { driver: d } = await setup(page, 'animation'); + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + await expect(tooltip).toBeHidden(); + + // Wait for the duration of the animation (e.g., 500ms) + await page.waitForTimeout(500); + + await expect(tooltip).toBeVisible(); + }); + + test(`GIVEN an open tooltip with an animation + WHEN moving the mouse away from the trigger + THEN the tooltip should close with an animation`, async ({ page }) => { + const { driver: d } = await setup(page, 'animation'); + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + await page.waitForTimeout(500); + await expect(tooltip).toBeVisible(); + + await page.mouse.move(0, 0); + + // Wait for the duration of the animation (e.g., 500ms) + await page.waitForTimeout(500); + + await expect(tooltip).toBeHidden(); + }); +}); diff --git a/packages/kit-headless/src/utils/test-utils.ts b/packages/kit-headless/src/utils/test-utils.ts new file mode 100644 index 000000000..f29eea881 --- /dev/null +++ b/packages/kit-headless/src/utils/test-utils.ts @@ -0,0 +1,8 @@ +import { expect } from '@playwright/test'; + +export function assertBoundingBoxExists( + box: { x: number; y: number; width: number; height: number } | null | undefined, +): asserts box is { x: number; y: number; width: number; height: number } { + expect(box).not.toBeNull(); + expect(box).not.toBeUndefined(); +} From 4c7fe5a676fdedbb18661beccb647e78c3f0dc11 Mon Sep 17 00:00:00 2001 From: Christopher Woolum Date: Sat, 13 Jul 2024 14:41:19 -0700 Subject: [PATCH 02/10] fix(tooltip): small tweaks to the tooltip state --- .changeset/tasty-pugs-decide.md | 5 ++ .../headless/tooltip/examples/animation.tsx | 2 + .../tooltip/examples/arrow-styling.tsx | 49 ++----------- .../headless/tooltip/examples/transition.tsx | 2 + .../routes/docs/headless/tooltip/index.mdx | 71 ++++--------------- .../headless/tooltip/snippets/animation.css | 4 +- .../tooltip/snippets/arrow-styling.css | 38 ++++++++++ .../headless/tooltip/snippets/tooltip.css | 47 ------------ .../headless/tooltip/snippets/transition.css | 4 +- .../src/components/tooltip/tooltip-panel.tsx | 7 +- .../src/components/tooltip/tooltip-root.tsx | 2 +- 11 files changed, 70 insertions(+), 161 deletions(-) create mode 100644 .changeset/tasty-pugs-decide.md create mode 100644 apps/website/src/routes/docs/headless/tooltip/snippets/arrow-styling.css diff --git a/.changeset/tasty-pugs-decide.md b/.changeset/tasty-pugs-decide.md new file mode 100644 index 000000000..5975c3b86 --- /dev/null +++ b/.changeset/tasty-pugs-decide.md @@ -0,0 +1,5 @@ +--- +'@qwik-ui/headless': minor +--- + +Implement a beta version of the Tooltip component diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx index 94a17a8f3..45321b077 100644 --- a/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx +++ b/apps/website/src/routes/docs/headless/tooltip/examples/animation.tsx @@ -1,6 +1,8 @@ import { component$ } from '@builder.io/qwik'; import { Tooltip } from '@qwik-ui/headless'; +import '../snippets/animation.css'; + export default component$(() => { return ( diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx index f653f2626..b9a88351d 100644 --- a/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx +++ b/apps/website/src/routes/docs/headless/tooltip/examples/arrow-styling.tsx @@ -1,53 +1,14 @@ -import { component$, useStyles$ } from '@builder.io/qwik'; +import { component$ } from '@builder.io/qwik'; import { Tooltip } from '@qwik-ui/headless'; -export default component$(() => { - useStyles$(` - .tooltip-panel { - background-color: #222; - color: #fff; - padding: 15px; - border-radius: 8px; - position: relative; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - transition: opacity 0.3s ease; - opacity: 0; - } - - .tooltip-panel[data-state="open"] { - opacity: 1; - } - - .tooltip-arrow { - position: absolute; - width: 20px; - height: 10px; - overflow: hidden; - } - - .tooltip-arrow::before { - content: ''; - position: absolute; - width: 10px; - height: 10px; - background-color: #222; - top: -5px; - left: calc(50% - 5px); - transform: rotate(45deg); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - transition: top 0.3s ease; - } - - .tooltip-panel[data-state="open"] .tooltip-arrow::before { - top: -8px; - } - `); +import '../snippets/arrow-styling.css'; +export default component$(() => { return ( Hover or Focus me - - + +

Tooltip Title

This tooltip has a snazzy styled arrow!

diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx index ac88f3e92..3f7bf3fcc 100644 --- a/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx +++ b/apps/website/src/routes/docs/headless/tooltip/examples/transition.tsx @@ -1,6 +1,8 @@ import { component$ } from '@builder.io/qwik'; import { Tooltip } from '@qwik-ui/headless'; +import '../snippets/transition.css'; + export default component$(() => { return ( diff --git a/apps/website/src/routes/docs/headless/tooltip/index.mdx b/apps/website/src/routes/docs/headless/tooltip/index.mdx index 0b89dc17a..ec4b02ed8 100644 --- a/apps/website/src/routes/docs/headless/tooltip/index.mdx +++ b/apps/website/src/routes/docs/headless/tooltip/index.mdx @@ -109,50 +109,20 @@ Tooltips are useful for displaying contextual information or additional details ### Styling open tooltips ```tsx -.tooltip-panel[data-state="open"] { +.tooltip-panel[data-open] { background: lightblue; } ``` -Use the data-state attribute on the `` component to specifically style the tooltip when it's open. +Use the `data-open` and `data-closed` attributes on the `` component to specifically style the tooltip when it's open. ### Cross-browser Animations Entry and exit animations have often been a frustrating experience in web development, especially trying to animate between `display: none`, a discrete property. -Luckily, the Qwik UI team has done an excellent job of managing animations on polyfill browsers for you using the `data-state` attributes. +Luckily, the Qwik UI team has done an excellent job of managing animations on polyfill browsers for you using the `data-*` attributes. -```css -.tooltip-animation { - transform: scale(0); -} - -.tooltip-animation[data-state='open'] { - animation: tooltip-grow 0.5s ease-in-out forwards; -} - -.tooltip-animation[data-state='closing'] { - animation: tooltip-shrink 0.4s ease-in-out forwards; -} - -@keyframes tooltip-shrink { - from { - transform: scale(1); - } - to { - transform: scale(0); - } -} - -@keyframes tooltip-grow { - from { - transform: scale(0); - } - to { - transform: scale(1); - } -} -``` + ## Tooltip Behavior @@ -206,9 +176,9 @@ If Tailwind is the framework of choice, then styles can be added using the [arbi To use an animation, add the following CSS classes to the component. -- The `data-state="open"` attribute determines the animation that happens when it is first opened. +- The `data-open` attribute determines the animation that happens when it is first opened. -- The `data-state="closing"` class determines what class is added when the tooltip is **closed**. +- The `data-closed` class determines what class is added when the tooltip is **closed**. Here's the CSS imported from the example: @@ -216,7 +186,7 @@ Here's the CSS imported from the example: ### Transition declarations -Transitions use the same classes for entry and exit animations. Those being `data-state="opening"` and `data-state="closing"`. They are explained more in the `Caveats` section. +Transitions use the same classes for entry and exit animations. Those being `data-open` and `data-closed`. They are explained more in the `Caveats` section. @@ -224,24 +194,7 @@ Transitions use the same classes for entry and exit animations. Those being `dat CSS from the example: -```css -.tooltip-transition { - opacity: 0; - transition: - opacity 0.5s, - display 0.5s, - overlay 0.5s; - transition-behavior: allow-discrete; -} - -.tooltip-transition[data-state='open'] { - opacity: 1; -} - -.tooltip-transition[data-state='closing'] { - opacity: 0; -} -``` + ## Additional References @@ -279,23 +232,23 @@ To read more about tooltips you can check it out on: description: 'The space between the trigger element and the tooltip.', }, { - name: 'data-state="closing"', + name: 'data-closing"', type: 'selector', description: 'Style the element when the tooltip is closing. This occurs when the popover has a delay set.', }, { - name: 'data-state="closed"', + name: 'data-closed', type: 'selector', description: 'Style the element when the tooltip is closed.', }, { - name: 'data-state="opening"', + name: 'data-opening', type: 'selector', description: 'Style the element when the tooltip is in the process of opening. This occurs when the popover has a delay set.', }, { - name: 'data-state="open"', + name: 'data-open', type: 'selector', description: 'Style the element when the tooltip is open.', }, diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css b/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css index d450b4c2f..0ef140ab2 100644 --- a/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/animation.css @@ -2,11 +2,11 @@ transform: scale(0); } -.tooltip-animation[data-state='open'] { +.tooltip-animation[data-open] { animation: tooltip-grow 0.5s ease-in-out forwards; } -.tooltip-animation[data-state='closing'] { +.tooltip-animation[data-closing] { animation: tooltip-shrink 0.4s ease-in-out forwards; } diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/arrow-styling.css b/apps/website/src/routes/docs/headless/tooltip/snippets/arrow-styling.css new file mode 100644 index 000000000..a37f1313e --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/arrow-styling.css @@ -0,0 +1,38 @@ +.tooltip-arrow-styled-panel { + background-color: #222; + color: #fff; + padding: 15px; + border-radius: 8px; + position: relative; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + transition: opacity 0.3s ease; + opacity: 0; +} + +.tooltip-arrow-styled-panel[data-open] { + opacity: 1; +} + +.tooltip-arrow-styled-arrow { + position: absolute; + width: 20px; + height: 10px; + overflow: hidden; +} + +.tooltip-arrow-styled-arrow::before { + content: ''; + position: absolute; + width: 10px; + height: 10px; + background-color: #222; + top: -5px; + left: calc(50% - 5px); + transform: rotate(45deg); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: top 0.3s ease; +} + +.tooltip-arrow-styled-panel[data-open] .tooltip-arrow-styled-arrow::before { + top: -8px; +} diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css b/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css index ba53fa158..b0a6ae097 100644 --- a/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/tooltip.css @@ -21,50 +21,3 @@ 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } - -.tooltip-animation { - transform: scale(0); -} - -.tooltip-animation[data-state='open'] { - animation: tooltip-grow 0.5s ease-in-out forwards; -} - -.tooltip-animation[data-state='closing'] { - animation: tooltip-shrink 0.4s ease-in-out forwards; -} - -@keyframes tooltip-shrink { - from { - transform: scale(1); - } - to { - transform: scale(0); - } -} - -@keyframes tooltip-grow { - from { - transform: scale(0); - } - to { - transform: scale(1); - } -} - -.tooltip-transition { - opacity: 0; - transition: - opacity 0.5s, - display 0.5s, - overlay 0.5s; - transition-behavior: allow-discrete; -} - -.tooltip-transition[data-state='open'] { - opacity: 1; -} - -.tooltip-transition[data-state='closing'] { - opacity: 0; -} diff --git a/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css b/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css index 225be8bd1..7c7266a35 100644 --- a/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css +++ b/apps/website/src/routes/docs/headless/tooltip/snippets/transition.css @@ -7,10 +7,10 @@ transition-behavior: allow-discrete; } -.tooltip-transition[data-state='opening'] { +.tooltip-transition[data-open] { opacity: 1; } -.tooltip-transition[data-state='closing'] { +.tooltip-transition[data-closed] { opacity: 0; } diff --git a/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx b/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx index 9dad5d7d6..dbbc01d54 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx +++ b/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx @@ -11,12 +11,7 @@ export const HTooltipPanel = component$((props: HTooltipPanelProps) => { const context = useContext(TooltipContextId); return ( - + ); diff --git a/packages/kit-headless/src/components/tooltip/tooltip-root.tsx b/packages/kit-headless/src/components/tooltip/tooltip-root.tsx index 738020dc0..b3affacce 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-root.tsx +++ b/packages/kit-headless/src/components/tooltip/tooltip-root.tsx @@ -81,7 +81,7 @@ export const HTooltipRoot = component$((props: TooltipProps) => { return ( Date: Wed, 31 Jul 2024 20:45:43 -0700 Subject: [PATCH 03/10] fix: remove animations form tooltip docs and fix placement example --- .../routes/docs/headless/tooltip/examples/placement.tsx | 2 +- apps/website/src/routes/docs/headless/tooltip/index.mdx | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx index e6bc9a3e2..32362498f 100644 --- a/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx +++ b/apps/website/src/routes/docs/headless/tooltip/examples/placement.tsx @@ -24,7 +24,7 @@ export default component$(() => { - + Hover or Focus me ` compon ### Cross-browser Animations -Entry and exit animations have often been a frustrating experience in web development, especially trying to animate between `display: none`, a discrete property. +{/\* Entry and exit animations have often been a frustrating experience in web development, especially trying to animate between `display: none`, a discrete property. Luckily, the Qwik UI team has done an excellent job of managing animations on polyfill browsers for you using the `data-*` attributes. +*/} + +> Animations are not currently available in the Tooltip component. We are working on a solution to provide a more seamless experience. In the meantime, you can still use transitions. ## Tooltip Behavior @@ -170,7 +173,7 @@ If Tailwind is the framework of choice, then styles can be added using the [arbi > The arbitrary variant can be customized/abstracted even further by [adding a variant](https://tailwindcss.com/docs/plugins#adding-variants) as a plugin in the tailwind config. -## Animations +{/\* ## Animations @@ -182,7 +185,7 @@ To use an animation, add the following CSS classes to the component. Here's the CSS imported from the example: - + */} ### Transition declarations From 656e0db6e33588ccb853ec3a1e5f22581fb35779 Mon Sep 17 00:00:00 2001 From: Christopher Woolum Date: Thu, 1 Aug 2024 08:53:47 -0700 Subject: [PATCH 04/10] feat(tooltip): implement onOpenChange$ --- .../headless/tooltip/examples/onChange.tsx | 19 +++++++++++++++++++ .../routes/docs/headless/tooltip/index.mdx | 13 +++++++++++++ .../src/components/tooltip/tooltip-context.ts | 4 +++- .../src/components/tooltip/tooltip-panel.tsx | 7 ++++++- .../src/components/tooltip/tooltip-root.tsx | 6 ++++-- .../src/components/tooltip/tooltip.driver.ts | 5 +++++ .../src/components/tooltip/tooltip.test.ts | 17 +++++++++++++++++ 7 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 apps/website/src/routes/docs/headless/tooltip/examples/onChange.tsx diff --git a/apps/website/src/routes/docs/headless/tooltip/examples/onChange.tsx b/apps/website/src/routes/docs/headless/tooltip/examples/onChange.tsx new file mode 100644 index 000000000..a701eb20a --- /dev/null +++ b/apps/website/src/routes/docs/headless/tooltip/examples/onChange.tsx @@ -0,0 +1,19 @@ +import { component$, useSignal } from '@builder.io/qwik'; +import { Tooltip } from '@qwik-ui/headless'; + +export default component$(() => { + const tooltipState = useSignal<'open' | 'closed'>('closed'); + + return ( + <> + (tooltipState.value = e)} flip> + Hover or Focus me + + + Tooltip content here + + + The tooltip is {tooltipState.value} + + ); +}); diff --git a/apps/website/src/routes/docs/headless/tooltip/index.mdx b/apps/website/src/routes/docs/headless/tooltip/index.mdx index 0bf2b191f..ce01194eb 100644 --- a/apps/website/src/routes/docs/headless/tooltip/index.mdx +++ b/apps/website/src/routes/docs/headless/tooltip/index.mdx @@ -199,6 +199,13 @@ CSS from the example: +## Events + +The tooltip contains a `onOpenChange$` event that runs when the tooltip opens or closes. +This can be used to trigger additional actions when the tooltip is opened or closed. + + + ## Additional References Qwik UI aims to be in line with the standard whenever possible. Our goal is to empower Qwik developers to create amazing experiences for their users. @@ -255,6 +262,12 @@ To read more about tooltips you can check it out on: type: 'selector', description: 'Style the element when the tooltip is open.', }, + { + name: 'onOpenChange$', + type: 'QRL', + description: 'QRL handler that runs when the tooltip opens or closes.', + info: 'QRL<(state: "open" | "closed") => void>', + }, ]} /> diff --git a/packages/kit-headless/src/components/tooltip/tooltip-context.ts b/packages/kit-headless/src/components/tooltip/tooltip-context.ts index 160aa1d75..fcef105ab 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-context.ts +++ b/packages/kit-headless/src/components/tooltip/tooltip-context.ts @@ -1,4 +1,4 @@ -import { createContextId, Signal } from '@builder.io/qwik'; +import { createContextId, QRL, Signal } from '@builder.io/qwik'; export const TooltipContextId = createContextId('Tooltip'); @@ -11,6 +11,8 @@ export type TooltipContext = { triggerRef: Signal; state: Signal; + + onOpenChange$: QRL<(state: 'open' | 'closed') => void>; }; export type TriggerDataState = 'closing' | 'closed' | 'opening' | 'open'; diff --git a/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx b/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx index dbbc01d54..898e02c8c 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx +++ b/packages/kit-headless/src/components/tooltip/tooltip-panel.tsx @@ -11,7 +11,12 @@ export const HTooltipPanel = component$((props: HTooltipPanelProps) => { const context = useContext(TooltipContextId); return ( - + context.onOpenChange$(e.newState)} + id={context.localId} + > ); diff --git a/packages/kit-headless/src/components/tooltip/tooltip-root.tsx b/packages/kit-headless/src/components/tooltip/tooltip-root.tsx index b3affacce..c3b74809b 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip-root.tsx +++ b/packages/kit-headless/src/components/tooltip/tooltip-root.tsx @@ -7,6 +7,7 @@ import { useContextProvider, useId, useSignal, + $, } from '@builder.io/qwik'; import { FloatingProps, HPopoverRoot } from '../popover/popover-root'; import { TooltipContext, TooltipContextId, TriggerDataState } from './tooltip-context'; @@ -27,7 +28,7 @@ export type TooltipRootProps = { * QRL handler that runs when the tooltip opens or closes. * @param open The new state of the tooltip. */ - onOpenChange$?: QRL<(state: TriggerDataState) => void>; + onOpenChange$?: QRL<(state: 'open' | 'closed') => void>; /** * A value that determines how long before the tooltip will @@ -58,7 +59,7 @@ export const HTooltipRoot = component$((props: TooltipProps) => { gutter, delayDuration = 0, flip, - onOpenChange$: _, + onOpenChange$, ...rest } = props; @@ -74,6 +75,7 @@ export const HTooltipRoot = component$((props: TooltipProps) => { triggerRef, delayDuration, state: tooltipState, + onOpenChange$: $((e) => onOpenChange$?.(e)), }; useContextProvider(TooltipContextId, context); diff --git a/packages/kit-headless/src/components/tooltip/tooltip.driver.ts b/packages/kit-headless/src/components/tooltip/tooltip.driver.ts index 4c753a3d2..705cdbabd 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip.driver.ts +++ b/packages/kit-headless/src/components/tooltip/tooltip.driver.ts @@ -51,6 +51,10 @@ export function createTooltipDriver(rootLocator: T) { return getTrigger().all(); }; + const getOnChangeVerificationText = (state: 'open' | 'closed') => { + return rootLocator.getByText(`The tooltip is ${state}`); + }; + const getProgrammaticButtonTrigger = () => { return rootLocator.locator('button'); }; @@ -65,5 +69,6 @@ export function createTooltipDriver(rootLocator: T) { openTooltip, getProgrammaticButtonTrigger, getTooltipByTextContent, + getOnChangeVerificationText, }; } diff --git a/packages/kit-headless/src/components/tooltip/tooltip.test.ts b/packages/kit-headless/src/components/tooltip/tooltip.test.ts index b7b49dc95..5c10c1b0e 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip.test.ts +++ b/packages/kit-headless/src/components/tooltip/tooltip.test.ts @@ -221,3 +221,20 @@ test.describe('Tooltip Animations', () => { await expect(tooltip).toBeHidden(); }); }); + +test.describe('Tooltip Events', () => { + test(`GIVEN a tooltip with opOpenChange configured + WHEN hovering over the trigger + THEN the text should say "The tooltip is open"`, async ({ page }) => { + const { driver: d } = await setup(page, 'onChange'); + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + expect(d.getOnChangeVerificationText('closed')).toBeVisible(); + + await trigger.hover(); + await expect(tooltip).toBeVisible(); + + expect(d.getOnChangeVerificationText('open')).toBeVisible(); + }); +}); From 66474373741709861a4c1222c63a6d1b6c306c01 Mon Sep 17 00:00:00 2001 From: Christopher Woolum Date: Thu, 8 Aug 2024 21:55:37 -0700 Subject: [PATCH 05/10] chore: fix changeset type --- .changeset/tasty-pugs-decide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/tasty-pugs-decide.md b/.changeset/tasty-pugs-decide.md index 5975c3b86..f13016f04 100644 --- a/.changeset/tasty-pugs-decide.md +++ b/.changeset/tasty-pugs-decide.md @@ -1,5 +1,5 @@ --- -'@qwik-ui/headless': minor +'@qwik-ui/headless': patch --- -Implement a beta version of the Tooltip component +feat: implement a beta version of the Tooltip component From d401c37d077eefb78915ac29cf24a2415cf6e81e Mon Sep 17 00:00:00 2001 From: Christopher Woolum Date: Thu, 8 Aug 2024 22:12:29 -0700 Subject: [PATCH 06/10] test: update placement test to remove loop --- .../src/components/tooltip/tooltip.driver.ts | 31 +++++ .../src/components/tooltip/tooltip.test.ts | 114 +++++++++++------- 2 files changed, 101 insertions(+), 44 deletions(-) diff --git a/packages/kit-headless/src/components/tooltip/tooltip.driver.ts b/packages/kit-headless/src/components/tooltip/tooltip.driver.ts index 705cdbabd..3012f7f0c 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip.driver.ts +++ b/packages/kit-headless/src/components/tooltip/tooltip.driver.ts @@ -59,6 +59,35 @@ export function createTooltipDriver(rootLocator: T) { return rootLocator.locator('button'); }; + const selectOption = async (placement: string) => { + const dropdown = rootLocator.getByLabel('Select Tooltip Placement:'); + await dropdown.selectOption(placement); + }; + + const validateTooltipPosition = async (placement: string) => { + const triggerBoundingBox = await getTrigger().boundingBox(); + const tooltipBoundingBox = await getTooltip().boundingBox(); + + switch (placement) { + case 'top': + expect(tooltipBoundingBox?.y).toBeLessThan(triggerBoundingBox?.y ?? 0); + break; + case 'right': + expect(tooltipBoundingBox?.x).toBeGreaterThan( + (triggerBoundingBox?.x ?? 0) + (triggerBoundingBox?.width ?? 0), + ); + break; + case 'bottom': + expect(tooltipBoundingBox?.y).toBeGreaterThan( + (triggerBoundingBox?.y ?? 0) + (triggerBoundingBox?.height ?? 0), + ); + break; + case 'left': + expect(tooltipBoundingBox?.x).toBeLessThan(triggerBoundingBox?.x ?? 0); + break; + } + }; + return { ...rootLocator, locator: rootLocator, @@ -70,5 +99,7 @@ export function createTooltipDriver(rootLocator: T) { getProgrammaticButtonTrigger, getTooltipByTextContent, getOnChangeVerificationText, + selectOption, + validateTooltipPosition, }; } diff --git a/packages/kit-headless/src/components/tooltip/tooltip.test.ts b/packages/kit-headless/src/components/tooltip/tooltip.test.ts index 5c10c1b0e..13baa4053 100644 --- a/packages/kit-headless/src/components/tooltip/tooltip.test.ts +++ b/packages/kit-headless/src/components/tooltip/tooltip.test.ts @@ -74,50 +74,76 @@ test.describe('Keyboard Behavior', () => { }); test.describe('Placement and Positioning', () => { - const placements = ['top', 'right', 'bottom', 'left'] as const; - - test.describe('Tooltip Placements', () => { - placements.forEach((placement) => { - test(`GIVEN a tooltip with placement set to ${placement} - WHEN hovering over the trigger - THEN the tooltip should appear at the ${placement} of the trigger`, async ({ - page, - }) => { - const { driver: d } = await setup(page, 'placement'); - - const dropdown = page.getByLabel('Select Tooltip Placement:'); - await dropdown.selectOption(placement); - - const tooltip = d.getTooltip(); - const trigger = d.getTrigger(); - - await trigger.hover(); - - await expect(tooltip).toBeVisible(); - - const triggerBoundingBox = await trigger.boundingBox(); - const tooltipBoundingBox = await tooltip.boundingBox(); - - switch (placement) { - case 'top': - expect(tooltipBoundingBox?.y).toBeLessThan(triggerBoundingBox?.y ?? 0); - break; - case 'right': - expect(tooltipBoundingBox?.x).toBeGreaterThan( - (triggerBoundingBox?.x ?? 0) + (triggerBoundingBox?.width ?? 0), - ); - break; - case 'bottom': - expect(tooltipBoundingBox?.y).toBeGreaterThan( - (triggerBoundingBox?.y ?? 0) + (triggerBoundingBox?.height ?? 0), - ); - break; - case 'left': - expect(tooltipBoundingBox?.x).toBeLessThan(triggerBoundingBox?.x ?? 0); - break; - } - }); - }); + test(`GIVEN a tooltip with placement set to top + WHEN hovering over the trigger + THEN the tooltip should appear at the top of the trigger`, async ({ page }) => { + const { driver: d } = await setup(page, 'placement'); + + const placement = 'top'; + d.selectOption(placement); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + + await expect(tooltip).toBeVisible(); + + d.validateTooltipPosition(placement); + }); + + test(`GIVEN a tooltip with placement set to right + WHEN hovering over the trigger + THEN the tooltip should appear at the right of the trigger`, async ({ page }) => { + const { driver: d } = await setup(page, 'placement'); + + const placement = 'right'; + d.selectOption(placement); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + + await expect(tooltip).toBeVisible(); + + d.validateTooltipPosition(placement); + }); + + test(`GIVEN a tooltip with placement set to bottom + WHEN hovering over the trigger + THEN the tooltip should appear at the bottom of the trigger`, async ({ page }) => { + const { driver: d } = await setup(page, 'placement'); + + const placement = 'bottom'; + d.selectOption(placement); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + + await expect(tooltip).toBeVisible(); + + d.validateTooltipPosition(placement); + }); + + test(`GIVEN a tooltip with placement set to left + WHEN hovering over the trigger + THEN the tooltip should appear at the left of the trigger`, async ({ page }) => { + const { driver: d } = await setup(page, 'placement'); + + const placement = 'left'; + d.selectOption(placement); + + const tooltip = d.getTooltip(); + const trigger = d.getTrigger(); + + await trigger.hover(); + + await expect(tooltip).toBeVisible(); + + d.validateTooltipPosition(placement); }); test(`GIVEN a tooltip with a gutter configured From 676e0ee98544f459f6bcbcf91b6bce8199abd113 Mon Sep 17 00:00:00 2001 From: jack shelton Date: Sun, 18 Aug 2024 13:42:13 -0500 Subject: [PATCH 07/10] fix: remove breaking examples and tooltip route --- .../routes/docs/headless/tooltip/index.mdx | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/apps/website/src/routes/docs/headless/tooltip/index.mdx b/apps/website/src/routes/docs/headless/tooltip/index.mdx index ce01194eb..22d654297 100644 --- a/apps/website/src/routes/docs/headless/tooltip/index.mdx +++ b/apps/website/src/routes/docs/headless/tooltip/index.mdx @@ -3,6 +3,7 @@ title: Qwik UI | Tooltip --- import { statusByComponent } from '~/_state/component-statuses'; + import styles from './snippets/tooltip.css'; @@ -118,13 +119,6 @@ Use the `data-open` and `data-closed` attributes on the `` compon ### Cross-browser Animations -{/\* Entry and exit animations have often been a frustrating experience in web development, especially trying to animate between `display: none`, a discrete property. - -Luckily, the Qwik UI team has done an excellent job of managing animations on polyfill browsers for you using the `data-*` attributes. - - -*/} - > Animations are not currently available in the Tooltip component. We are working on a solution to provide a more seamless experience. In the meantime, you can still use transitions. ## Tooltip Behavior @@ -173,20 +167,6 @@ If Tailwind is the framework of choice, then styles can be added using the [arbi > The arbitrary variant can be customized/abstracted even further by [adding a variant](https://tailwindcss.com/docs/plugins#adding-variants) as a plugin in the tailwind config. -{/\* ## Animations - - - -To use an animation, add the following CSS classes to the component. - -- The `data-open` attribute determines the animation that happens when it is first opened. - -- The `data-closed` class determines what class is added when the tooltip is **closed**. - -Here's the CSS imported from the example: - - */} - ### Transition declarations Transitions use the same classes for entry and exit animations. Those being `data-open` and `data-closed`. They are explained more in the `Caveats` section. @@ -300,7 +280,3 @@ To read more about tooltips you can check it out on: #### Complex HTML: - -#### Arrow Styling: - - From a8023f1ebb5d836fd255fd97b22d6ae8a0bc5273 Mon Sep 17 00:00:00 2001 From: jack shelton Date: Sun, 18 Aug 2024 13:48:13 -0500 Subject: [PATCH 08/10] fix: checkbox tests preventing us from opening pw --- .../src/components/checkbox/checkbox.test.ts | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/kit-headless/src/components/checkbox/checkbox.test.ts b/packages/kit-headless/src/components/checkbox/checkbox.test.ts index 8e2e311aa..75b808902 100644 --- a/packages/kit-headless/src/components/checkbox/checkbox.test.ts +++ b/packages/kit-headless/src/components/checkbox/checkbox.test.ts @@ -109,10 +109,8 @@ test.describe('checklist', () => { }); test(`GIVEN checklist with all unchecked checkboxes - WHEN all checkboxes are checked - the chekbox with aria-controls should have aria-checked true`, async ({ - page, - }) => { + WHEN all checkboxes are checked with space + the tri state checkbox should have aria-checked true`, async ({ page }) => { const exampleName = 'test-list'; const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toBeVisible(); @@ -122,7 +120,7 @@ test.describe('checklist', () => { }); test(`GIVEN checklist with all unchecked checkboxes - WHEN the checklist's checkbox is checked + WHEN the checklist's checkbox is checked with space THEN all chekboxes should have aria-checked true`, async ({ page }) => { const exampleName = 'test-list'; const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); @@ -135,7 +133,7 @@ test.describe('checklist', () => { // TODO: reme two part of test by adding new test file test(`GIVEN checklist with all unchecked checkboxes - WHEN the checklist's checkbox is checked twice + WHEN the checklist's checkbox is checked twice using space THEN all chekboxes should go from aria-checked true to aria-checkded false`, async ({ page, }) => { @@ -278,8 +276,8 @@ test.describe('checklist', () => { }); test(`GIVEN checklist with all unchecked checkboxes - WHEN all checkboxes are checked - the chekbox with aria-controls should have aria-checked true`, async ({ + WHEN all checkboxes are checked using click + THEN the checkbox with aria-controls should have aria-checked true`, async ({ page, }) => { const exampleName = 'test-list'; @@ -291,8 +289,8 @@ test.describe('checklist', () => { }); test(`GIVEN checklist with all unchecked checkboxes - WHEN the checklist's checkbox is checked - THEN all chekboxes should have aria-checked true`, async ({ page }) => { + WHEN the checklist's checkbox is checked by clicking + THEN all checkboxes should have aria-checked true`, async ({ page }) => { const exampleName = 'test-list'; const { getTriCheckbox, getCheckbox } = await setup(page, exampleName); await expect(getTriCheckbox()).toBeVisible(); @@ -304,7 +302,7 @@ test.describe('checklist', () => { // TODO: reme two part of test by adding new test file test(`GIVEN checklist with all unchecked checkboxes - WHEN the checklist's checkbox is checked twice + WHEN the checklist's checkbox is checked twice using click THEN all chekboxes should go from aria-checked true to aria-checkded false`, async ({ page, }) => { @@ -320,23 +318,24 @@ test.describe('checklist', () => { await expect(getCheckbox().nth(1)).toHaveAttribute('aria-checked', 'false'); await expect(getCheckbox().nth(2)).toHaveAttribute('aria-checked', 'false'); }); - test(`GIVEN checklist with checkboxes - WHEN the values of aria-controls are search - IT should always return a valid, non-duplicate, checkboxes`, async ({ page }) => { - const { getTriCheckbox } = await setup(page, 'test-list'); - await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); - const magic = await getTriCheckbox().getAttribute('aria-controls'); - expect(magic).not.toBe(null); - const idArr = magic!.split(' '); - expect(isUniqArr(idArr)).toBe(true); - for (let index = 0; index < idArr.length; index++) { - const elementId = idArr[index]; - const PosCheckbox = page.locator(`#${elementId}`); - await expect(PosCheckbox).toBeVisible(); - const role = await PosCheckbox.getAttribute('role'); - expect(role).toBe('checkbox'); - } - }); + + // test(`GIVEN checklist with checkboxes + // WHEN the values of aria-controls are search + // IT should always return a valid, non-duplicate, checkboxes`, async ({ page }) => { + // const { getTriCheckbox } = await setup(page, 'test-list'); + // await expect(getTriCheckbox()).toHaveAttribute('aria-controls'); + // const magic = await getTriCheckbox().getAttribute('aria-controls'); + // expect(magic).not.toBe(null); + // const idArr = magic!.split(' '); + // expect(isUniqArr(idArr)).toBe(true); + // for (let index = 0; index < idArr.length; index++) { + // const elementId = idArr[index]; + // const PosCheckbox = page.locator(`#${elementId}`); + // await expect(PosCheckbox).toBeVisible(); + // const role = await PosCheckbox.getAttribute('role'); + // expect(role).toBe('checkbox'); + // } + // }); test(`GIVEN a controlled checklist with a checklist signal of true and default checkboxes as children WHEN a child checkbox is unchecked From 04d384bbce99f068932a1ab5381c0231f552e6e8 Mon Sep 17 00:00:00 2001 From: jack shelton Date: Sun, 18 Aug 2024 13:56:18 -0500 Subject: [PATCH 09/10] refactor: deprecate popover hover prop in favor of tooltip --- .../headless/popover/examples/corners.tsx | 4 +- .../docs/headless/popover/examples/hover.tsx | 15 ------- .../headless/popover/examples/placement.tsx | 4 +- .../headless/popover/examples/test-hide.tsx | 2 +- .../routes/docs/headless/popover/index.mdx | 7 +-- .../src/components/popover/popover-root.tsx | 1 + .../src/components/popover/popover.test.ts | 45 ++----------------- 7 files changed, 11 insertions(+), 67 deletions(-) delete mode 100644 apps/website/src/routes/docs/headless/popover/examples/hover.tsx diff --git a/apps/website/src/routes/docs/headless/popover/examples/corners.tsx b/apps/website/src/routes/docs/headless/popover/examples/corners.tsx index 28cf1e47e..d1de86ed7 100644 --- a/apps/website/src/routes/docs/headless/popover/examples/corners.tsx +++ b/apps/website/src/routes/docs/headless/popover/examples/corners.tsx @@ -3,9 +3,9 @@ import { Popover } from '@qwik-ui/headless'; export default component$(() => { return ( - +
- Hover over me + Click me
I am on the top-right corner! diff --git a/apps/website/src/routes/docs/headless/popover/examples/hover.tsx b/apps/website/src/routes/docs/headless/popover/examples/hover.tsx deleted file mode 100644 index 62eb69713..000000000 --- a/apps/website/src/routes/docs/headless/popover/examples/hover.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { component$ } from '@builder.io/qwik'; -import { Popover } from '@qwik-ui/headless'; - -export default component$(() => { - return ( - -
-

I'm a mini tooltip!

- Hover over me -
- - I am anchored to the trigger! -
- ); -}); diff --git a/apps/website/src/routes/docs/headless/popover/examples/placement.tsx b/apps/website/src/routes/docs/headless/popover/examples/placement.tsx index 6628b8c38..ebc1b76f3 100644 --- a/apps/website/src/routes/docs/headless/popover/examples/placement.tsx +++ b/apps/website/src/routes/docs/headless/popover/examples/placement.tsx @@ -3,10 +3,10 @@ import { Popover } from '@qwik-ui/headless'; export default component$(() => { return ( - +

popover on the right ⤵️

- Hover over me + Click me
I am anchored to the trigger! diff --git a/apps/website/src/routes/docs/headless/popover/examples/test-hide.tsx b/apps/website/src/routes/docs/headless/popover/examples/test-hide.tsx index d00b6e61f..68bf16bd2 100644 --- a/apps/website/src/routes/docs/headless/popover/examples/test-hide.tsx +++ b/apps/website/src/routes/docs/headless/popover/examples/test-hide.tsx @@ -4,7 +4,7 @@ export default component$(() => { const { hidePopover } = usePopover('hide-id'); return ( - + Click me My Hero! diff --git a/apps/website/src/routes/docs/headless/popover/index.mdx b/apps/website/src/routes/docs/headless/popover/index.mdx index 9684301ec..adeefe34a 100644 --- a/apps/website/src/routes/docs/headless/popover/index.mdx +++ b/apps/website/src/routes/docs/headless/popover/index.mdx @@ -3,6 +3,7 @@ title: Qwik UI | Popover --- import { statusByComponent } from '~/_state/component-statuses'; + import styles from './snippets/popover.css'; @@ -273,12 +274,6 @@ Instead, the popover will be fixed position, and you can use CSS to position it. The `Popover.Root` component is designed for positioning elements that float and facilitating interactions with them. -### Hover - -If we'd like to show the `Popover.Panel` on hover, we can use the `hover` prop. - - - ### Custom Floating Position By default, popovers will float below the trigger component. diff --git a/packages/kit-headless/src/components/popover/popover-root.tsx b/packages/kit-headless/src/components/popover/popover-root.tsx index 1c71b3bf9..12a86d0f5 100644 --- a/packages/kit-headless/src/components/popover/popover-root.tsx +++ b/packages/kit-headless/src/components/popover/popover-root.tsx @@ -14,6 +14,7 @@ export type PopoverRootProps = { manual?: boolean; ref?: Signal; floating?: boolean | TPlacement; + /** @deprecated Use the tooltip instead, which adheres to the WAI-ARIA design pattern. */ hover?: boolean; id?: string; 'bind:anchor'?: Signal; diff --git a/packages/kit-headless/src/components/popover/popover.test.ts b/packages/kit-headless/src/components/popover/popover.test.ts index e561b1b9b..ca1248daa 100644 --- a/packages/kit-headless/src/components/popover/popover.test.ts +++ b/packages/kit-headless/src/components/popover/popover.test.ts @@ -148,52 +148,15 @@ test.describe('Mouse Behavior', () => { await expect(secondPopover).toBeHidden(); }); - test(`GIVEN a popover with hover enabled - WHEN hovering over the popover - THEN the popover should appear above the trigger`, async ({ page }) => { - const { driver: d } = await setup(page, 'hover'); - - const popover = d.getPopover(); - const trigger = d.getTrigger(); - - await trigger.hover(); - - await expect(popover).toBeVisible(); - - const popoverBoundingBox = await popover.boundingBox(); - const triggerBoundingBox = await trigger.boundingBox(); - - const triggerTopEdge = triggerBoundingBox?.y ?? Number.MAX_VALUE; - - expect(popoverBoundingBox?.y).toBeLessThan(triggerTopEdge); - }); - - test(`GIVEN an open popover with hover enabled - WHEN hovering await from the popover - THEN the popover should disappear`, async ({ page }) => { - const { driver: d } = await setup(page, 'hover'); - - const popover = d.getPopover(); - const trigger = d.getTrigger(); - - await trigger.hover(); - - await expect(popover).toBeVisible(); - - await page.mouse.move(0, 0); - - await expect(popover).toBeHidden(); - }); - test(`GIVEN a popover with placement set to right - WHEN hovering over the popover + WHEN clicking the popover THEN the popover should appear to the right of the trigger`, async ({ page }) => { const { driver: d } = await setup(page, 'placement'); const popover = d.getPopover(); const trigger = d.getTrigger(); - await trigger.hover(); + await trigger.click(); await expect(popover).toBeVisible(); @@ -456,8 +419,8 @@ test.describe('Keyboard Behavior', () => { }); test(`GIVEN an open programmatic popover - WHEN the hidePopover function is called - THEN the popover should be hidden`, async ({ page }) => { + WHEN the hidePopover function is called + THEN the popover should be hidden`, async ({ page }) => { const { driver: d } = await setup(page, 'test-hide'); // Initial open From d1eacd6e2cd9af917fabf0ab7587a55609bf633e Mon Sep 17 00:00:00 2001 From: jack shelton Date: Sun, 18 Aug 2024 14:08:57 -0500 Subject: [PATCH 10/10] latest --- ...closed-popover-popover-chrome-108-darwin.png | Bin 0 -> 8305 bytes ...opened-popover-popover-chrome-108-darwin.png | Bin 0 -> 8305 bytes ...closed-tooltip-popover-chrome-108-darwin.png | Bin 0 -> 8123 bytes ...opened-tooltip-popover-chrome-108-darwin.png | Bin 0 -> 10875 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/kit-headless/src/components/popover/popover.test.ts-snapshots/closed-popover-popover-chrome-108-darwin.png create mode 100644 packages/kit-headless/src/components/popover/popover.test.ts-snapshots/opened-popover-popover-chrome-108-darwin.png create mode 100644 packages/kit-headless/src/components/tooltip/tooltip.test.ts-snapshots/closed-tooltip-popover-chrome-108-darwin.png create mode 100644 packages/kit-headless/src/components/tooltip/tooltip.test.ts-snapshots/opened-tooltip-popover-chrome-108-darwin.png diff --git a/packages/kit-headless/src/components/popover/popover.test.ts-snapshots/closed-popover-popover-chrome-108-darwin.png b/packages/kit-headless/src/components/popover/popover.test.ts-snapshots/closed-popover-popover-chrome-108-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..51e8c3f3a9e14f83525f5706445b6c87f3fed1f5 GIT binary patch literal 8305 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 JKR)v9{{Uz$jeP(B literal 0 HcmV?d00001 diff --git a/packages/kit-headless/src/components/popover/popover.test.ts-snapshots/opened-popover-popover-chrome-108-darwin.png b/packages/kit-headless/src/components/popover/popover.test.ts-snapshots/opened-popover-popover-chrome-108-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..51e8c3f3a9e14f83525f5706445b6c87f3fed1f5 GIT binary patch literal 8305 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 JKR)v9{{Uz$jeP(B literal 0 HcmV?d00001 diff --git a/packages/kit-headless/src/components/tooltip/tooltip.test.ts-snapshots/closed-tooltip-popover-chrome-108-darwin.png b/packages/kit-headless/src/components/tooltip/tooltip.test.ts-snapshots/closed-tooltip-popover-chrome-108-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..e006f20774fad6d7fdbb8ca14a972bf811f31ca3 GIT binary patch literal 8123 zcmeHM{a@189{=_$`D>I|777DcMO$$?IiueTdu-R>; zuGzFybj_yuRX{UPQN*6K&4PRY@&VD%P*BM!U=9R=(}@HUvQ{_wW1q2m~zykBgr#e+vW&T@ePtqQoOXdmxV3WekGcsQbVE z>S#*QxPo4lPqnm78GZWywk_WGhX)=yg!W=^Tq4wSSv zDQA={I#+8VmPm}g^L|DlCj=J*UW>qdplb!BF?;j@@^oGFE*dY`pF_jD2ce}PoIA02 z1KM2b2%-Hq+<=y&&HGDtqha|bXfX)8x7mi;w#k+fHs;t=!lsBewFr{!;?t?JmX|P0BSm5l>CU)ZWwO(vf_UwTOPv&VjKkdMW4}3=W_v zt4GJjtIEpC@=sQIiFcIdZ6%~V71KU@W&OLjqbr6e)-H!$D<5oWfgrzn<3M`spVv1_ zyStC7TC*x2Us)3#6chx*mO(r0otmgNS+?s=0X=%6mxoCne~~tPEeJt0?$|f`RW7eu`S6-9lW)=~ zR?5rER5gnxQ_fqB1z()}MzyOloI7dgndqyN7x^TTpZxR@Ci>|OfuT26J9z)1r^#s% zWcGd!oZX8@)Y$n{Ldwt&Jg_{xAn7!VVH)FQy!orZCo$U3GSj7>q=_1P`}$(L%7PXC zwDv7^-M@Zykv#n58jq@#2?j5$AgjeJszooCSAYAEeE-5Sy`IYyBFHK?|Hz4n3H=`- zjJxYo2G5&j+pxqK7Bwtq`Z)j0aUP#9YdOC}o9P^X=aZo9?xxgfCpCifHYCQdi9$hG z)v-`q*};mju5!_+PFXmW=Sd^RU(MUs^?y7borL2o3Rn^AFy=d)@0|ib9P_ zCUd3Bj_S4stZB3qb9VSi!S*=3f-yG%>z~%XgPpzaAXCtkggL`Rg%{e6^UY>@@At@CoRClqG`he zcsY4RAB5q!{`#6^-2kD))LmZeWo&a}HsSGjvUzrjI&U$C1FW_8?cAyoRISpUm=XfO zgQN#}G9^0JoRLbmPM2^)n5Hg0k4tBR#MQ=8(j#D)O%gS~60;&ivyVN=FCSf&qzfVF z{n=$3pw|T*brhx3wi7LhM37Y!m&C!A^J$~EE|CFTS+n6V$}=Zzx#Oc=L=#v+@9Q8ZvEgphWrS3&dsn~yRoXDzd zzq-y#a|vr6y0Tt(*}eD>}aMUBLYJrT8*N`G%Nu=D9K6XH#SJyO>ilxnv z9}^bcC5af4baQ}%-%%LWrO6wMYjr)6gr(5F;Wg(VC@8h7Z~%PbfHi+tH{t5)N(J!l zt&cr@x}tq+V^w_P1ix39PK#3rc+ujb<1k#hHw!_KjR*3&qjLNVMe^&LacO=foYyj5 z^}c+LQL>6UPXZ$DVfLGI(hv8nNZXY@loiVr@&Q4aQcvcab`&|wXb={e25J&05xJMM zlJQw?*ygtR#|T`RJ$4L&xO>6fKuhSr>rC(NJE5gy;7y7Tt49TojT01RdQ{)jn_jV6 z89Pf#@oX6W89|)#7i3MjE_P&jgd#<64hch5EFT6jz8Xc` zvGYF+kk#@}w4nwQ9+?oxj;P~s>RI;Oh1zXbrV<& zeX)9t6znF`YDRH|VKfCM>s8qWSSACwaTqLJ1H{n;{Jw8ijBZ2F`qbNDoXDQp)g#N+ zL>>mNKuar%O=x_vkYW*R-v#T(sn^ra`hu*a2K?-mJ!IXH_}#OqTjswj8{ZZWyls zP+}>^%s^j5A7-wslnW=Rev?>`xD!3K<-iRD>?h_UJ5pYsaP>{P1KPw+V7tWN|J*i3 zzqfE{X}gHD-PhXgrfv7|wsM87G-In^*{XH6%H;oW0oPjYy7MQrn!I+HLeJrDTYTc+ zLK(T`#=lF>pD$XdF-I+4ING%?-2l2mSM7JBVXfmiG;G_nP>%jD27Y)=*K;$lABz^{ Oq5Xk}zUJ&Xap7OGurW^n literal 0 HcmV?d00001 diff --git a/packages/kit-headless/src/components/tooltip/tooltip.test.ts-snapshots/opened-tooltip-popover-chrome-108-darwin.png b/packages/kit-headless/src/components/tooltip/tooltip.test.ts-snapshots/opened-tooltip-popover-chrome-108-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..8f3a2566a7cef3617c3543ae1a7312f5d700d422 GIT binary patch literal 10875 zcmeI2>06WMwufJ|O05dJms=2Jur*380tyI*v8@9}5Nd6uh={E~M1~-dAwUS-Ry|e) z4f7Z+BA%^)f)ZvNL53s(G6y0<5`hpPfj|= zq(07w%+t*Fe82kz_G;BdeYf^BU(am;?e-(mlib0a&CWrTMAQR|(2=5BZ9KR1^oKKgn_r5eti=4{h;*gP~Ro6W?p z?3u5tXH1fPg(iaL$j**)Hk-9zKiH9p*)UJ^4Hl#PJP|#A%)~xN&&z8P(PR0?Wos-u z!*VPvOTn@dE-T@(5-uy@e?SRmTq|Y$!R0ZtBX-=tp;}56Kiy~Ou0hw#v_?H$rIHQs zrh+gm^&P^tX&DZ~DpGkWadh8rSB-|kbN1^x*)RvA2K4&;+}zyi=Zi@Jg0?IIU85+~ z9HEQKInD(7`YpEih@AZK0Ve50@yTZgW%8(j$G`0tSB@r$Dh9#~Y(skWOf>Vt2WHwP z@!{Q%yi+tw91V9wsg1=njLQ)to^exr(00izXWKulil44q5R}x2TbfN7xoVZtln^x- z*~1Lg6y-&oq}#meI^h$VRx@4iP!~HNJ0PnntXq<_;zf^N*QNW_(9Uxp7icU>)c`^WlYo zV@$Y* z;TnELhbfKcn|W7X3}QAV6p)|dd;DnBE4n86>FMb=46JuFEWWX{#ha#>dU`Y@sH{SI zALk}0#=Xk}k4mLrhQi`vb67QOwXSjQB^~<*K20mIAFqM2Yo}`^EoS(M2L)Ea-H&c; z+Viz(-o>AcV?W-#VeDkDQSmf`TyZX$P4MyYQOu2}WLXmfM;mYAhyUCFcasrf707U= zT3r}7G3H|?T~*<>P}daQ+Z*uEJ39Z z)k@h}yMOzEK6chb`Rfjk+|0b_Av_Fw`{$Ys5ieHh`-4;AXJ2n14}>!L(`xawgiGc` z28-|*9n_6ASKJq3quwmkAK zN9GXBTKH7#A`9A@6QA{g%yFO%h_@<;(h~;g2^D(Hof%+ydKe zu1O&`4Dn^1hKG_2`xe`X&fxN!Z>E`f+0tzkvZ;Z^uF`}qu0Ya?ri<4odDD`8{r&x$ z37Z#qG#YtuFq$_wzAuVLU1)rFcmEcRvOzPz6}(()8G;Mi_3Pt1XfG&`VeR`lVdulh z5Zim{Hev|qLlBC!izC-_413I(+>XN?mE!8j>`CNAu)nq}>l!DSfZaYx6<9NZa6yie zx7%2eMT~@4nCnTCaWL$&5j2B)xZ@m&L<+Nx7IR@7+7LPdc+A7w_nw6GmDxq^!mzZ; z75~6uc;0Z=bpMWf1Xkn#se!uVXm+Z$am-fmrPf!vF1ffY`^fX;9Al?g8xo&7cH33` z0B;!i{+N8`stfm&IJ5PVu5&M6=As(+EaxRC1782vm#j#*z9R+gZ&P<3FN>X@a8xgj zX4wS&Bn>bnSWzTnxMmJbE%g|^X<&U{zOYhf-&TU8oS_OSmr_6cp-so_zpBIf`{4Dm zbM1elkRsSpvzl^@;>cHYjIxsx<_cK+mAa0*3E5u9qqy0%QB(bb+^D{Q3|t;(^ue(L ztBW7sF)zaTHpZ;^9Gj_^ek@21e0%FF`EzaOcR^HFT&$$oG>3CZ`PS9BBPb_{U+9(* zIQI57iK0_8RpC>_53T6ivH!}tX8LxwYOeYZZR59I4T>&S(n71B+N$|JY2Q#-%~S`I z#B-GOp3Unf!6;UP?xx*an;&u{IptvP0%jo4 zJrg7;Iq?h6IMTS#e;gg+KEbe`w_+=>X@?qdb+CZLV9ZbU6cITZ4d-rBM!4u{rw8jD zGhQz7_%d8B;S+4vUm|sPdb$h-Q9ukRit!@p0!_2v8`c1-B>3;5{C-i4k=KU$1n#ro zoIz=kjOClbno2Wjz z5>8X^x<*GHcuZtaE2_du>tshH6UCgBtJj@VFO8YHjL(gviYM53#eNwKcT{{V_#_+7 zkONzm_ir_SxREiTBnLU8Ts{8Wnf$8o+`z}Lvq(IUVP-;dKRZL zsj@y(LWHc3C`4{`+toO03#oRGkU>jGa#nmq3e8Z@71dA^;4X5|k0>`;G%QJ$CcGNX z=a+9w+V{1-@5N)t((c(dJ0X9xf?T#wJ>j*1VeZ4yT@Ss8b06EQ1lhspJcbc(zrq8D zWRtYSE$NP+nl7utv{qwBf`fv_=ZKOC>tRGG?s@imi}GX7)BTg3WN&}*@=e>8>{wPc znC5jlYp5AP|4tt_ zAcQPHo!uKuJ=`S|T_HRAt*#JjEl;e_VOQWC8 zkQ;G?evibfuln$>wb_=3#>Krk4z8tLUB#89CbY$w;VLQH!TN0D4MD31l9Pe3nqo>C z(d+E|G*6=ouK(S3^P5b`fj}(r>(dl|8swnB@?mqz{3<<;vod7$`pxgBE@*%oEy3+3 z=5D;eQPgsw^_l01r((woS!TU-ua!SX*MWrh0sjGPF#AGJl$^?f%o$pyu{#SFMj0>6 z=%|UF_LvPFtd9IN930JMB8vh$CB4bs8(bDPSS@gKtqyiGLhhVphKTIh>AKju-8oH~Sq+|K`)6b!U=Yhp+babd)p~tTe1O*o@9ZUpev6jZIJbh@Y>(o~H}+&H#C1-?VK^M9=?FHU1sN({c8| z$&)7|=DWP2<#OPyj*bp`a#CsDmpEFaNtsB9)ul`^V+;Ab%ADE1i^qYEhyCZo8@fH~v% zK*8MKM?g{?9l++1Z8GdzDc*C0gw*Ogh3XZ27 zT0l$-86rx--spAeHUQ-=zj{YPM3YgpO;c|)k4lEjvzO^x7aMuf$45FionYsu=*Z}3 zplNb}Kz!MS?dSa2x%fVsMup}FWc&v7W$G#7pN{VCbWl>eOWL|@0O`5(Lvd0jP01W@FK|WmLHty(UvR## zXL290=M?$q4$pj4I}iaiT3B|xTtr9)4pAZGTs&EDQ6Yx(hLZ8zWRK;mE9+awix~NP zgS%__*FeMrU;u4hOm_7MTMBN9}TenN1^)@~nC9%!KzFwvRr65kv$8*e%gQhpSO zF9Pg=Bb<2j>utCJA-u@CW}2UQtl&@pRCrnYE{rHAWF+i^>S1B~?2m?6;@|1!KIu?N znj^qL#N8~^NL@538_iCP@iY9`XsO>`T?v4^j-X7jFwMZpWz2@W)d@x{+x_iMXpHoq)J&ABzrAV{^(#}x1Rz5b%9n&L3={RtRaCvfMOw$s zeRQ08zS6rYA|R}C_)5II)sGD)OG}O)+s0CfswYeONDZopsb@WcvQ~>$@PTswX8O7< zwz~)@lYPoBO|+$^qXPAkltX-;sMQxYT}KAaE`)jz)fU$Ai!*@kRtsFRXGQ<{hl1%5 zC|6p08r1Vkoi3{57WIoG$Qw$bc2B{1ou&lbch91uLK=XzUnF0FJ$MfK_gs01lke0C z8htdOVkmY}VEfRkls%g4l21t4v3Jyu?w;?6CrG3WjUz$Ax1`~bWzb1*^7iC}RGj>! ze*G=Xh1 zjw2XT#%>D~;7TC*lkXtLVESjBoTGMw@26CeA)TN@@k6KKbx8_i3HR_s@exk`U|idr zMU9A#`&51@rtj0ZVbkuu5%Ja#Hv{xQ38tT_^GysSqJ4I3_P4j*hBo@2BUE zfm6Nb3c$@o-KRhvQz@Sa`T{E1pfuvL$80=bw>?^W&rRApa5}` zoUhDrq(lH9SGeccno9+&*1K1YvyGy9?;5Ou+m!{z#}@l7jp(y}To0cS8S1RW@ZHS9_a0Pxsazr`QIYG)gflX+K$|F*+D^A^s)%KtFkzww5FTs2K=Xy!nQ zsBvN>&2-QHep|<=wNy8f_DC!$-L_PxHjL!YTh7I(uG@LIsOB8O0p;VKv~0Mwy-aR| zn+`ZW(@|4`vK4aLnk8RQoVe@awb+Nc!$@myPBD4TsOkX9wy%5s_oyB(nJPNVibQbv z^77goQiIdd%&QdKN^PCCTVL%j3m<-oq#N+~Eeh%geoGs}rGXNBK3W7_5duAjRA1Gp zNa81Pn5kkB3DAc?-x5}Nx6_ul*k~6q^Fo^lFoJqqbQ}mEC!1jjWwzOgN9QbfutWK< zE-e$b7znK>Hc(JR2f>sNIU?b>@S`q@Qwb`oK&e=~gQchItZ3>b!A1rYl~s^YKSZ(t z{SlzL8dLnyeLBv+KQ3>ytG^8$g|SYC`Xado>c)MLvRRNt@&E~K0mVY4<%4!o z_4ywwnIE4-N7IhqcW(w=%`dL#|5n>D{5$j#!nB~^U^fVz?5{4s-FidjUae!837wGg z&U!t|)43_0&UiEwEeLgktsOUfKakRGUFygEKB7zn*})}MJ0LlGpjHkvlxV6R8VfE7 zwHVDSOaQXJ)%pkaP3b89f)OoDEY*RsDFk)>AoGra-8|@q7hZpvQ>ONki1tRK~1;+iZTgGD;M19sSi!XFh=MLA5 zY8z-#;hJ?EM7MIns&|6b+~6qmF+VlmPeRhjNUXE@Rsl!SE&cKVfcKXE?KpO8e*{!A zk@!+Og;RZdHR2>xPXfVZTapdzFP=j&AHOteLVHEl!hZbT4Ye``Qs9&C(vzV0V5Xia zyMZYFLW71HyC7CwAt|Sl4i(%6hOmV$jiR8jkFJCUhWC@-55j#^_B~##;i)X3i{$G( z^M~$5xg1=g{P#D1t=qKwPUn^^o(>!VS;Gq8n;GeXVbj}JY(?z)Pr>$2PsFeVb;&&# z_Qg*0zX0fO%OATVEBuG(etG6)!!Cb_xhyjOA3wy@lo~nQLjUu%X^ccoF5SHXOWdRN zDHzT|mmR(uKii5PH`aWI9-B5KqQ~wX7#efN9U8zG_B<0v5L+I&Y@B7!fjIcThz`B< XTdaUPCHMEC^$7<(4wU?G=3oB