interactive.ts

 1import merge from "ts-deepmerge"
 2import { DeepPartial } from "utility-types"
 3
 4export type InteractiveState =
 5    | "default"
 6    | "hovered"
 7    | "clicked"
 8    | "selected"
 9    | "disabled"
10
11export type Interactive<T> = {
12    default: T
13    hovered?: T
14    clicked?: T
15    selected?: T
16    disabled?: T
17}
18
19export const NO_DEFAULT_OR_BASE_ERROR =
20    "An interactive object must have a default state, or a base property."
21export const NOT_ENOUGH_STATES_ERROR =
22    "An interactive object must have a default and at least one other state."
23
24interface InteractiveProps<T> {
25    base?: T
26    state: Partial<Record<InteractiveState, DeepPartial<T>>>
27}
28
29/**
30 * Helper function for creating Interactive<T> objects that works with Toggle<T>-like behavior.
31 * It takes a default object to be used as the value for `default` field and fills out other fields
32 * with fields from either `base` or from the `state` object which contains values for specific states.
33 * Notably, it does not touch `hover`, `clicked`, `selected` and `disabled` states if there are no modifications for them.
34 *
35 * @param defaultObj Object to be used as the value for the `default` field.
36 * @param base Optional object containing base fields to be included in the resulting object.
37 * @param state Object containing optional modified fields to be included in the resulting object for each state.
38 * @returns Interactive<T> object with fields from `base` and `state`.
39 */
40export function interactive<T extends object>({
41    base,
42    state,
43}: InteractiveProps<T>): Interactive<T> {
44    if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
45
46    let default_state: T
47
48    if (state.default && base) {
49        default_state = merge(base, state.default) as T
50    } else {
51        default_state = base ? base : (state.default as T)
52    }
53
54    const interactive_obj: Interactive<T> = {
55        default: default_state,
56    }
57
58    let state_count = 0
59
60    if (state.hovered !== undefined) {
61        interactive_obj.hovered = merge(
62            interactive_obj.default,
63            state.hovered
64        ) as T
65        state_count++
66    }
67
68    if (state.clicked !== undefined) {
69        interactive_obj.clicked = merge(
70            interactive_obj.default,
71            state.clicked
72        ) as T
73        state_count++
74    }
75
76    if (state.selected !== undefined) {
77        interactive_obj.selected = merge(
78            interactive_obj.default,
79            state.selected
80        ) as T
81        state_count++
82    }
83
84    if (state.disabled !== undefined) {
85        interactive_obj.disabled = merge(
86            interactive_obj.default,
87            state.disabled
88        ) as T
89        state_count++
90    }
91
92    if (state_count < 1) {
93        throw new Error(NOT_ENOUGH_STATES_ERROR)
94    }
95
96    return interactive_obj
97}