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 derived attrs #472

Merged
merged 7 commits into from
Apr 17, 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
145 changes: 73 additions & 72 deletions packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getContext, setContext, tick, untrack } from "svelte";
import { getContext, setContext } from "svelte";
import {
type Box,
type BoxedValues,
Expand Down Expand Up @@ -29,13 +29,9 @@ type AccordionBaseStateProps = ReadonlyBoxedValues<{
}>;

class AccordionBaseState {
id = undefined as unknown as ReadonlyBox<string>;
id: ReadonlyBox<string>;
node = boxedState<HTMLElement | null>(null);
disabled: ReadonlyBox<boolean>;
#attrs = $derived({
id: this.id.value,
"data-accordion-root": "",
} as const);

constructor(props: AccordionBaseStateProps) {
this.id = props.id;
Expand All @@ -52,7 +48,10 @@ class AccordionBaseState {
}

get props() {
return this.#attrs;
return {
id: this.id.value,
"data-accordion-root": "",
} as const;
}
}

Expand Down Expand Up @@ -121,15 +120,8 @@ type AccordionItemStateProps = ReadonlyBoxedValues<{

export class AccordionItemState {
#value: ReadonlyBox<string>;
disabled = undefined as unknown as ReadonlyBox<boolean>;
root = undefined as unknown as AccordionState;
isSelected = $derived(this.root.includesItem(this.value));
isDisabled = $derived(this.disabled.value || this.root.disabled.value);
#attrs = $derived({
"data-accordion-item": "",
"data-state": getDataOpenClosed(this.isSelected),
"data-disabled": getDataDisabled(this.isDisabled),
} as const);
disabled: ReadonlyBox<boolean>;
root: AccordionState;

constructor(props: AccordionItemStateProps) {
this.#value = props.value;
Expand All @@ -141,12 +133,24 @@ export class AccordionItemState {
return this.#value.value;
}

get isSelected() {
return this.root.includesItem(this.value);
}

get isDisabled() {
return this.disabled.value || this.root.disabled.value;
}

updateValue() {
this.root.toggleItem(this.value);
}

get props() {
return this.#attrs;
return {
"data-accordion-item": "",
"data-state": getDataOpenClosed(this.isSelected),
"data-disabled": getDataDisabled(this.isDisabled),
} as const;
}

createTrigger(props: AccordionTriggerStateProps) {
Expand All @@ -170,48 +174,35 @@ type AccordionTriggerStateProps = ReadonlyBoxedValues<{
}>;

class AccordionTriggerState {
#disabled = undefined as unknown as ReadonlyBox<boolean>;
#id = undefined as unknown as ReadonlyBox<string>;
#disabled: ReadonlyBox<boolean>;
#id: ReadonlyBox<string>;
#node = boxedState<HTMLElement | null>(null);
#root = undefined as unknown as AccordionState;
#itemState = undefined as unknown as AccordionItemState;
#onclickProp = boxedState<AccordionTriggerStateProps["onclick"]>(readonlyBox(() => () => {}));
#onkeydownProp = boxedState<AccordionTriggerStateProps["onkeydown"]>(
readonlyBox(() => () => {})
);

// Disabled if the trigger itself, the item it belongs to, or the root is disabled
#isDisabled = $derived(
this.#disabled.value || this.#itemState.disabled.value || this.#root.disabled.value
);
#attrs = $derived({
id: this.#id.value,
disabled: this.#isDisabled,
"aria-expanded": getAriaExpanded(this.#itemState.isSelected),
"aria-disabled": getAriaDisabled(this.#isDisabled),
"data-disabled": getDataDisabled(this.#isDisabled),
"data-value": this.#itemState.value,
"data-state": getDataOpenClosed(this.#itemState.isSelected),
"data-accordion-trigger": "",
} as const);
#root: AccordionState;
#itemState: AccordionItemState;
#composedClick: EventCallback<MouseEvent>;
#composedKeydown: EventCallback<KeyboardEvent>;

constructor(props: AccordionTriggerStateProps, itemState: AccordionItemState) {
this.#disabled = props.disabled;
this.#itemState = itemState;
this.#root = itemState.root;
this.#onclickProp.value = props.onclick;
this.#onkeydownProp.value = props.onkeydown;
this.#id = props.id;
this.#composedClick = composeHandlers(props.onclick, this.#onclick);
this.#composedKeydown = composeHandlers(props.onkeydown, this.#onkeydown);

useNodeById(this.#id, this.#node);
}

#onclick = composeHandlers(this.#onclickProp, () => {
get #isDisabled() {
return this.#disabled.value || this.#itemState.disabled.value || this.#root.disabled.value;
}

#onclick = () => {
if (this.#isDisabled) return;
this.#itemState.updateValue();
});
};

#onkeydown = composeHandlers(this.#onkeydownProp, (e: KeyboardEvent) => {
#onkeydown = (e: KeyboardEvent) => {
const handledKeys = [kbd.ARROW_DOWN, kbd.ARROW_UP, kbd.HOME, kbd.END, kbd.SPACE, kbd.ENTER];
if (this.#isDisabled || !handledKeys.includes(e.key)) return;

Expand All @@ -237,14 +228,22 @@ class AccordionTriggerState {
};

candidateItems[keyToIndex[e.key]!]?.focus();
});
};

