Skip to content

Commit 7ed150c

Browse files
docs(popover): major docs / functionality changes
1 parent ce61308 commit 7ed150c

File tree

8 files changed

+207
-25
lines changed

8 files changed

+207
-25
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ export default component$(() => {
1414
<PopoverTrigger
1515
ref={buttonRef}
1616
disableClickInitPopover
17-
onMouseEnter$={() => {
17+
onPointerEnter$={() => {
1818
initPopover$();
19-
myPopoverRef.value?.togglePopover();
19+
myPopoverRef.value?.showPopover();
2020
}}
21-
onMouseLeave$={() => {
22-
myPopoverRef.value?.togglePopover();
21+
onPointerLeave$={() => {
22+
myPopoverRef.value?.hidePopover();
2323
}}
2424
popoverTargetAction="show"
2525
popovertarget="anchor-ref-id"
@@ -36,7 +36,7 @@ export default component$(() => {
3636
placement="top"
3737
gutter={4}
3838
id="anchor-ref-id"
39-
class="listbox shadow-dark-low rounded-md border-2 border-slate-300 bg-slate-800 !p-4 text-white"
39+
class="my-transition listbox shadow-dark-low rounded-md border-2 border-slate-300 bg-slate-800 !p-4 text-white"
4040
>
4141
I am anchored to the trigger!
4242
</Popover>

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

Lines changed: 173 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Here are a couple of example components where the Popover API can be used. Inclu
7474
]}
7575
/>
7676

77-
## Styling
77+
## Caveats
7878

7979
<Note status="warning">
8080
While this component handles most of the complexity under the hood, there are some minor
@@ -107,14 +107,14 @@ A separate declaration is needed to select popovers in all browsers.
107107
108108
## Popovertarget
109109

110-
To add a popover trigger, it can be done similar to the native API, using the `popovertarget` attribute along with the corresponding popover id. This includes being used on **native button elements**, but it can be leveraged as a prop as well.
110+
To add a popover trigger, it can be done similar to the native API, using the `popovertarget` attribute along with the corresponding popover id.
111111

112112
```tsx
113113
// Opens when trigger is clicked, with a matching id to popovertarget
114114
<PopoverTrigger popovertarget="get-20-off">Get 20% off your next order!</PopoverTrigger>
115115

116-
// Native API: Adding popover here is the same as popover="auto"
117-
<button popovertarget="get-20-off" popover>Get 20% off your next order!</button>
116+
// Native API: popovertarget is an invoker
117+
<button popovertarget="get-20-off">Get 20% off your next order!</button>
118118
```
119119

120120
> This can be used to trigger popups anywhere in the component tree!
@@ -145,8 +145,6 @@ An auto popover will automatically hide when you click outside of it and typical
145145

146146
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.
147147

148-
<Showcase name="flip" />
149-
150148
### Popover Methods
151149

152150
There are two methods for showing and hiding popovers manually in JavaScript. The `showPopover` and `hidePopover` methods.
@@ -171,14 +169,182 @@ useTask$(function syncPopoverStateTask({ track }) {
171169

172170
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.
173171

172+
> Setting `floating={true}` will add 8-10 or so kb, we currently use javascript to float items, and have tried to make it as minimal and incremental as possible for the upcoming anchor API.
173+
174174
To float an element, it must have an anchored element to latch onto. This can be done with the `anchorRef` prop.
175175

176+
Below is a mini tooltip implementation enabled by anchor behavior. Keep in mind, this is not accessible, but an example of how this API can be used. We strongly suggest using the Qwik UI Tooltip component.
177+
176178
### AnchorRef Prop
177179

178180
<Showcase name="anchor-ref" />
179181

180182
### Floating Prop
181183

182-
This API is purposely opt-in / incremental. At some point JavaScript will not be needed here thanks to the CSS Anchor API that is currently experimental.
184+
This API is purposely opt-in / incremental. At some point JavaScript will not be needed here at all, and so we want to ensure a smooth migration path when that becomes widely supported.
183185

184186
We chose not to use an Anchor polyfill here due to the difference in bundle size compared to a JavaScript implementation.
187+
188+
## **Configuring the Listbox**
189+
190+
The `Popover` component is designed for positioning elements that float and facilitating interactions with them.
191+
192+
For the following examples, we'll be using the Combobox component. `ComboboxPopover` is merely a wrapper of the Popover component. Under the hood, it looks something like this:
193+
194+
```tsx
195+
<Popover
196+
{...props}
197+
manual
198+
id={popoverId}
199+
floating={true}
200+
// passing in a custom ref for programmatic behavior
201+
anchorRef={context.inputRef}
202+
popoverRef={context.popoverRef}
203+
class="listbox"
204+
>
205+
<Slot />
206+
</Popover>
207+
```
208+
209+
### Placement
210+
211+
To set the default position of the listbox, you can use the `placement` prop. In the example below, we've set placement to top. When the user opens the listbox, it will be above the input.
212+
213+
<Showcase name="placement" />
214+
215+
The example above sets the `placement` prop to `top`. The default placement is **bottom**.
216+
217+
### Flip
218+
219+
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.
220+
221+
<Showcase name="flip" />
222+
223+
### Gutter
224+
225+
In the previous docs examples, we use the gutter property on the listbox. Gutter is the space between the anchor element and the floating element.
226+
227+
<Showcase name="gutter" />
228+
229+
> In this case, that is the listbox or unordered list, and the input element. If Flip is enabled, it will provide a gutter space for both the top and bottom.
230+
231+
## Styling
232+
233+
Styles can be added normally like any other component in Qwik UI, such as adding a class. The Popover API however, exposes the `[popover]` and `:popover-open` attribute and pseudo-class selectors which can be used to style both the open and closed states.
234+
235+
From an earlier section, we learned that the `:popover-open` psuedo-class cannot be polyfilled, and so a class is added instead.
236+
237+
```tsx
238+
[popover] {
239+
/* Make the popover a grid */
240+
display: grid;
241+
}
242+
243+
[popover]:not(:popover-open) {
244+
/* Make sure to hide it unless open */
245+
display: none;
246+
}
247+
248+
/* Duplicate for polyfill browsers */
249+
[popover]:not(.\:popover-open) {
250+
display: none;
251+
}
252+
```
253+
254+
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. Below is an example of styling with `[popover]` as an arbitrary variant.
255+
256+
<Showcase name="styling" />
257+
258+
> 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.
259+
260+
## Animations
261+
262+
### The problem
263+
264+
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.
265+
266+
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.
267+
268+
> 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.
269+
270+
### Custom animation support
271+
272+
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**.
273+
274+
### Animation declarations
275+
276+
<Showcase name="listbox-animation" />
277+
278+
To use an animation, add the following CSS classes to the component.
279+
280+
- The `.popover-showing` class determines the animation that happens when it is first opened.
281+
282+
- The `.popover-closing` class determines what class is added when the listbox is **closed**.
283+
284+
Here's the CSS imported from the example:
285+
286+
```css
287+
@keyframes fadeIn {
288+
from {
289+
opacity: 0;
290+
}
291+
to {
292+
opacity: 1;
293+
}
294+
}
295+
296+
@keyframes fadeOut {
297+
from {
298+
opacity: 0;
299+
}
300+
to {
301+
opacity: 1;
302+
}
303+
}
304+
305+
.animate-in {
306+
animation: fadeIn both 500ms ease;
307+
}
308+
309+
.animate-out {
310+
animation: fadeOut both 500ms ease;
311+
}
312+
```
313+
314+
> These classes also apply to transition declarations as well. Below is an example of that use case.
315+
316+
### Transition declarations
317+
318+
<Showcase name="animation" />
319+
320+
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.
321+
322+
> 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.
323+
324+
## Additional References
325+
326+
Qwik UI aims to be in line with the standard whenever possible. Our goal is to give Qwik developers the proper tooling when it comes to creating accessible & complex web applications.
327+
328+
To read more about the popover API you can check it out on:
329+
330+
- [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
331+
- [Open UI Proposal](https://open-ui.org/components/popover.research.explainer/)
332+
- [What is the top layer?](https://developer.chrome.com/blog/what-is-the-top-layer/)
333+
334+
## Backdrops
335+
336+
<Showcase name="backdrop" />
337+
338+
## Additional Examples
339+
340+
### Auto Placement
341+
342+
Automatically places the listbox based on available space. **You must set flip to false before using it.** This comes in handy when you're unsure about the optimal placement for the floating element, or if you prefer not to set it manually.
343+
344+
<Showcase name="auto-placement" />
345+
346+
<Note status="caution">
347+
You cannot use **flip** and **autoPlacement** at the same time. They both manipulate the
348+
placement but with different strategies. Using both can result in a continuous reset
349+
loop as they try to override each other's work.
350+
</Note>

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
5050

5151
const beforeTeleportParentRef = useSignal<HTMLElement | undefined>(undefined);
5252
const popoverRef = useSignal<HTMLElement | undefined>(undefined);
53+
const isPolyfillSig = useSignal<boolean>(false);
5354

5455
/** have we rendered on the client yet? 0: no, 1: force, 2: yes */
5556
const hasRenderedOnClientSig = useSignal(isServer ? 0 : 2);
@@ -66,6 +67,8 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
6667

6768
if (isServer) return;
6869

70+
isPolyfillSig.value = true;
71+
6972
let polyfillContainer: HTMLDivElement | null = document.querySelector(
7073
'div[data-qwik-ui-popover-polyfill]',
7174
);
@@ -97,17 +100,19 @@ export const PopoverImpl = component$<PopoverImplProps>((props) => {
97100
onBeforeToggle$={(e: ToggleEvent) => {
98101
if (!popoverRef.value) return;
99102

103+
console.log(e.newState);
104+
100105
setTimeout(() => {
101106
if (e.newState === 'open' && popoverRef.value) {
102-
supportShowAnimation(popoverRef.value);
107+
supportShowAnimation(popoverRef.value, isPolyfillSig.value);
103108
}
104109
}, 5);
105110

106111
if (e.newState === 'closed') {
107112
supportClosingAnimation(popoverRef.value);
108113
}
109114
}}
110-
onToggle$={(e) => {
115+
onToggle$={(e: ToggleEvent) => {
111116
if (props.popoverRef) {
112117
props.popoverRef.value = popoverRef.value;
113118
}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export function usePopover(popovertarget: string) {
1414
const didInteractSig = useSignal<boolean>(false);
1515
const popoverSig = useSignal<HTMLElement | null>(null);
1616

17-
const hookExecutedSig = useSignal<boolean>(false);
18-
1917
const loadPolyfill$ = $(async () => {
2018
await import('@oddbird/popover-polyfill');
2119
document.dispatchEvent(new CustomEvent('poppolyload'));
@@ -54,12 +52,10 @@ export function usePopover(popovertarget: string) {
5452
if (!popover) return;
5553

5654
if (popover && popover.hasAttribute('popover')) {
55+
/* opens manual on any event */
5756
popover.showPopover();
5857
}
5958
}
60-
61-
console.log('HOOK EXECUTED');
62-
hookExecutedSig.value = true;
6359
});
6460

6561
// event is created after teleported properly

packages/kit-headless/src/components/popover/popover.css

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616

1717
/* Popover Presets: TODO figure out specificity to make this easier for user to override */
1818
.listbox {
19-
margin: unset;
20-
padding: unset;
21-
border: unset;
22-
overflow: unset;
23-
position: absolute;
19+
--listbox-margin: unset;
20+
--listbox-padding: unset;
21+
--listbox-border: unset;
22+
--listbox-overflow: unset;
23+
--listbox-position: absolute;
24+
25+
margin: var(--listbox-margin);
26+
padding: var(--listbox-padding);
27+
border: var(--listbox-border);
28+
overflow: var(--listbox-overflow);
29+
position: var(--listbox-position);
2430
}

packages/kit-headless/src/components/popover/utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
/**
22
* Adds CSS-Class to support popover-opening-animation
33
*/
4-
export function supportShowAnimation(popover: HTMLElement) {
4+
export function supportShowAnimation(popover: HTMLElement, isPolyfill: boolean) {
5+
popover.classList.remove('popover-closing');
56
popover.classList.add('popover-showing');
7+
8+
if (isPolyfill) {
9+
/* to support tooltips that enter quickly */
10+
popover.classList.add(':popover-open');
11+
}
612
}
713

814
/**
@@ -11,6 +17,7 @@ export function supportShowAnimation(popover: HTMLElement) {
1117
* export function supportClosingAnimation(popover: HTMLElement, afterAnimate: () => void) {
1218
*/
1319
export function supportClosingAnimation(popover: HTMLElement) {
20+
console.log('Closing animation:', popover.classList.contains('popover-showing'));
1421
popover.classList.remove('popover-showing');
1522
popover.classList.add('popover-closing');
1623

@@ -27,8 +34,10 @@ export function supportClosingAnimation(popover: HTMLElement) {
2734
if (animationDuration !== '0s') {
2835
popover.addEventListener('animationend', runAnimationEnd, { once: true });
2936
} else if (transitionDuration !== '0s') {
37+
console.log('inside transition');
3038
popover.addEventListener('transitionend', runTransitionEnd, { once: true });
3139
} else {
40+
console.log('I RAN!');
3241
popover.classList.remove('popover-closing');
3342
}
3443
}

0 commit comments

Comments
 (0)