Skip to content

Commit e3427db

Browse files
docs(popover): docs so far
1 parent c766358 commit e3427db

File tree

10 files changed

+230
-100
lines changed

10 files changed

+230
-100
lines changed

apps/website/src/routes/docs/headless/modal/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A window overlaid on either the primary window or another dialog window. Modal c
1616

1717
The modal makes use of the HTML `dialog` element, which is supported in [every major browser](https://caniuse.com/?search=dialog).
1818

19-
> For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/) _(in progress)_. In the near future, Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `<dialog>` element itself for non-modal dialogs.
19+
> For non-modal UI elements, it is preferred to use [Qwik UI's popover component](../../../docs/headless/popover/). Qwik's popover component can be applied to the most-semantically-relevant HTML element, including the `<dialog>` element itself for non-modal dialogs.
2020
2121
Modals are used when an important choice needs to be made in the application. The rest of the content isn't interactive until a certain action has been performed.
2222

apps/website/src/routes/docs/headless/popover/examples/anchor-ref.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@ import { Popover, PopoverTrigger } from '@qwik-ui/headless';
33
import { usePopover } from '@qwik-ui/headless';
44

55
export default component$(() => {
6-
const buttonRef = useSignal<HTMLButtonElement>();
76
const { showPopover, hidePopover } = usePopover(`anchor-ref-id`);
8-
const myPopoverRef = useSignal<HTMLElement>();
7+
const triggerRef = useSignal<HTMLButtonElement>();
8+
const popoverRef = useSignal<HTMLElement>();
99

1010
return (
1111
<>
12-
<div>
13-
We're using popover target action on the trigger.{' '}
12+
<div class="flex flex-col items-center justify-center gap-2">
13+
<p>I'm a mini tooltip!</p>
1414
<PopoverTrigger
15-
ref={buttonRef}
15+
ref={triggerRef}
1616
disableClickInitPopover
1717
onPointerEnter$={() => {
1818
showPopover();
19-
console.log('in pointer!');
2019
}}
2120
onPointerLeave$={() => {
2221
hidePopover();
@@ -25,13 +24,13 @@ export default component$(() => {
2524
popovertarget="anchor-ref-id"
2625
class="rounded-md border-2 border-slate-300 bg-slate-800 px-3 py-1 text-white"
2726
>
28-
Popover Trigger
27+
Hover over me
2928
</PopoverTrigger>
3029
</div>
3130

3231
<Popover
33-
anchorRef={buttonRef}
34-
popoverRef={myPopoverRef}
32+
ref={popoverRef}
33+
anchorRef={triggerRef}
3534
floating={true}
3635
placement="top"
3736
gutter={4}

apps/website/src/routes/docs/headless/popover/examples/animation.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { component$, useSignal } from '@builder.io/qwik';
2+
import {
3+
Combobox,
4+
ComboboxLabel,
5+
ComboboxControl,
6+
ComboboxInput,
7+
ComboboxTrigger,
8+
ComboboxPopover,
9+
ComboboxListbox,
10+
ComboboxOption,
11+
ResolvedOption,
12+
} from '@qwik-ui/headless';
13+
14+
import './animation.css';
15+
16+
export default component$(() => {
17+
const isListboxOpenSig = useSignal(false);
18+
19+
const animationExample = ['I', 'Float', 'Above', 'Other', 'Content!'];
20+
21+
return (
22+
<Combobox
23+
class="w-fit bg-transparent"
24+
options={animationExample}
25+
filter$={(value: string, options) =>
26+
options.filter(({ option }) => {
27+
return option.toLowerCase().startsWith(value.toLowerCase());
28+
})
29+
}
30+
bind:isListboxOpenSig={isListboxOpenSig}
31+
>
32+
<ComboboxLabel class=" mb-2 block font-semibold text-white">
33+
I open a floating element!
34+
</ComboboxLabel>
35+
<ComboboxControl class="relative flex items-center rounded-sm border-[1px] border-slate-400 bg-[#1f2532]">
36+
<ComboboxInput
37+
class="px-d2 w-fit bg-slate-900 px-2 pr-6 text-white placeholder:text-slate-500"
38+
placeholder="Wallaby Rd."
39+
/>
40+
<ComboboxTrigger class="group absolute right-0 h-6 w-6 bg-transparent">
41+
<svg
42+
xmlns="http://www.w3.org/2000/svg"
43+
viewBox="0 0 24 24"
44+
fill="none"
45+
stroke-width="2"
46+
class="stroke-white transition-transform duration-[450ms] group-aria-expanded:-rotate-180"
47+
stroke-linecap="round"
48+
stroke-linejoin="round"
49+
>
50+
<polyline points="6 9 12 15 18 9"></polyline>
51+
</svg>
52+
</ComboboxTrigger>
53+
</ComboboxControl>
54+
<ComboboxPopover gutter={8}>
55+
<ComboboxListbox
56+
class={`w-44 rounded-sm border-[1px] border-slate-400 bg-slate-900 px-4 py-2`}
57+
optionRenderer$={(option: ResolvedOption, index: number) => (
58+
<ComboboxOption
59+
key={option.key}
60+
class="group rounded-sm border-2 border-transparent px-2 text-white hover:bg-slate-500 aria-disabled:text-slate-600 aria-disabled:hover:bg-slate-700 aria-selected:border-slate-200 aria-selected:bg-slate-500"
61+
index={index}
62+
resolved={option}
63+
>
64+
{option.label}
65+
</ComboboxOption>
66+
)}
67+
/>
68+
</ComboboxPopover>
69+
</Combobox>
70+
);
71+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Popover, usePopover } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
const { togglePopover } = usePopover(`programmatic-id`);
6+
return (
7+
<>
8+
<button
9+
preventdefault:click
10+
class="rounded-md border-2 border-slate-400 bg-slate-800 px-3 py-1 text-white"
11+
onKeyDown$={(e) => {
12+
if (e.key === 'o') {
13+
togglePopover();
14+
}
15+
}}
16+
>
17+
Focus me and press the 'o' key!
18+
</button>
19+
<Popover
20+
onBeforeToggle$={() => {
21+
console.log('I before toggle!');
22+
}}
23+
id="programmatic-id"
24+
class="shadow-dark-medium rounded-md border-2 border-slate-300 bg-slate-800 px-3 py-1 opacity-0"
25+
>
26+
I was programmatically opened!
27+
</Popover>
28+
</>
29+
);
30+
});

apps/website/src/routes/docs/headless/popover/index.mdx

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ A non-modal primitive with overlays that open and close around a DOM element.
1919
`"Early" access to MDN's native popover API`,
2020
'Support across all browsers',
2121
'Resumable / Lazily executes code on interaction',
22-
'Top Layer behavior',
23-
'Opt-in floating behavior',
24-
'Portal polyfill works across meta-frameworks',
22+
'UI is placed above everything else.',
23+
'stick or float elements to other elements, similar to the upcoming Anchor API.',
2524
]}
2625
/>
2726

@@ -39,6 +38,8 @@ This is particularly useful for displaying additional information or options wit
3938
4039
Most importantly, this API is a foundation for other headless components. Qwik UI is one of the first _(if not the first)_ headless libraries to align with the specification. The content inside of Qwik UI popovers are also natively resumable.
4140

41+
Even if a popover is in the HTML Tree, its children will not execute unless resumed.
42+
4243
## Versatile use cases <span class="ml-2 text-red-400 dark:text-red-400 text-base px-2 bg-red-100 rounded-md shadow-light-low dark:shadow-dark-medium border-b-2 border-[1px] border-red-200 dark:border-red-300">WIP</span>
4344

4445
Here are a couple of example components where the Popover API can be used. Including other parts of Qwik UI headless. Throughout the documentation, we will show the Combobox being used as an example.
@@ -119,7 +120,7 @@ A separate declaration is needed to select popovers in all browsers.
119120
120121
### Cross-browser Animations
121122

122-
Entry and exit animations have often been a frustrating experience. Especially trying to animate between `display: none`, a discrete property.
123+
Entry and exit animations have often been a frustrating experience in web development. Especially trying to animate between `display: none`, a discrete property.
123124

124125
Until recently, discrete properties were not animatable. Luckily, there's [new properties to CSS](https://developer.chrome.com/blog/entry-exit-animations) that solve this problem.
125126

@@ -189,28 +190,60 @@ An auto popover will automatically hide when you click outside of it and typical
189190

190191
On the other hand, a `manual` popover needs to be manually hidden, such as toggling the button or programmatically, and allows for scenarios like nested popovers in menus.
191192

192-
### Popover Methods
193+
We can add a manual popover by adding the `popover="manual"` prop, or `manual` shorthand.
194+
195+
> Notice that the last opened popover is always above the other!
196+
197+
## Programmatic Behavior
193198

194-
There are two methods for showing and hiding popovers manually in JavaScript. The `showPopover` and `hidePopover` methods.
199+
<Showcase name="programmatic" />
195200

196-
For example, here is programmatic behavior syncing the popover state with the listbox in the `Combobox` component.
201+
We can also enable programmatic behavior with popovers. Qwik UI provides several functions you can use to control this behavior.
202+
203+
<AnatomyTable
204+
propDescriptors={[
205+
{
206+
name: 'showPopover()',
207+
description: 'Opens the popover.',
208+
},
209+
{
210+
name: 'togglePopover()',
211+
description: 'Toggles the popover between the open and closed state.',
212+
},
213+
{
214+
name: 'hidePopover()',
215+
description: 'Closes the popover.',
216+
},
217+
]}
218+
/>
219+
220+
We can access them anywhere by importing the `usePopover` hook. We also need to pass in the **popover id or popovertarget string** to usePopover, which will find the popover we intend to perform an action on.
221+
222+
For example, here is programmatic behavior syncing the popover state with the listbox in Qwik UI's `Combobox` component.
197223

198224
```tsx
199-
useTask$(function syncPopoverStateTask({ track }) {
225+
// near the top of the component
226+
const { showPopover, hidePopover } = usePopover(popoverId);
227+
228+
useTask$(async ({ track }) => {
200229
track(() => context.isListboxOpenSig.value);
201230

202-
if (!context.popoverRef.value) {
203-
return;
204-
}
231+
if (isServer) return;
205232

206-
context.isListboxOpenSig.value
207-
? context.popoverRef.value.showPopover()
208-
: context.popoverRef.value.hidePopover();
233+
if (context.isListboxOpenSig.value) {
234+
showPopover();
235+
} else {
236+
hidePopover();
237+
}
209238
});
210239
```
211240

241+
> In the native spec, these are methods, although we want to ensure the polyfill loads on interaction, rather than page load with a proxy.
242+
212243
## Floating Behavior
213244

245+
<Showcase name="floating-intro" />
246+
214247
To use the popover API with floating elements, you can add the `floating={true}` prop to the Popover component. This API enables the use of JavaScript to dynamically position the listbox using the `top` & `left` absolute properties.
215248

216249
> Enabling the `floating={true}` property will introduce a slight increase in code size, as we currently utilize JavaScript to implement floating items. We've strived to keep it as minimal as possible, but powerful in building complex components in anticipation of the forthcoming anchor API.
@@ -238,12 +271,12 @@ For the following examples, we'll be using the Combobox component. `ComboboxPopo
238271
```tsx
239272
<Popover
240273
{...props}
241-
manual
242274
id={popoverId}
275+
ref={context.popoverRef}
276+
manual
243277
floating={true}
244-
// passing in a custom ref for programmatic behavior
278+
// "stick" to the input
245279
anchorRef={context.inputRef}
246-
popoverRef={context.popoverRef}
247280
class="listbox"
248281
>
249282
<Slot />
@@ -256,8 +289,6 @@ To set the default position of the listbox, you can use the `placement` prop. In
256289

257290
<Showcase name="placement" />
258291

259-
The example above sets the `placement` prop to `top`. The default placement is **bottom**.
260-
261292
### Flip
262293

263294
Allows the listbox to flip its position based on available space. It's enabled by default, but can be disabled by adding `flip={false}` on the listbox.
@@ -301,21 +332,44 @@ If Tailwind is the framework of choice, then styles can be added using the [arbi
301332

302333
> 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.
303334
304-
## Animations
335+
### Listbox preset
305336

306-
### The problem
337+
By default, the popover API comes with built-in styles, including fixed behavior, margin, the list goes on.
307338

308-
Currently, native animation support for the popover API is [not quite there yet](https://open-ui.org/components/popover.research.explainer/#animation-of-popovers). Part of this has to do with the native API managing the popover state and adding the `display: none` declaration.
339+
There are times when we want to override this behavior. An example being when we want an absolutely positioned listbox.
309340

310-
This declaration is `unanimatable`, meaning you cannot animate to or from display none at this time. With that said, we currently have a way to animate popovers using a clever trick under the hood.
341+
To do that, we can add the `listbox` class to the popover component.
311342

312-
> There are [new properties to CSS](https://developer.chrome.com/blog/entry-exit-animations) that aim to fix this problem, but regardless we need to be able to support all browsers.
343+
If you need to override any of the listbox properties, use the following CSS variables:
313344

314-
### Custom animation support
345+
<AnatomyTable
346+
propDescriptors={[
347+
{
348+
name: 'margin',
349+
description: '--listbox-margin',
350+
},
351+
{
352+
name: 'padding',
353+
description: '--listbox-padding',
354+
},
355+
{
356+
name: 'border',
357+
description: '--listbox-border',
358+
},
359+
{
360+
name: 'overflow',
361+
description: '--listbox-overflow',
362+
},
363+
{
364+
name: 'position',
365+
description: '--listbox-position',
366+
},
367+
]}
368+
/>
315369

316-
Regardless of native support, animations need support in polyfill browsers too, and so we've provided a way for you to use both `animation` and `transition` declarations in your `Popover` component for **all browsers**.
370+
> Another option is to use the `!important` syntax to override any of the CSS variable values.
317371
318-
### Animation declarations
372+
## Animations
319373

320374
<Showcase name="listbox-animation" />
321375

@@ -359,8 +413,6 @@ Here's the CSS imported from the example:
359413
360414
### Transition declarations
361415

362-
<Showcase name="animation" />
363-
364416
Transitions use the same classes for entry and exit animations. Those being `.popover-showing` and `.popover-closing`. They are explained move in the section above.
365417

366418
> 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 **~70%** of browsers.

packages/kit-headless/src/components/combobox/combobox-popover.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const ComboboxPopover = component$(
3232
id={customPopoverId}
3333
floating={true}
3434
anchorRef={context.inputRef}
35-
popoverRef={context.popoverRef}
35+
ref={context.popoverRef}
3636
class={['listbox', props.class]}
3737
manual
3838
>

packages/kit-headless/src/components/popover/floating.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const FloatingPopover = component$(
133133
});
134134

135135
return (
136-
<PopoverImpl {...props} popoverRef={popoverRef}>
136+
<PopoverImpl {...props} ref={popoverRef}>
137137
<Slot />
138138
</PopoverImpl>
139139
);

0 commit comments

Comments
 (0)