Skip to content

Commit

Permalink
simplify composing event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
anatolzak committed Apr 17, 2024
1 parent 31f7aa3 commit c83b8ef
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 44 deletions.
22 changes: 10 additions & 12 deletions packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,16 @@ class AccordionTriggerState {
#node = boxedState<HTMLElement | null>(null);
#root: AccordionState;
#itemState: AccordionItemState;
#onclickProp = boxedState<AccordionTriggerStateProps["onclick"]>(readonlyBox(() => () => {}));
#onkeydownProp = boxedState<AccordionTriggerStateProps["onkeydown"]>(
readonlyBox(() => () => {})
);
#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);
}
Expand All @@ -199,12 +197,12 @@ class AccordionTriggerState {
return this.#disabled.value || this.#itemState.disabled.value || this.#root.disabled.value;
}

#onclick = composeHandlers(this.#onclickProp, () => {
#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 @@ -230,7 +228,7 @@ class AccordionTriggerState {
};

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

get props() {
return {
Expand All @@ -243,8 +241,8 @@ class AccordionTriggerState {
"data-state": getDataOpenClosed(this.#itemState.isSelected),
"data-accordion-trigger": "",
//
onclick: this.#onclick,
onkeydown: this.#onkeydown,
onclick: this.#composedClick,
onkeydown: this.#composedKeydown,
} as const;
}
}
Expand Down
20 changes: 10 additions & 10 deletions packages/bits-ui/src/lib/bits/checkbox/checkbox.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,31 @@ class CheckboxRootState {
required: ReadonlyBox<boolean>;
name: ReadonlyBox<string | undefined>;
value: ReadonlyBox<string | undefined>;
#onclickProp = boxedState<CheckboxRootStateProps["onclick"]>(readonlyBox(() => () => {}));
#onkeydownProp = boxedState<CheckboxRootStateProps["onkeydown"]>(readonlyBox(() => () => {}));
#composedClick: EventCallback<MouseEvent>;
#composedKeydown: EventCallback<KeyboardEvent>;

constructor(props: CheckboxRootStateProps) {
this.checked = props.checked;
this.disabled = props.disabled;
this.required = props.required;
this.name = props.name;
this.value = props.value;
this.#onclickProp.value = props.onclick;
this.#onkeydownProp.value = props.onkeydown;
this.#composedClick = composeHandlers(props.onclick, this.#onclick);
this.#composedKeydown = composeHandlers(props.onkeydown, this.#onkeydown);
}

#onkeydown = composeHandlers(this.#onkeydownProp, (e) => {
#onkeydown = (e: KeyboardEvent) => {
if (e.key === kbd.ENTER) e.preventDefault();
});
};

#onclick = composeHandlers(this.#onclickProp, () => {
#onclick = () => {
if (this.disabled.value) return;
if (this.checked.value === "indeterminate") {
this.checked.value = true;
return;
}
this.checked.value = !this.checked.value;
});
};

createIndicator() {
return new CheckboxIndicatorState(this);
Expand All @@ -85,8 +85,8 @@ class CheckboxRootState {
"data-checkbox-root": "",
disabled: this.disabled.value,
//
onclick: this.#onclick,
onkeydown: this.#onkeydown,
onclick: this.#composedClick,
onkeydown: this.#composedKeydown,
} as const;
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,16 @@ type CollapsibleTriggerStateProps = ReadonlyBoxedValues<{

class CollapsibleTriggerState {
#root: CollapsibleRootState;
#onclickProp = boxedState<CollapsibleTriggerStateProps["onclick"]>(readonlyBox(() => () => {}));
#composedClick: EventCallback<MouseEvent>;

constructor(props: CollapsibleTriggerStateProps, root: CollapsibleRootState) {
this.#root = root;
this.#onclickProp.value = props.onclick;
this.#composedClick = composeHandlers(props.onclick, this.#onclick);
}

#onclick = composeHandlers(this.#onclickProp, () => {
#onclick = () => {
this.#root.toggleOpen();
});
};

