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: more improvements #466

Merged
merged 19 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
130 changes: 57 additions & 73 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, onMount, setContext, tick, untrack } from "svelte";
import { getContext, setContext, tick, untrack } from "svelte";
import {
type Box,
type BoxedValues,
Expand All @@ -13,7 +13,6 @@ import {
getDataOpenClosed,
kbd,
readonlyBox,
styleToString,
verifyContextDeps,
} from "$lib/internal/index.js";
import type { StyleProperties } from "$lib/shared/index.js";
Expand All @@ -26,15 +25,10 @@ type AccordionBaseStateProps = ReadonlyBoxedValues<{
disabled: boolean;
}>;

interface AccordionRootAttrs {
id: string;
"data-accordion-root": string;
}

class AccordionBaseState {
id = undefined as unknown as ReadonlyBox<string>;
disabled: ReadonlyBox<boolean>;
#attrs: AccordionRootAttrs = $derived({
#attrs = $derived({
id: this.id.value,
"data-accordion-root": "",
} as const);
Expand Down Expand Up @@ -76,9 +70,8 @@ export class AccordionSingleState extends AccordionBaseState {
/**
* MULTIPLE
*/
interface AccordionMultiStateProps extends AccordionBaseStateProps {
value: Box<string[]>;
}

type AccordionMultiStateProps = AccordionBaseStateProps & BoxedValues<{ value: string[] }>;

export class AccordionMultiState extends AccordionBaseState {
#value: Box<string[]>;
Expand Down Expand Up @@ -116,7 +109,7 @@ type AccordionItemStateProps = ReadonlyBoxedValues<{
export class AccordionItemState {
#value: ReadonlyBox<string>;
disabled = undefined as unknown as ReadonlyBox<boolean>;
root: AccordionState = undefined as unknown as AccordionState;
root = undefined as unknown as AccordionState;
isSelected = $derived(this.root.includesItem(this.value));
isDisabled = $derived(this.disabled.value || this.root.disabled.value);
#attrs = $derived({
Expand Down Expand Up @@ -164,60 +157,60 @@ type AccordionTriggerStateProps = ReadonlyBoxedValues<{
}>;

class AccordionTriggerState {
disabled = undefined as unknown as ReadonlyBox<boolean>;
id = undefined as unknown as ReadonlyBox<string>;
root = undefined as unknown as AccordionState;
itemState = undefined as unknown as AccordionItemState;
onclickProp = boxedState<AccordionTriggerStateProps["onclick"]>(readonlyBox(() => () => {}));
onkeydownProp = boxedState<AccordionTriggerStateProps["onkeydown"]>(
#disabled = undefined as unknown as ReadonlyBox<boolean>;
#id = undefined as unknown as ReadonlyBox<string>;
#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
#isDisabled = $derived(
this.#disabled.value || this.#itemState.disabled.value || this.#root.disabled.value
);
#attrs: Record<string, unknown> = $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),
#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);

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.#disabled = props.disabled;
this.#itemState = itemState;
this.#root = itemState.root;
this.#onclickProp.value = props.onclick;
this.#onkeydownProp.value = props.onkeydown;
this.#id = props.id;
}

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

onkeydown = composeHandlers(this.onkeydownProp, (e: KeyboardEvent) => {
#onkeydown = composeHandlers(this.#onkeydownProp, (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;
if (this.#isDisabled || !handledKeys.includes(e.key)) return;

e.preventDefault();

if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
this.itemState.updateValue();
this.#itemState.updateValue();
return;
}

if (!this.root.id.value || !this.id.value) return;
if (!this.#root.id.value || !this.#id.value) return;

const rootEl = document.getElementById(this.root.id.value);
const rootEl = document.getElementById(this.#root.id.value);
if (!rootEl) return;
const itemEl = document.getElementById(this.id.value);
const itemEl = document.getElementById(this.#id.value);
if (!itemEl) return;

const items = Array.from(rootEl.querySelectorAll<HTMLElement>("[data-accordion-trigger]"));
Expand All @@ -241,8 +234,8 @@ class AccordionTriggerState {
get props() {
return {
...this.#attrs,
onclick: this.onclick,
onkeydown: this.onkeydown,
onclick: this.#onclick,
onkeydown: this.#onkeydown,
};
}
}
Expand All @@ -260,16 +253,14 @@ type AccordionContentStateProps = BoxedValues<{

class AccordionContentState {
item = undefined as unknown as AccordionItemState;
originalStyles = boxedState<{ transitionDuration: string; animationName: string } | undefined>(
undefined
);
isMountAnimationPrevented = $state(false);
originalStyles: { transitionDuration: string; animationName: string } | undefined = undefined;
isMountAnimationPrevented = false;
width = boxedState(0);
height = boxedState(0);
presentEl: Box<HTMLElement | undefined> = boxedState<HTMLElement | undefined>(undefined);
presentEl = boxedState<HTMLElement | undefined>(undefined);
forceMount = undefined as unknown as ReadonlyBox<boolean>;
present = $derived(this.item.isSelected);
#attrs: Record<string, unknown> = $derived({
#attrs = $derived({
"data-state": getDataOpenClosed(this.item.isSelected),
"data-disabled": getDataDisabled(this.item.isDisabled),
"data-value": this.item.value,
Expand Down Expand Up @@ -305,7 +296,7 @@ class AccordionContentState {

tick().then(() => {
// get the dimensions of the element
this.originalStyles.value = this.originalStyles.value || {
this.originalStyles = this.originalStyles || {
transitionDuration: node.style.transitionDuration,
animationName: node.style.animationName,
};
Expand All @@ -319,8 +310,8 @@ class AccordionContentState {
this.width.value = rect.width;

// unblock any animations/transitions that were originally set if not the initial render
if (!untrack(() => this.isMountAnimationPrevented)) {
const { animationName, transitionDuration } = this.originalStyles.value;
if (!this.isMountAnimationPrevented) {
const { animationName, transitionDuration } = this.originalStyles;
node.style.transitionDuration = transitionDuration;
node.style.animationName = animationName;
}
Expand All @@ -337,8 +328,8 @@ class AccordionContentState {
* CONTEXT METHODS
*/

export const ACCORDION_ROOT_KEY = "Accordion.Root";
export const ACCORDION_ITEM_KEY = "Accordion.Item";
export const ACCORDION_ROOT_KEY = Symbol("Accordion.Root");
export const ACCORDION_ITEM_KEY = Symbol("Accordion.Item");

type AccordionState = AccordionSingleState | AccordionMultiState;

Expand All @@ -350,23 +341,16 @@ type InitAccordionProps = {
};

export function setAccordionRootState(props: InitAccordionProps) {
if (props.type === "single") {
const { value, type, ...rest } = props;
return setContext(
ACCORDION_ROOT_KEY,
new AccordionSingleState({ ...rest, value: value as Box<string> })
);
} else {
const { value, type, ...rest } = props;
return setContext(
ACCORDION_ROOT_KEY,
new AccordionMultiState({ ...rest, value: value as Box<string[]> })
);
}
const { type, ...rest } = props;
const rootState =
type === "single"
? new AccordionSingleState(rest as AccordionSingleStateProps)
: new AccordionMultiState(rest as AccordionMultiStateProps);
return setContext(ACCORDION_ROOT_KEY, rootState);
}

export function getAccordionRootState(): AccordionState {
return getContext(ACCORDION_ROOT_KEY);
export function getAccordionRootState() {
return getContext<AccordionState>(ACCORDION_ROOT_KEY);
}

export function setAccordionItemState(props: Omit<AccordionItemStateProps, "rootState">) {
Expand All @@ -377,8 +361,8 @@ export function setAccordionItemState(props: Omit<AccordionItemStateProps, "root
return itemState;
}

export function getAccordionItemState(): AccordionItemState {
return getContext(ACCORDION_ITEM_KEY);
export function getAccordionItemState() {
return getContext<AccordionItemState>(ACCORDION_ITEM_KEY);
}

export function getAccordionTriggerState(props: AccordionTriggerStateProps): AccordionTriggerState {
Expand Down
42 changes: 10 additions & 32 deletions packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,54 +19,43 @@ type AvatarRootStateProps = {
style: ReadonlyBox<StyleProperties>;
};

interface AvatarRootAttrs {
"data-avatar-root": string;
"data-status": ImageLoadingStatus;
}

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: AvatarRootAttrs = $derived({
#attrs = $derived({
"data-avatar-root": "",
"data-status": this.loadingStatus.value,
style: styleToString(this.styleProp.value),
});

#imageTimerId: NodeJS.Timeout | undefined = undefined;
} as const);

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

$effect.pre(() => {
if (!this.src.value) return;
this.#loadImage(this.src.value);
return this.#loadImage(this.src.value);
});
}

#loadImage(src: string) {
// clear any existing timers before creating a new one
clearTimeout(this.#imageTimerId);
let imageTimerId: NodeJS.Timeout;
const image = new Image();
image.src = src;
this.loadingStatus.value = "loading";
image.onload = () => {
// if its 0 then we don't need to add a delay
if (this.delayMs.value !== 0) {
this.#imageTimerId = setTimeout(() => {
this.loadingStatus.value = "loaded";
}, this.delayMs.value);
} else {
imageTimerId = setTimeout(() => {
this.loadingStatus.value = "loaded";
}
}, this.delayMs.value);
};
image.onerror = () => {
this.loadingStatus.value = "error";
};
return () => clearTimeout(imageTimerId);
}

createImage(props: AvatarImageStateProps) {
Expand All @@ -86,12 +75,6 @@ class AvatarRootState {
* IMAGE
*/

interface AvatarImageAttrs {
style: string;
src: AvatarImageSrc;
"data-avatar-image": string;
}

type AvatarImageStateProps = ReadonlyBoxedValues<{
src: AvatarImageSrc;
style: StyleProperties;
Expand All @@ -100,7 +83,7 @@ type AvatarImageStateProps = ReadonlyBoxedValues<{
class AvatarImageState {
root = undefined as unknown as AvatarRootState;
styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#attrs: AvatarImageAttrs = $derived({
#attrs = $derived({
style: styleToString({
...this.styleProp.value,
display: this.root.loadingStatus.value === "loaded" ? "block" : "none",
Expand All @@ -124,19 +107,14 @@ class AvatarImageState {
* FALLBACK
*/

interface AvatarFallbackAttrs {
style: string;
"data-avatar-fallback": string;
}

type AvatarFallbackStateProps = ReadonlyBoxedValues<{
style: StyleProperties;
}>;

class AvatarFallbackState {
root = undefined as unknown as AvatarRootState;
styleProp = undefined as unknown as ReadonlyBox<StyleProperties>;
#attrs: AvatarFallbackAttrs = $derived({
#attrs = $derived({
style: styleToString({
...this.styleProp.value,
display: this.root.loadingStatus.value === "loaded" ? "none" : "block",
Expand Down
Loading
Loading