Skip to content

Commit 7e87d33

Browse files
cwoolummaiieul
andauthored
Update animations functionality to only support the native popover approach (#970)
* Change how popover animations work and update docs * fix: modal route not working + naming * fix: modal route not working + naming #2 * fix: p code style * chore: remove unnecessary import * refactor: make compatibility note easier to maintain --------- Co-authored-by: maiieul <[email protected]>
1 parent 9cb3c96 commit 7e87d33

File tree

25 files changed

+384
-305
lines changed

25 files changed

+384
-305
lines changed

.changeset/two-jeans-share.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik-ui/headless': minor
3+
---
4+
5+
We are removing the existing popover animations shimming and instead wil now only support native popover animations. This is considered a breaking change but will be more reliable overall.

.eslintignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ node_modules
22
dist
33
coverage
44
.eslintrc.*
5-
vite.config.ts
5+
vite.config.ts
6+
packages/kit-headless/browsers/**

.github/workflows/test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
jobs:
88
test:
99
runs-on: ubuntu-latest
10+
name: Test NodeJS ${{ matrix.node_version }}
1011

1112
strategy:
1213
matrix:

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
],
77
"editor.codeActionsOnSave": {
88
"source.removeUnusedImports": "explicit"
9-
}
9+
},
10+
"vitest.disableWorkspaceWarning": true
1011
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Note, NoteStatus } from '../note/note'; // Adjust the import path based on your structure
3+
4+
export const TopLayerAnimationsCaveats = component$(() => {
5+
return (
6+
<Note status={NoteStatus.Warning}>
7+
<strong>Important Caveats for Animating Discrete Properties</strong>
8+
9+
<ul class="mt-4 list-disc bg-gradient-to-b pl-4">
10+
<li>
11+
<strong>
12+
Animating <code>display</code> and <code>overlay</code>:
13+
</strong>
14+
<p>
15+
The <code>display</code> property must be included in the transitions list to
16+
ensure the element remains visible throughout the animation. The value flips
17+
from <code>none</code> to <code>block</code> at 0% of the animation, ensuring
18+
visibility for the entire duration. The&nbsp;
19+
<code>overlay</code> ensures the element stays in the top layer until the
20+
animation completes.
21+
</p>
22+
</li>
23+
<li>
24+
<strong>
25+
Using <code>transition-behavior: allow-discrete</code>:
26+
</strong>
27+
<p>
28+
This property is essential when animating discrete properties like{' '}
29+
<code>display</code> and <code>overlay</code>, which are not typically
30+
animatable. It ensures smooth transitions for these discrete properties.
31+
</p>
32+
</li>
33+
<li>
34+
<strong>
35+
Setting Starting Styles with <code>@starting-style</code>:
36+
</strong>
37+
<p>
38+
CSS transitions are only triggered when a property changes on a visible
39+
element. The&nbsp;
40+
<code>@starting-style</code> at-rule allows you to set initial styles (e.g.,{' '}
41+
<code>opacity</code> and
42+
<code>transform</code>) when the element first appears, ensuring that the
43+
animation behaves predictably.
44+
</p>
45+
</li>
46+
</ul>
47+
</Note>
48+
);
49+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { component$ } from '@builder.io/qwik';
2+
import { Note, NoteStatus } from '../note/note'; // Adjust the import path based on your structure
3+
4+
export const BrowserAnimationsCompatability = component$(() => {
5+
return (
6+
<Note status={NoteStatus.Info}>
7+
<div class="flex flex-col gap-2">
8+
<h4>
9+
<strong>Browser Compatability</strong>
10+
</h4>
11+
<p>
12+
<a href="https://caniuse.com/?search=popover%20API">
13+
Browser versions that do not support the popover API natively
14+
</a>{' '}
15+
have known issues when trying to use animations or transitions. If you need to
16+
support legacy versions of browsers, please be sure to test this functionality
17+
independently.
18+
</p>
19+
</div>
20+
</Note>
21+
);
22+
});

apps/website/src/components/mdx-components/index.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { KeyboardInteractionTable } from '../keyboard-interaction-table/keyboard
1111
import { Note } from '../note/note';
1212
import { Showcase } from '../showcase/showcase';
1313
import { StatusBanner } from '../status-banner/status-banner';
14+
import { TopLayerAnimationsCaveats } from '../animations/caveats';
15+
import { BrowserAnimationsCompatability } from '../animations/compatability';
1416

1517
export const components: Record<string, Component> = {
1618
p: component$<PropsOf<'p'>>(({ ...props }) => {
@@ -134,4 +136,6 @@ export const components: Record<string, Component> = {
134136
StatusBanner,
135137
Showcase,
136138
AutoAPI,
139+
TopLayerAnimationsCaveats,
140+
BrowserAnimationsCompatability,
137141
};

apps/website/src/routes/docs/headless/modal/examples/animatable.tsx

+30-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,37 @@
11
import { component$, useStyles$ } from '@builder.io/qwik';
22
import { Modal, Label } from '@qwik-ui/headless';
3-
import styles from '../snippets/animation.css?inline';
43

54
export default component$(() => {
6-
useStyles$(styles);
5+
useStyles$(`
6+
.modal-animation {
7+
animation: modalClose 0.35s ease-in-out forwards;
8+
}
9+
10+
.modal-animation:popover-open {
11+
animation: modalOpen 0.75s ease-in-out forwards;
12+
}
13+
14+
@keyframes modalOpen {
15+
from {
16+
opacity: 0;
17+
transform: scale(0.9);
18+
}
19+
to {
20+
opacity: 1;
21+
transform: scale(1);
22+
}
23+
}
24+
25+
@keyframes modalClose {
26+
from {
27+
opacity: 1;
28+
transform: scale(1);
29+
}
30+
to {
31+
opacity: 0;
32+
transform: scale(0.9);
33+
}
34+
}`);
735

836
return (
937
<Modal.Root>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { component$, useStyles$ } from '@builder.io/qwik';
2+
import { Modal, Label } from '@qwik-ui/headless';
3+
4+
export default component$(() => {
5+
useStyles$(`
6+
.modal-animation[open]::backdrop {
7+
animation: backdropFadeIn 0.75s ease-in-out forwards;
8+
}
9+
10+
@keyframes backdropFadeIn {
11+
from {
12+
background-color: rgba(0, 0, 0, 0);
13+
}
14+
to {
15+
background-color: rgba(0, 0, 0, 0.65);
16+
}
17+
}`);
18+
19+
return (
20+
<Modal.Root>
21+
<Modal.Trigger class="modal-trigger">Open Modal</Modal.Trigger>
22+
<Modal.Panel class="modal-panel modal-animation">
23+
<Modal.Title>Edit Profile</Modal.Title>
24+
<Modal.Description>
25+
You can update your profile here. Hit the save button when finished.
26+
</Modal.Description>
27+
<Label>
28+
Name
29+
<input type="text" placeholder="John Doe" />
30+
</Label>
31+
<Label>
32+
Email
33+
<input type="text" placeholder="[email protected]" />
34+
</Label>
35+
<footer>
36+
<Modal.Close class="modal-close">Cancel</Modal.Close>
37+
<Modal.Close class="modal-close">Save Changes</Modal.Close>
38+
</footer>
39+
</Modal.Panel>
40+
</Modal.Root>
41+
);
42+
});

apps/website/src/routes/docs/headless/modal/examples/transition.tsx

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
import { component$, useStyles$ } from '@builder.io/qwik';
22
import { Modal, Label } from '@qwik-ui/headless';
3-
import styles from '../snippets/animation.css?inline';
43

54
export default component$(() => {
6-
useStyles$(styles);
5+
useStyles$(`
6+
.modal-transition {
7+
opacity: 0;
8+
transform: scale(0.9);
9+
transition:
10+
opacity 0.35s ease-in-out,
11+
transform 0.35s ease-in-out,
12+
display 0.35s,
13+
overlay 0.35s;
14+
transition-behavior: allow-discrete;
15+
}
16+
17+
.modal-transition:popover-open {
18+
opacity: 1;
19+
transform: scale(1);
20+
}
21+
22+
@starting-style {
23+
.modal-transition:popover-open {
24+
opacity: 0;
25+
transform: scale(0.9);
26+
}
27+
}`);
728

829
return (
930
<Modal.Root>

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

+11-17
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ title: Qwik UI | Modal
33
---
44

55
import topLayer from '../../../../../public/images/top-layer.webp';
6-
76
import { statusByComponent } from '~/_state/component-statuses';
87

98
<StatusBanner status={statusByComponent.headless.Modal} />
10-
import {FeatureList} from '~/components/feature-list/feature-list';
119

1210
# Modal
1311

@@ -170,33 +168,29 @@ This is done in a separate layer so that styles are easily overridable in consum
170168

171169
## Animations
172170

173-
Animating things to display none has historically been a significant challenge on the web. This is because display none is a `discrete` property, and is **unanimatable**.
174-
175-
> There is currently efforts to solve this problem. [New CSS properties](https://developer.chrome.com/blog/entry-exit-animations/) have been introduced, but currently do not provide good enough browser support.
171+
Modals require smooth entry and exit animations to enhance user experience. However, animating properties like display and overlay can be challenging because they are discrete properties and not traditionally animatable.
176172

177-
### Our current approach
173+
Modern browsers have introduced discrete animation capabilities, allowing us to animate these properties effectively. Below, we'll explore how to implement animations and transitions for modals using keyframe animations and CSS transitions.
178174

179-
Qwik UI automatically detects any `animation` or `transition` declarations under the hood and waits for them to finish before closing the modal. If there is no animation, then it will close normally.
175+
### Keyframe Animation Example
180176

181-
### Adding a transition
177+
Keyframes are ideal for handling the entry and exit of the modal. Here's an example using modalOpen for opening and modalClose for closing the modal:
182178

183-
<Showcase name="transition" />
179+
<Showcase name="animatable" />
184180

185-
To add an transition, use the `data-open`, `data-closing` and `data-closed` data attributes. Above is a snippet where we transition both the modal and backdrop's opacity.
181+
### Transition Declarations
186182

187-
### Adding an animation
183+
Transitions are useful for animating properties like opacity and transform. Here's how to implement transitions for the modal:
188184

189-
<Showcase name="animatable" />
185+
<Showcase name="transition" />
190186

191-
To add an animation, it's the same as with transitions, using the `data-open` and `data-closing` data attributes. Below is a snippet of the animation example above.
187+
<TopLayerAnimationsCaveats />
192188

193189
### Backdrop animations
194190

195-
Backdrop animations have also made significant progress in the last year, with support provided in over half of the major browsers, and close to 70% of users.
196-
197-
To add a backdrop animation, make sure to use the `::backdrop` pseudo selector to the end of the `data-closing` or `data-open` classes.
191+
To animate the modal's backdrop, use the `::backdrop` pseudo-element and include it in your keyframes or transitions:
198192

199-
> Firefox currently does not support backdrop animations. The fallback for browsers that do not support animated backdrops is the same as a non-animated backdrop.
193+
<Showcase name="backdrop-animatable" />
200194

201195
## Sheets
202196

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const api = {
2+
popover: [
3+
{
4+
floating: [],
5+
},
6+
{
7+
'popover-panel-arrow': [],
8+
},
9+
{
10+
'popover-panel-impl': [],
11+
},
12+
{
13+
'popover-panel': [],
14+
},
15+
{
16+
'popover-root': [],
17+
},
18+
{
19+
'popover-trigger': [],
20+
},
21+
{
22+
'use-popover': [],
23+
},
24+
],
25+
};

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

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
1-
import { component$ } from '@builder.io/qwik';
1+
import { component$, useStyles$ } from '@builder.io/qwik';
22
import { Popover } from '@qwik-ui/headless';
3+
34
export default component$(() => {
5+
useStyles$(`
6+
.popover-animation {
7+
animation: popover-shrink 0.4s ease-in-out forwards;
8+
}
9+
10+
/* For exit animation */
11+
.popover-animation:popover-open {
12+
animation: popover-grow 0.5s ease-in-out forwards;
13+
}
14+
15+
@keyframes popover-shrink {
16+
from {
17+
transform: scale(1);
18+
display: block;
19+
}
20+
21+
to {
22+
transform: scale(0);
23+
display: none;
24+
}
25+
}
26+
27+
@keyframes popover-grow {
28+
from {
29+
transform: scale(0);
30+
}
31+
32+
to {
33+
transform: scale(1);
34+
}
35+
}
36+
`);
37+
438
return (
539
<Popover.Root>
640
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>

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

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
1-
import { component$ } from '@builder.io/qwik';
1+
import { component$, useStyles$ } from '@builder.io/qwik';
22
import { Popover } from '@qwik-ui/headless';
3+
34
export default component$(() => {
5+
useStyles$(`
6+
.popover-transition {
7+
opacity: 0;
8+
transform: scale(0.5);
9+
transition:
10+
opacity 0.3s ease-out,
11+
transform 0.3s ease-out,
12+
display 0.3s,
13+
overlay 0.3s;
14+
transition-behavior: allow-discrete;
15+
}
16+
17+
.popover-transition:popover-open {
18+
opacity: 1;
19+
transform: scale(1);
20+
transition:
21+
opacity 0.3s ease-out,
22+
transform 0.3s ease-out,
23+
display 0.3s,
24+
overlay 0.3s;
25+
}
26+
27+
@starting-style {
28+
.popover-transition:popover-open {
29+
opacity: 0;
30+
transform: scale(0.5);
31+
}
32+
}`);
33+
434
return (
535
<Popover.Root>
636
<Popover.Trigger class="popover-trigger">Popover Trigger</Popover.Trigger>

0 commit comments

Comments
 (0)