get props() {
return {
...this.#attrs,
onclick: this.#onclick,
onkeydown: this.#onkeydown,
};
id: this.#id.value,
disabled: this.#isDisabled,
"aria-expanded": getAriaExpanded(this.#itemState.isSelected),
"aria-disabled": getAriaDisabled(this.#isDisabled),
"data-disabled": getDataDisabled(this.#isDisabled),
"data-value": this.#itemState.value,
"data-state": getDataOpenClosed(this.#itemState.isSelected),
"data-accordion-trigger": "",
//
onclick: this.#composedClick,
onkeydown: this.#composedKeydown,
} as const;
}
}

Expand All @@ -259,28 +258,15 @@ type AccordionContentStateProps = ReadonlyBoxedValues<{
}>;

class AccordionContentState {
item = undefined as unknown as AccordionItemState;
item: AccordionItemState;
node = boxedState<HTMLElement | null>(null);
#id = undefined as unknown as ReadonlyBox<string>;
#id: 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);
#width = $state(0);
#height = $state(0);
#forceMount: ReadonlyBox<boolean>;
#styleProp: ReadonlyBox<StyleProperties>;

constructor(props: AccordionContentStateProps, item: AccordionItemState) {
this.item = item;
Expand Down Expand Up @@ -320,8 +306,8 @@ class AccordionContentState {
node.style.animationName = "none";

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

// unblock any animations/transitions that were originally set if not the initial render
if (!this.#isMountAnimationPrevented) {
Expand All @@ -333,8 +319,23 @@ class AccordionContentState {
});
}

get present() {
return this.#forceMount.value || this.item.isSelected;
}

get props() {
return this.#attrs;
return {
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}px`,
"--bits-accordion-content-width": `${this.#width}px`,
}),
} as const;
}
}

Expand Down
56 changes: 27 additions & 29 deletions packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,13 @@ type AvatarImageSrc = string | null | undefined;
class AvatarRootState {
src = readonlyBox<AvatarImageSrc>(() => null);
delayMs: ReadonlyBox<number>;
loadingStatus = undefined as unknown as Box<ImageLoadingStatus>;
styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#attrs = $derived({
"data-avatar-root": "",
"data-status": this.loadingStatus.value,
style: styleToString(this.styleProp.value),
} as const);
loadingStatus: Box<ImageLoadingStatus>;
styleProp: ReadonlyBox<StyleProperties>;

constructor(props: AvatarRootStateProps) {
this.delayMs = props.delayMs;
this.loadingStatus = props.loadingStatus;
this.styleProp = props.style;

$effect.pre(() => {
if (!this.src.value) return;
Expand Down Expand Up @@ -67,7 +63,11 @@ class AvatarRootState {
}

get props() {
return this.#attrs;
return {
"data-avatar-root": "",
"data-status": this.loadingStatus.value,
style: styleToString(this.styleProp.value),
} as const;
}
}

Expand All @@ -81,16 +81,8 @@ type AvatarImageStateProps = ReadonlyBoxedValues<{
}>;

class AvatarImageState {
root = undefined as unknown as AvatarRootState;
styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#attrs = $derived({
style: styleToString({
...this.styleProp.value,
display: this.root.loadingStatus.value === "loaded" ? "block" : "none",
}),
"data-avatar-image": "",
src: this.root.src.value,
} as const);
root: AvatarRootState;
styleProp: ReadonlyBox<StyleProperties>;

constructor(props: AvatarImageStateProps, root: AvatarRootState) {
this.root = root;
Expand All @@ -99,7 +91,14 @@ class AvatarImageState {
}

get props() {
return this.#attrs;
return {
style: styleToString({
...this.styleProp.value,
display: this.root.loadingStatus.value === "loaded" ? "block" : "none",
}),
"data-avatar-image": "",
src: this.root.src.value,
} as const;
}
}

Expand All @@ -112,23 +111,22 @@ type AvatarFallbackStateProps = ReadonlyBoxedValues<{
}>;

class AvatarFallbackState {
root = undefined as unknown as AvatarRootState;
styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#attrs = $derived({
style: styleToString({
...this.styleProp.value,
display: this.root.loadingStatus.value === "loaded" ? "none" : "block",
}),
"data-avatar-fallback": "",
} as const);
root: AvatarRootState;
styleProp: ReadonlyBox<StyleProperties>;

constructor(props: AvatarFallbackStateProps, root: AvatarRootState) {
this.styleProp = props.style;
this.root = root;
}

get props() {
return this.#attrs;
return {
style: styleToString({
...this.styleProp.value,
display: this.root.loadingStatus.value === "loaded" ? "none" : "block",
}),
"data-avatar-fallback": "",
} as const;
}
}

Expand Down
Loading
Loading