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}