Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

next: remove dependency on bind:this for Presence #467

Merged
merged 2 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 40 additions & 29 deletions packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getDataOpenClosed,
kbd,
readonlyBox,
styleToString,
verifyContextDeps,
} from "$lib/internal/index.js";
import type { StyleProperties } from "$lib/shared/index.js";
Expand Down Expand Up @@ -244,43 +245,52 @@ class AccordionTriggerState {
* CONTENT
*/

type AccordionContentStateProps = BoxedValues<{
presentEl: HTMLElement | undefined;
}> &
ReadonlyBoxedValues<{
forceMount: boolean;
}>;
type AccordionContentStateProps = ReadonlyBoxedValues<{
forceMount: boolean;
id: string;
style: StyleProperties;
}>;

class AccordionContentState {
item = undefined as unknown as AccordionItemState;
originalStyles: { transitionDuration: string; animationName: string } | undefined = undefined;
isMountAnimationPrevented = false;
width = boxedState(0);
height = boxedState(0);
presentEl = boxedState<HTMLElement | undefined>(undefined);
forceMount = undefined as unknown as ReadonlyBox<boolean>;
present = $derived(this.item.isSelected);
node = boxedState<HTMLElement | null>(null);
#id = undefined as unknown as ReadonlyBox<string>;
#originalStyles: { transitionDuration: string; animationName: string } | undefined = undefined;
#isMountAnimationPrevented = false;
#width = boxedState(0);
#height = boxedState(0);
#forceMount = undefined as unknown as ReadonlyBox<boolean>;
present = $derived(this.#forceMount.value || this.item.isSelected);
#styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#attrs = $derived({
id: this.#id.value,
"data-state": getDataOpenClosed(this.item.isSelected),
"data-disabled": getDataDisabled(this.item.isDisabled),
"data-value": this.item.value,
"data-accordion-content": "",
style: styleToString({
...this.#styleProp.value,
"--bits-accordion-content-height": `${this.#height.value}px`,
"--bits-accordion-content-width": `${this.#width.value}px`,
}),
} as const);

style: StyleProperties = $derived({
"--bits-accordion-content-height": `${this.height.value}px`,
"--bits-accordion-content-width": `${this.width.value}px`,
});

constructor(props: AccordionContentStateProps, item: AccordionItemState) {
this.item = item;
this.forceMount = props.forceMount;
this.isMountAnimationPrevented = this.item.isSelected;
this.presentEl = props.presentEl;
this.#forceMount = props.forceMount;
this.#isMountAnimationPrevented = this.item.isSelected;
this.#id = props.id;
this.#styleProp = props.style;

$effect.root(() => {
tick().then(() => {
this.node.value = document.getElementById(this.#id.value);
});
});

$effect.pre(() => {
const rAF = requestAnimationFrame(() => {
this.isMountAnimationPrevented = false;
this.#isMountAnimationPrevented = false;
});

return () => {
Expand All @@ -290,13 +300,14 @@ class AccordionContentState {

$effect(() => {
// eslint-disable-next-line no-unused-expressions
this.item.isSelected;
const node = untrack(() => this.presentEl.value);
this.present;
const node = this.node.value;
if (!node) return;

tick().then(() => {
if (!this.node) return;
// get the dimensions of the element
this.originalStyles = this.originalStyles || {
this.#originalStyles = this.#originalStyles || {
transitionDuration: node.style.transitionDuration,
animationName: node.style.animationName,
};
Expand All @@ -306,12 +317,12 @@ class AccordionContentState {
node.style.animationName = "none";

const rect = node.getBoundingClientRect();
this.height.value = rect.height;
this.width.value = rect.width;
this.#height.value = rect.height;
this.#width.value = rect.width;

// unblock any animations/transitions that were originally set if not the initial render
if (!this.isMountAnimationPrevented) {
const { animationName, transitionDuration } = this.originalStyles;
if (!this.#isMountAnimationPrevented) {
const { animationName, transitionDuration } = this.#originalStyles;
node.style.transitionDuration = transitionDuration;
node.style.animationName = animationName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,39 @@
import { getAccordionContentState } from "../accordion.svelte.js";
import type { AccordionContentProps } from "../types.js";
import Presence from "$lib/bits/utilities/presence.svelte";
import { box, readonlyBox } from "$lib/internal/box.svelte.js";
import { styleToString } from "$lib/internal/style.js";
import { readonlyBox } from "$lib/internal/box.svelte.js";
import { generateId } from "$lib/internal/id.js";

let {
child,
asChild,
el: elProp = $bindable(),
el = $bindable(),
id: idProp = generateId(),
forceMount: forceMountProp = false,
children,
style: styleProp = {},
...restProps
}: AccordionContentProps & { forceMount?: boolean } = $props();

const el = box(
() => elProp,
(v) => (elProp = v)
);
}: AccordionContentProps = $props();

const id = readonlyBox(() => idProp);
const style = readonlyBox(() => styleProp);
const forceMount = readonlyBox(() => forceMountProp);
const content = getAccordionContentState({ presentEl: el, forceMount });
const content = getAccordionContentState({ forceMount, id, style });
</script>

<Presence forceMount={true} present={content.present} bind:el={el.value}>
{#snippet presence({ node, present })}
<Presence forceMount={true} present={content.present} node={content.node}>
{#snippet presence({ present })}
{@const mergedProps = {
...restProps,
...content.props,
style: styleToString({
...styleProp,
...content.style,
}),
hidden: present.value ? undefined : true,
}}
{#if asChild}
{@render child?.({ props: mergedProps })}
{@render child?.({
props: mergedProps,
})}
{:else}
<div {...mergedProps} bind:this={node.value} hidden={present.value ? undefined : true}>
<div {...mergedProps} bind:this={el}>
{@render children?.()}
</div>
{/if}
Expand Down
4 changes: 3 additions & 1 deletion packages/bits-ui/src/lib/bits/accordion/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@ export type AccordionContentPropsWithoutHTML<
inTransitionConfig?: TransitionParams<In>;
outTransition?: Out;
outTransitionConfig?: TransitionParams<Out>;
forceMount?: boolean;
id?: string;
}>;

export type AccordionContentProps<
T extends Transition = Transition,
In extends Transition = Transition,
Out extends Transition = Transition,
> = AccordionContentPropsWithoutHTML<T, In, Out> & PrimitiveDivAttributes;
> = AccordionContentPropsWithoutHTML<T, In, Out> & Omit<PrimitiveDivAttributes, "id">;

export type AccordionHeaderPropsWithoutHTML = WithAsChild<{
asChild?: boolean;
Expand Down
93 changes: 52 additions & 41 deletions packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getContext, onMount, setContext } from "svelte";
import { getContext, onMount, setContext, tick } from "svelte";
import { getAriaExpanded, getDataDisabled, getDataOpenClosed } from "$lib/internal/attrs.js";
import {
type Box,
Expand Down Expand Up @@ -53,30 +53,29 @@ class CollapsibleRootState {
}
}

type CollapsibleContentStateProps = BoxedValues<{
presentEl: HTMLElement | undefined;
}> &
ReadonlyBoxedValues<{
id: string;
style: StyleProperties;
}>;
type CollapsibleContentStateProps = ReadonlyBoxedValues<{
id: string;
style: StyleProperties;
forceMount: boolean;
}>;

class CollapsibleContentState {
root = undefined as unknown as CollapsibleRootState;
currentStyle = $state<{ transitionDuration: string; animationName: string }>();
styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#originalStyles: { transitionDuration: string; animationName: string } | undefined = undefined;
#styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
node = boxedState<HTMLElement | null>(null);
#isMountAnimationPrevented = $state(false);
#width = $state(0);
#height = $state(0);
#presentEl: Box<HTMLElement | undefined>;
present = $derived(this.root.open);
#forceMount = undefined as unknown as ReadonlyBox<boolean>;
present = $derived(this.#forceMount.value || this.root.open.value);
#attrs = $derived({
id: this.root.contentId.value,
"data-state": getDataOpenClosed(this.root.open.value),
"data-disabled": getDataDisabled(this.root.disabled.value),
"data-collapsible-content": "",
style: styleToString({
...this.styleProp.value,
...this.#styleProp.value,
"--bits-collapsible-content-height": this.#height ? `${this.#height}px` : undefined,
"--bits-collapsible-content-width": this.#width ? `${this.#width}px` : undefined,
}),
Expand All @@ -85,42 +84,55 @@ class CollapsibleContentState {
constructor(props: CollapsibleContentStateProps, root: CollapsibleRootState) {
this.root = root;
this.#isMountAnimationPrevented = root.open.value;
this.#presentEl = props.presentEl;
this.#forceMount = props.forceMount;
this.root.contentId = props.id;
this.styleProp = props.style;
this.#styleProp = props.style;

onMount(() => {
requestAnimationFrame(() => {
this.#isMountAnimationPrevented = false;
$effect.root(() => {
tick().then(() => {
this.node.value = document.getElementById(this.root.contentId.value);
});
});

$effect.pre(() => {
// eslint-disable-next-line no-unused-expressions
this.root.open.value;
const node = this.#presentEl.value;
if (!node) return;
const rAF = requestAnimationFrame(() => {
this.#isMountAnimationPrevented = false;
});

this.currentStyle = this.currentStyle || {
transitionDuration: node.style.transitionDuration,
animationName: node.style.animationName,
return () => {
cancelAnimationFrame(rAF);
};
});

// block any animations/transitions so the element renders at full dimensions
node.style.transitionDuration = "0s";
node.style.animationName = "none";

// get the dimensions of the element
const rect = node.getBoundingClientRect();
this.#height = rect.height;
this.#width = rect.width;

// unblock any animations/transitions that were originally set if not the initial render
if (!this.#isMountAnimationPrevented) {
const { animationName, transitionDuration } = this.currentStyle;
node.style.transitionDuration = transitionDuration;
node.style.animationName = animationName;
}
$effect(() => {
// eslint-disable-next-line no-unused-expressions
this.present;
const node = this.node.value;
if (!node) return;

tick().then(() => {
if (!this.node) return;
// get the dimensions of the element
this.#originalStyles = this.#originalStyles || {
transitionDuration: node.style.transitionDuration,
animationName: node.style.animationName,
};

// block any animations/transitions so the element renders at full dimensions
node.style.transitionDuration = "0s";
node.style.animationName = "none";

const rect = node.getBoundingClientRect();
this.#height = rect.height;
this.#width = rect.width;

// unblock any animations/transitions that were originally set if not the initial render
if (!this.#isMountAnimationPrevented) {
const { animationName, transitionDuration } = this.#originalStyles;
node.style.transitionDuration = transitionDuration;
node.style.animationName = animationName;
}
});
});
}

Expand All @@ -136,7 +148,6 @@ type CollapsibleTriggerStateProps = ReadonlyBoxedValues<{
class CollapsibleTriggerState {
#root = undefined as unknown as CollapsibleRootState;
#onclickProp = boxedState<CollapsibleTriggerStateProps["onclick"]>(readonlyBox(() => () => {}));

#attrs = $derived({
type: "button",
"aria-controls": this.#root.contentId.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,41 @@
import type { CollapsibleContentProps } from "../types.js";
import Presence from "$lib/bits/utilities/presence.svelte";
import { generateId } from "$lib/internal/id.js";
import { box, readonlyBox } from "$lib/internal/box.svelte.js";
import { readonlyBox } from "$lib/internal/box.svelte.js";

let {
child,
asChild,
el: elProp = $bindable(),
forceMount = false,
el = $bindable(),
forceMount: forceMountProp = false,
children,
id: idProp = generateId(),
style: styleProp = {},
...restProps
}: CollapsibleContentProps & { forceMount?: boolean } = $props();

const id = readonlyBox(() => idProp);
const el = box(
() => elProp,
(v) => (elProp = v)
);
const style = readonlyBox(() => styleProp);
const content = getCollapsibleContentState({ id, presentEl: el, style });
const forceMount = readonlyBox(() => forceMountProp);

const mergedProps = $derived({
...restProps,
...content.props,
});
const style = readonlyBox(() => styleProp);
const content = getCollapsibleContentState({ id, style, forceMount });
</script>

<Presence present={forceMount || content.root.open.value} bind:el={el.value}>
{#snippet presence({ node, present })}
<Presence forceMount={true} present={content.present} node={content.node}>
{#snippet presence({ present })}
{@const mergedProps = {
...restProps,
...content.props,
hidden: present.value ? undefined : true,
}}
{#if asChild}
{@render child?.({ props: { ...mergedProps, hidden: !present.value } })}
{@render child?.({
props: mergedProps,
})}
{:else}
<div {...mergedProps} hidden={!present.value} bind:this={node.value}>
<div {...mergedProps} bind:this={el}>
{@render children?.()}
</div>
{/if}
{/snippet}
</Presence>

<style>
[hidden="false"] {
display: block !important;
}
</style>
Loading
Loading