mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Fix glitchy animation of floating elements (#2224)
* Set max-height of 400px to scrollable Popover * Pass computed placement to user components to set different animation directions when the placement changes. * Move elevation effect from WithFloating/WithOverlay to Popover * Apply same changes as in WithFloating to WithOverlay * Adjust FloatingArrow CSS to Popover * Run eslint and formatter
This commit is contained in:
parent
1ab4ee38c6
commit
a48c96559d
8 changed files with 131 additions and 69 deletions
|
@ -18,19 +18,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
z-index: 60;
|
||||
|
||||
/* outer border */
|
||||
border: 1px solid #b6b6b6;
|
||||
|
||||
&.dark {
|
||||
border-color: #060606;
|
||||
}
|
||||
border: 1px solid var(--border-subtle);
|
||||
|
||||
/* Rotate the box to indicate the different directions */
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 1px 1px 0 0 #565656;
|
||||
}
|
||||
|
|
|
@ -3,52 +3,101 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { create_in_transition } from "svelte/internal";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
import { floatingKey } from "./context-keys";
|
||||
|
||||
export let scrollable = false;
|
||||
let element: HTMLDivElement;
|
||||
let wrapper: HTMLDivElement;
|
||||
let hidden = true;
|
||||
let minHeight = 0;
|
||||
|
||||
let placement: Placement;
|
||||
|
||||
const placementStore = getContext<Writable<Promise<Placement>>>(floatingKey);
|
||||
|
||||
/* await computed placement of floating element to determine animation direction */
|
||||
$: if ($placementStore !== undefined) {
|
||||
$placementStore.then((computedPlacement) => {
|
||||
if (placement != computedPlacement) {
|
||||
placement = computedPlacement;
|
||||
/* use internal function to animate popover */
|
||||
create_in_transition(element, slide, { duration: 200 }).start();
|
||||
hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
/* set min-height on wrapper to ensure correct
|
||||
popover placement at animation start */
|
||||
minHeight = wrapper.offsetHeight;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="popover-wrapper d-flex"
|
||||
style:--min-height="{minHeight}px"
|
||||
bind:this={wrapper}
|
||||
>
|
||||
<div
|
||||
class="popover"
|
||||
class:scrollable
|
||||
class:dark={$pageTheme.isDark}
|
||||
transition:slide={{ duration: 200 }}
|
||||
>
|
||||
class:hidden
|
||||
class:top={placement === "top"}
|
||||
class:right={placement === "right"}
|
||||
class:bottom={placement === "bottom"}
|
||||
class:left={placement === "left"}
|
||||
bind:this={element}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/vars" as *;
|
||||
@use "sass/elevation" as elevation;
|
||||
|
||||
.popover-wrapper {
|
||||
min-height: var(--min-height, 0);
|
||||
}
|
||||
|
||||
.popover {
|
||||
border-radius: 5px;
|
||||
background-color: color(canvas-elevated);
|
||||
@include elevation.elevation(8);
|
||||
|
||||
align-self: flex-start;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--canvas-elevated);
|
||||
border: 1px solid var(--border-subtle);
|
||||
|
||||
min-width: var(--popover-width, 1rem);
|
||||
max-width: 95vw;
|
||||
|
||||
/* Needs this much space for FloatingArrow to be positioned */
|
||||
padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px);
|
||||
|
||||
font-size: 1rem;
|
||||
color: color(fg);
|
||||
|
||||
/* outer border */
|
||||
border: 1px solid palette(lightgray, 6);
|
||||
|
||||
&.dark {
|
||||
border-color: palette(darkgray, 9);
|
||||
}
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 0 0 0 1px palette(lightgray, 3);
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 0 0 0 1px palette(darkgray, 2);
|
||||
}
|
||||
&.scrollable {
|
||||
max-height: 80vh;
|
||||
max-height: 400px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
width: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* alignment determines slide animation direction */
|
||||
&.top,
|
||||
&.left {
|
||||
align-self: flex-end;
|
||||
}
|
||||
&.bottom,
|
||||
&.right {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,8 +10,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
} from "@floating-ui/dom";
|
||||
import type { Callback } from "@tslib/typing";
|
||||
import { singleCallback } from "@tslib/typing";
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import { createEventDispatcher, onDestroy, setContext } from "svelte";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import isClosingClick from "../sveltelib/closing-click";
|
||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||
|
@ -23,12 +24,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
||||
import positionFloating from "../sveltelib/position/position-floating";
|
||||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||
import { floatingKey } from "./context-keys";
|
||||
import FloatingArrow from "./FloatingArrow.svelte";
|
||||
|
||||
export let portalTarget: HTMLElement | null = null;
|
||||
|
||||
let placement: Placement = "bottom";
|
||||
export { placement as preferredPlacement };
|
||||
|
||||
/* Used by Popover to set animation direction depending on placement */
|
||||
const placementPromise = writable(undefined as Promise<Placement> | undefined);
|
||||
setContext(floatingKey, placementPromise);
|
||||
|
||||
export let offset = 5;
|
||||
/* 30px box shadow from elevation(8) */
|
||||
export let shift = 30;
|
||||
|
@ -72,8 +79,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
): Promise<void> {
|
||||
return position(reference, floating);
|
||||
): Promise<Placement> {
|
||||
const promise = position(reference, floating);
|
||||
$placementPromise = promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function position(
|
||||
|
@ -81,8 +90,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
) => Promise<void> = applyPosition,
|
||||
): Promise<void> {
|
||||
) => Promise<Placement> = applyPosition,
|
||||
): Promise<Placement | void> {
|
||||
if (reference && floating) {
|
||||
return callback(reference, floating, positionCurried);
|
||||
}
|
||||
|
@ -97,9 +106,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
callback: PositioningCallback,
|
||||
): Callback {
|
||||
const innerFloating = floating;
|
||||
return callback(reference, innerFloating, () =>
|
||||
positionCurried(reference, innerFloating),
|
||||
);
|
||||
return callback(reference, innerFloating, () => {
|
||||
$placementPromise = positionCurried(reference, innerFloating);
|
||||
});
|
||||
}
|
||||
|
||||
let cleanup: Callback | null = null;
|
||||
|
@ -186,7 +195,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as elevation;
|
||||
span.floating-reference {
|
||||
line-height: 1;
|
||||
}
|
||||
|
@ -195,9 +203,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
border-radius: 5px;
|
||||
|
||||
z-index: 890;
|
||||
&.show {
|
||||
@include elevation.elevation(8);
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
|
|
|
@ -3,11 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
||||
import type {
|
||||
FloatingElement,
|
||||
Placement,
|
||||
ReferenceElement,
|
||||
} from "@floating-ui/dom";
|
||||
import type { Callback } from "@tslib/typing";
|
||||
import { singleCallback } from "@tslib/typing";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher, setContext } from "svelte";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import isClosingClick from "../sveltelib/closing-click";
|
||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||
|
@ -18,6 +23,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
||||
import positionOverlay from "../sveltelib/position/position-overlay";
|
||||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||
import { overlayKey } from "./context-keys";
|
||||
|
||||
/* Used by Popover to set animation direction depending on placement */
|
||||
const placementPromise = writable(undefined as Promise<Placement> | undefined);
|
||||
setContext(overlayKey, placementPromise);
|
||||
|
||||
export let padding = 0;
|
||||
export let inline = false;
|
||||
|
@ -50,8 +60,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
): Promise<void> {
|
||||
return position(reference, floating);
|
||||
): Promise<Placement> {
|
||||
const promise = position(reference, floating);
|
||||
$placementPromise = promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function position(
|
||||
|
@ -59,8 +71,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
) => Promise<void> = applyPosition,
|
||||
): Promise<void> {
|
||||
) => Promise<Placement> = applyPosition,
|
||||
): Promise<Placement | void> {
|
||||
if (reference && floating) {
|
||||
return callback(reference, floating, positionCurried);
|
||||
}
|
||||
|
@ -75,9 +87,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
callback: PositioningCallback,
|
||||
): Callback {
|
||||
const innerFloating = floating;
|
||||
return callback(reference, innerFloating, () =>
|
||||
positionCurried(reference, innerFloating),
|
||||
);
|
||||
return callback(reference, innerFloating, () => {
|
||||
$placementPromise = positionCurried(reference, innerFloating);
|
||||
});
|
||||
}
|
||||
|
||||
let cleanup: Callback;
|
||||
|
@ -147,15 +159,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as elevation;
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
z-index: 40;
|
||||
&.show {
|
||||
@include elevation.elevation(5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,3 +6,5 @@ export const sectionKey = Symbol("section");
|
|||
export const buttonGroupKey = Symbol("buttonGroup");
|
||||
export const dropdownKey = Symbol("dropdown");
|
||||
export const modalsKey = Symbol("modals");
|
||||
export const floatingKey = Symbol("floating");
|
||||
export const overlayKey = Symbol("overlay");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
||||
import type { FloatingElement, Placement, ReferenceElement } from "@floating-ui/dom";
|
||||
|
||||
/**
|
||||
* The interface of a function that calls `computePosition` of floating-ui.
|
||||
|
@ -9,4 +9,4 @@ import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
|||
export type PositionAlgorithm = (
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
) => Promise<void>;
|
||||
) => Promise<Placement>;
|
||||
|
|
|
@ -30,7 +30,7 @@ function positionFloating({
|
|||
return async function(
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
): Promise<void> {
|
||||
): Promise<Placement> {
|
||||
const middleware: Middleware[] = [
|
||||
flip(),
|
||||
offset(offsetArg),
|
||||
|
@ -63,11 +63,13 @@ function positionFloating({
|
|||
} = await computePosition(reference, floating, computeArgs);
|
||||
|
||||
if (middlewareData.hide?.escaped) {
|
||||
return hideCallback("escaped");
|
||||
hideCallback("escaped");
|
||||
return computedPlacement;
|
||||
}
|
||||
|
||||
if (middlewareData.hide?.referenceHidden) {
|
||||
return hideCallback("referenceHidden");
|
||||
hideCallback("referenceHidden");
|
||||
return computedPlacement;
|
||||
}
|
||||
|
||||
Object.assign(floating.style, {
|
||||
|
@ -102,6 +104,8 @@ function positionFloating({
|
|||
top: arrowY ? `${arrowY}px` : "",
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
});
|
||||
|
||||
return computedPlacement;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { ComputePositionConfig, FloatingElement, Middleware, ReferenceElement } from "@floating-ui/dom";
|
||||
import type { ComputePositionConfig, FloatingElement, Middleware, Placement, ReferenceElement } from "@floating-ui/dom";
|
||||
import { computePosition, inline, offset } from "@floating-ui/dom";
|
||||
|
||||
import type { PositionAlgorithm } from "./position-algorithm";
|
||||
|
@ -20,7 +20,7 @@ function positionOverlay({
|
|||
return async function(
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
): Promise<void> {
|
||||
): Promise<Placement> {
|
||||
const middleware: Middleware[] = inlineArg ? [inline()] : [];
|
||||
|
||||
const { width, height } = reference.getBoundingClientRect();
|
||||
|
@ -35,7 +35,7 @@ function positionOverlay({
|
|||
middleware,
|
||||
};
|
||||
|
||||
const { x, y, middlewareData } = await computePosition(
|
||||
const { x, y, middlewareData, placement } = await computePosition(
|
||||
reference,
|
||||
floating,
|
||||
computeArgs,
|
||||
|
@ -57,6 +57,8 @@ function positionOverlay({
|
|||
width: `${width + 2 * padding}px`,
|
||||
height: `${height + 2 * padding}px`,
|
||||
});
|
||||
|
||||
return placement;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue