Skip to content

Commit

Permalink
next: remove derived attrs (#472)
Browse files Browse the repository at this point in the history
  • Loading branch information
anatolzak authored Apr 17, 2024
1 parent ae9ce2b commit c645dd9
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 291 deletions.
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

0 comments on commit c645dd9

Please sign in to comment.