get props() {
return {
Expand All @@ -168,7 +168,7 @@ class CollapsibleTriggerState {
disabled: this.#root.disabled.value,
"data-collapsible-trigger": "",
//
onclick: this.#onclick,
onclick: this.#composedClick,
} as const;
}
}
Expand Down
12 changes: 6 additions & 6 deletions packages/bits-ui/src/lib/bits/label/label.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { type ReadonlyBoxedValues, boxedState, readonlyBox } from "$lib/internal/box.svelte.js";
import type { ReadonlyBoxedValues } from "$lib/internal/box.svelte.js";
import { type EventCallback, composeHandlers } from "$lib/internal/events.js";

type LabelRootStateProps = ReadonlyBoxedValues<{
onmousedown: EventCallback<MouseEvent>;
}>;

class LabelRootState {
#onmousedownProp = boxedState<LabelRootStateProps["onmousedown"]>(readonlyBox(() => () => {}));
#composedMousedown: EventCallback<MouseEvent>;

constructor(props: LabelRootStateProps) {
this.#onmousedownProp.value = props.onmousedown;
this.#composedMousedown = composeHandlers(props.onmousedown, this.#onmousedown);
}

#onmousedown = composeHandlers(this.#onmousedownProp, (e) => {
#onmousedown = (e: MouseEvent) => {
if (e.detail > 1) e.preventDefault();
});
};

get props() {
return {
onmousedown: this.#onmousedown,
onmousedown: this.#composedMousedown,
};
}
}
Expand Down
19 changes: 11 additions & 8 deletions packages/bits-ui/src/lib/bits/radio-group/radio-group.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,26 @@ class RadioGroupItemState {
#root: RadioGroupRootState;
#disabled: RadioGroupItemStateProps["disabled"];
#value: RadioGroupItemStateProps["value"];
#onclickProp = boxedState<RadioGroupItemStateProps["onclick"]>(readonlyBox(() => () => {}));
#onkeydownProp = boxedState<RadioGroupItemStateProps["onkeydown"]>(readonlyBox(() => () => {}));
#composedClick: EventCallback<MouseEvent>;
#composedKeydown: EventCallback<KeyboardEvent>;

constructor(props: RadioGroupItemStateProps, root: RadioGroupRootState) {
this.#disabled = props.disabled;
this.#value = props.value;
this.#root = root;
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, () => {
#onclick = () => {
this.#root.selectValue(this.#value.value);
});
};

onkeydown = composeHandlers(this.#onkeydownProp, (e) => {
#onkeydown = (e: KeyboardEvent) => {
if (!this.#root.node.value || !this.#node.value) return;
const items = this.#root.getRadioItemNodes();
if (!items.length) return;
Expand Down Expand Up @@ -165,7 +168,7 @@ class RadioGroupItemState {
itemToFocus.focus();
this.#root.selectValue(itemToFocus.dataset.value as string);
}
});
};

get #isDisabled() {
return this.#disabled.value || this.#root.disabled.value;
Expand All @@ -187,8 +190,8 @@ class RadioGroupItemState {
type: "button",
role: "radio",
//
onclick,
onkeydown,
onclick: this.#composedClick,
onkeydown: this.#composedKeydown,
} as const;
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/bits-ui/src/lib/internal/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createEventDispatcher } from "svelte";
import type { Box, ReadonlyBox } from "./box.svelte.js";
import type { ReadonlyBox } from "./box.svelte.js";

type MeltEvent<T extends Event = Event> = {
detail: {
Expand Down Expand Up @@ -41,15 +41,15 @@ export type EventCallback<E extends Event = Event, T extends Element = Element>
) => void;

export function composeHandlers<E extends Event = Event, T extends Element = Element>(
...handlers: Array<EventCallback<E, T> | Box<ReadonlyBox<EventCallback<E, T>>> | undefined>
...handlers: Array<EventCallback<E, T> | ReadonlyBox<EventCallback<E, T>> | undefined>
): (e: E & { currentTarget: EventTarget & T }) => void {
return function (this: T, e: E & { currentTarget: EventTarget & T }) {
for (const handler of handlers) {
if (!handler || e.defaultPrevented) return;
if (typeof handler === "function") {
handler.call(this, e);
} else {
handler.value.value.call(this, e);
handler.value.call(this, e);
}
}
};
Expand Down

0 comments on commit c83b8ef

Please sign in to comment.