@bccampus/ui-components 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ import { omit } from "@/lib/object";
2
+ import { map, type PreinitializedMapStore, } from "nanostores";
3
+ import type { CompositeDataItemState, CompositeItemKey, CompositeDataItemOptions } from "./types";
4
+
5
+ export class CompositeDataItem<T extends object> implements Iterable<CompositeDataItem<T>> {
6
+ #key: CompositeItemKey;
7
+ parent: CompositeDataItem<T> | null = null;
8
+ children?: CompositeDataItem<T>[];
9
+ level: number;
10
+
11
+ data: PreinitializedMapStore<T>;
12
+ state: PreinitializedMapStore<CompositeDataItemState>;
13
+
14
+ pointers: {
15
+ left?: CompositeItemKey;
16
+ right?: CompositeItemKey;
17
+ up?: CompositeItemKey;
18
+ down?: CompositeItemKey;
19
+ };
20
+ childrenProp: string
21
+
22
+ constructor(item: T, options: CompositeDataItemOptions<T>, parent: CompositeDataItem<T> | null) {
23
+ this.#key = options.getItemKey(item);
24
+ this.data = map(omit(item, [options.itemChildrenProp]));
25
+ this.state = map({
26
+ focused: false,
27
+ selected: false,
28
+ disabled: false,
29
+ ...options.initialState
30
+ })
31
+ this.pointers = {};
32
+ this.parent = parent;
33
+ this.level = parent ? parent.level + 1 : 0;
34
+
35
+ this.childrenProp = options.itemChildrenProp;
36
+
37
+ const children = options.getItemChildren(item);
38
+ if (children) {
39
+ this.children = children.map(child => {
40
+ const childItem = new CompositeDataItem(child, options, this);
41
+ return childItem;
42
+ })
43
+ }
44
+ }
45
+
46
+ get key() {
47
+ return this.#key;
48
+ }
49
+
50
+ *[Symbol.iterator](): Iterator<CompositeDataItem<T>> {
51
+ if (this.key !== "ALL") yield this;
52
+ if (this.children)
53
+ for (const child of this.children)
54
+ for (const item of child) yield item;
55
+ }
56
+
57
+ toJSON() {
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const json: any = { ...this.data.get() };
60
+
61
+ if (this.children) {
62
+ json[this.childrenProp] = [];
63
+ for (const child of this.children)
64
+ json[this.childrenProp].push(child.toJSON());
65
+ }
66
+
67
+ return json as T;
68
+ }
69
+
70
+ descendants() {
71
+ if (!this.children) return [];
72
+
73
+ const descendants: CompositeDataItem<T>[] = [];
74
+ this.children.forEach((child) => {
75
+ descendants.push(child);
76
+ const childDescendants = child.descendants();
77
+ if (childDescendants) descendants.push(...childDescendants);
78
+ }
79
+ )
80
+
81
+ return descendants;
82
+ }
83
+
84
+ ancestors() {
85
+ const ancestors: CompositeDataItem<T>[] = [];
86
+ let parent = this.parent;
87
+ while (parent) {
88
+ ancestors.push(parent);
89
+ parent = parent.parent;
90
+ }
91
+
92
+ return ancestors;
93
+ }
94
+
95
+
96
+ toggleSelect(recursive: boolean = false) {
97
+ if (this.state.get().selected) this.deselect(recursive);
98
+ else this.select(recursive);
99
+ }
100
+
101
+ select(recursive: boolean = false): CompositeItemKey[] {
102
+ this.state.setKey("selected", true);
103
+
104
+ if (recursive && this.children) {
105
+ const selectedChildKeys = this.children.map(child => child.select(true)).flat();
106
+ return [this.key, ...selectedChildKeys];
107
+ }
108
+
109
+ return [this.key]
110
+ }
111
+
112
+ deselect(recursive: boolean = false): CompositeItemKey[] {
113
+ this.state.setKey("selected", false);
114
+
115
+ if (recursive && this.children) {
116
+ const deselectedChildKeys = this.children.map(child => child.deselect(true)).flat();
117
+ return [this.key, ...deselectedChildKeys];
118
+ }
119
+
120
+ return [this.key];
121
+ }
122
+
123
+ disable(recursive: boolean = false): CompositeItemKey[] {
124
+ this.state.setKey("disabled", true);
125
+
126
+ if (recursive && this.children) {
127
+ const disabledChildKeys = this.children.map(child => child.disable(true)).flat();
128
+ return [this.key, ...disabledChildKeys];
129
+ }
130
+
131
+ return [this.key]
132
+ }
133
+
134
+ enable(recursive: boolean = false): CompositeItemKey[] {
135
+ this.state.setKey("disabled", false);
136
+
137
+ if (recursive && this.children) {
138
+ const enabledChildKeys = this.children.map(child => child.enable(true)).flat();
139
+ return [this.key, ...enabledChildKeys];
140
+ }
141
+
142
+ return [this.key];
143
+ }
144
+ }
@@ -0,0 +1,50 @@
1
+ import { useMemo } from "react";
2
+ import type { CompositeItemEventHandlers, CompositeItemProps } from "./types";
3
+ import { useStore } from "@nanostores/react";
4
+
5
+ export function CompositeComponentItem<T extends object>({
6
+ id,
7
+ className,
8
+ item,
9
+ mouseEventHandler,
10
+ keyboardEventHandler,
11
+ render,
12
+ }: CompositeItemProps<T>) {
13
+ const data = useStore(item.data);
14
+ const state = useStore(item.state);
15
+
16
+ const handlers: CompositeItemEventHandlers = useMemo(() => {
17
+ if (state.disabled) return {};
18
+
19
+ return {
20
+ mouseEventHandler: () => mouseEventHandler?.(item),
21
+ keyboardEventHandler: () => keyboardEventHandler?.(item),
22
+ };
23
+ }, [state.disabled, item, keyboardEventHandler, mouseEventHandler]);
24
+
25
+ return (
26
+ <div
27
+ id={id}
28
+ role="option"
29
+ aria-disabled={state.disabled}
30
+ data-key={item.key}
31
+ tabIndex={!state.disabled ? (state.focused ? 0 : -1) : undefined}
32
+ className={className}
33
+ >
34
+ {render({ data, key: item.key, level: item.level }, state, handlers)}
35
+ {item.children &&
36
+ item.children.length > 0 &&
37
+ [...item.children].map((child) => (
38
+ <CompositeComponentItem
39
+ id={`${id}-${item.key}`}
40
+ item={child}
41
+ key={child.key}
42
+ render={render}
43
+ className={className}
44
+ mouseEventHandler={mouseEventHandler}
45
+ keyboardEventHandler={keyboardEventHandler}
46
+ />
47
+ ))}
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,100 @@
1
+ import { useCallback, useImperativeHandle, useRef } from "react";
2
+ import { CompositeDataItem } from "./CompositeDataItem";
3
+ import type { CompositeItemKey, CompositeProps } from "./types";
4
+ import { useKeyboardEvent } from "@/hooks/use-keyboard-event";
5
+ import { useCompositeContext } from "./composite-data-context";
6
+ import { CompositeComponentItem } from "./composite-component-item";
7
+ import { useId } from "@/hooks/use-id";
8
+
9
+ export function CompositeComponent<T extends object>({
10
+ renderItem,
11
+ className,
12
+ itemClassName,
13
+ ref,
14
+ id,
15
+ ...props
16
+ }: CompositeProps<T>) {
17
+ const compositeData = useCompositeContext<T>();
18
+ const compositeRef = useRef<HTMLDivElement>(null);
19
+ const compositeId = useId(id);
20
+
21
+ const focus = useCallback(
22
+ (itemKey: CompositeItemKey) => {
23
+ compositeData.focus(itemKey);
24
+ // const focusedItemEl = compositeRef.current?.querySelector<HTMLDivElement>(`[data-key="${itemKey}"]`);
25
+ // if (focusedItemEl) focusedItemEl.focus();
26
+ },
27
+ [compositeData]
28
+ );
29
+
30
+ const focusDown = useCallback(() => {
31
+ if (compositeData.focusedItem.get()?.pointers.down) {
32
+ focus(compositeData.focusedItem.get()!.pointers.down!);
33
+ }
34
+ }, [compositeData, focus]);
35
+
36
+ const focusUp = useCallback(() => {
37
+ if (compositeData.focusedItem.get()?.pointers.up) {
38
+ focus(compositeData.focusedItem.get()!.pointers.up!);
39
+ }
40
+ }, [compositeData, focus]);
41
+
42
+ const handleKeyDown = useKeyboardEvent({
43
+ ArrowUp: focusUp,
44
+ ArrowLeft: focusUp,
45
+ ArrowDown: focusDown,
46
+ ArrowRight: focusDown,
47
+ Space: () => {
48
+ if (compositeData.focusedItem.get()) compositeData.toggleSelect(compositeData.focusedItem.get()!);
49
+ },
50
+ });
51
+
52
+ const handleItemMouseEvent = useCallback(
53
+ (item: CompositeDataItem<T>) => {
54
+ focus(item.key);
55
+ compositeData.toggleSelect(item);
56
+ },
57
+ [compositeData, focus]
58
+ );
59
+
60
+ useImperativeHandle(
61
+ ref,
62
+ () => {
63
+ return {
64
+ focusDown() {
65
+ focusDown();
66
+ },
67
+ focusUp() {
68
+ focusUp();
69
+ },
70
+ select() {
71
+ if (compositeData.focusedItem.get()) compositeData.toggleSelect(compositeData.focusedItem.get()!);
72
+ },
73
+ };
74
+ },
75
+ [compositeData, focusDown, focusUp]
76
+ );
77
+
78
+ return (
79
+ <div
80
+ ref={compositeRef}
81
+ id={compositeId}
82
+ className={className}
83
+ tabIndex={-1}
84
+ role="listbox"
85
+ onKeyDown={handleKeyDown}
86
+ {...props}
87
+ >
88
+ {[...compositeData].map((item) => (
89
+ <CompositeComponentItem
90
+ className={itemClassName}
91
+ id={`${compositeId}-${item.key}`}
92
+ item={item}
93
+ key={item.key}
94
+ render={renderItem}
95
+ mouseEventHandler={handleItemMouseEvent}
96
+ />
97
+ ))}
98
+ </div>
99
+ );
100
+ }
@@ -0,0 +1,31 @@
1
+ import { createContext, useContext, useMemo } from "react";
2
+ import { CompositeData } from "./CompositeData";
3
+ import type { CompositeOptions } from "./types";
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ const CompositeDataContext = createContext<CompositeData<any> | undefined>(undefined);
7
+
8
+ export function useCompositeContext<T extends object>() {
9
+ const context = useContext(CompositeDataContext);
10
+
11
+ if (!context) {
12
+ throw new Error("No CompositeDataContext has been set, use CompositeDataContext to provide a context");
13
+ }
14
+
15
+ return context as CompositeData<T>;
16
+ }
17
+
18
+ interface CompositeComponentContextProps<T extends object> extends CompositeOptions {
19
+ data: T[];
20
+ children: React.ReactNode;
21
+ }
22
+
23
+ export function CompositeComponentContext<T extends object>({
24
+ data,
25
+ children,
26
+ ...compositeOptions
27
+ }: CompositeComponentContextProps<T>) {
28
+ const composite = useMemo(() => new CompositeData(data, compositeOptions), [data, compositeOptions]);
29
+
30
+ return <CompositeDataContext value={composite}>{children}</CompositeDataContext>;
31
+ }
@@ -0,0 +1,4 @@
1
+ export * from './composite-component.tsx'
2
+ export * from './composite-component-item.tsx'
3
+ export * from './composite-data-context.tsx'
4
+ export * from './types.ts'
@@ -0,0 +1,81 @@
1
+ import type { KeyboardEventHandler, MouseEventHandler, ReactNode } from "react";
2
+ import type { CompositeDataItem } from "./CompositeDataItem";
3
+
4
+ export type CompositeItemKey = string | number;
5
+
6
+ export interface CompositeOptions {
7
+ disabledKeys?: CompositeItemKey[];
8
+ selectedKeys?: CompositeItemKey[];
9
+
10
+ itemKeyProp?: string;
11
+ itemChildrenProp?: string;
12
+ }
13
+
14
+ interface CompositeDataPropGetters<T> {
15
+ getItemKey: (item: T) => CompositeItemKey;
16
+ getItemChildren: (item: T) => T[] | undefined;
17
+ }
18
+
19
+ export type CompositeDataOptions<T> = Required<CompositeOptions> & CompositeDataPropGetters<T>;
20
+
21
+ export type CompositeDataItemOptions<T> = CompositeDataPropGetters<T> & {
22
+ initialState?: CompositeDataItemState;
23
+ itemChildrenProp: string;
24
+ };
25
+
26
+ export interface CompositeDataItemState {
27
+ focused: boolean;
28
+ selected: boolean;
29
+ disabled: boolean;
30
+ }
31
+
32
+ export interface CompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
33
+ renderItem: CompositeItemRenderFn<T>;
34
+ className?: string;
35
+ itemClassName?: string;
36
+ ref?: React.Ref<CompositeHandle>
37
+ }
38
+
39
+
40
+ export interface CompositeHandle {
41
+ focusUp: () => void;
42
+ focusDown: () => void;
43
+ select: () => void;
44
+ }
45
+
46
+ export interface CompositeItemEventHandlers {
47
+ mouseEventHandler?: MouseEventHandler<HTMLElement>;
48
+ keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
49
+ }
50
+
51
+ export type CompositeItemRenderFn<T extends object> = (
52
+ item: { data: T; level: number; key: CompositeItemKey },
53
+ state: CompositeDataItemState,
54
+ eventHandlers: CompositeItemEventHandlers
55
+ ) => ReactNode;
56
+
57
+ export interface CompositeItemProps<T extends object> {
58
+ id: string;
59
+ className?: string;
60
+
61
+ item: CompositeDataItem<T>;
62
+
63
+ mouseEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
64
+ keyboardEventHandler?: (itemAtom: CompositeDataItem<T>) => void;
65
+
66
+ remove?: () => void;
67
+ render: CompositeItemRenderFn<T>;
68
+ }
69
+
70
+ export interface CompositeFocusProvider {
71
+ setPointers: () => void;
72
+
73
+ goFirst: () => void;
74
+ goLast: () => void;
75
+
76
+ goUp: () => void;
77
+ goDown: () => void;
78
+ goLeft: () => void;
79
+ goRight: () => void;
80
+
81
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./icon-generator.tsx"
2
+ export * from "./types"
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Popover({
7
+ ...props
8
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
9
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
10
+ }
11
+
12
+ function PopoverTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
15
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
16
+ }
17
+
18
+ function PopoverContent({
19
+ className,
20
+ align = "center",
21
+ sideOffset = 4,
22
+ ...props
23
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
24
+ return (
25
+ <PopoverPrimitive.Portal>
26
+ <PopoverPrimitive.Content
27
+ data-slot="popover-content"
28
+ align={align}
29
+ sideOffset={sideOffset}
30
+ className={cn(
31
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ </PopoverPrimitive.Portal>
37
+ )
38
+ }
39
+
40
+ function PopoverAnchor({
41
+ ...props
42
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
43
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
44
+ }
45
+
46
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
@@ -0,0 +1,27 @@
1
+ import type { DependencyList, EffectCallback } from 'react';
2
+
3
+ import { useEffect, useRef } from 'react';
4
+
5
+ export function useEffectAfterMount(fn: EffectCallback, dependencies?: DependencyList) {
6
+ const mounted = useRef(false);
7
+
8
+ useEffect(
9
+ () => () => {
10
+ mounted.current = false;
11
+ },
12
+ []
13
+ );
14
+
15
+ useEffect(
16
+ () => {
17
+ if (mounted.current) {
18
+ return fn();
19
+ }
20
+
21
+ mounted.current = true;
22
+ return undefined;
23
+ },
24
+ // eslint-disable-next-line react-hooks/exhaustive-deps
25
+ dependencies
26
+ );
27
+ }
@@ -0,0 +1,5 @@
1
+ import { useId as reactUseId } from "react";
2
+
3
+ export function useId(id?: string): string {
4
+ return id ?? reactUseId();
5
+ }
@@ -0,0 +1,144 @@
1
+ import type { KeyboardEvent, KeyboardEventHandler } from 'react';
2
+ import { useMemo } from 'react';
3
+
4
+ const MODIFIER_KEYS = new Set(['ctrl', 'shift', 'alt', 'meta']);
5
+
6
+ const KEY_MAPPINGS: Record<string, string> = {
7
+ ' ': 'space',
8
+ };
9
+
10
+ export interface KeyBindings {
11
+ [sequence: string]: (event: KeyboardEvent) => void;
12
+ }
13
+
14
+ interface KeybindingLookupItem {
15
+ sequence: Set<string>;
16
+ handler: (event: KeyboardEvent) => void;
17
+ }
18
+
19
+ interface UseKeyboardEventOptions {
20
+ eventKeyProp: 'key' | 'code';
21
+ }
22
+
23
+ function isSequenceEqual<T>(sequenceA: Set<T>, sequenceB: Set<T>) {
24
+ if (sequenceA.size !== sequenceB.size) return false;
25
+
26
+ for (const element of sequenceB) {
27
+ if (!sequenceA.has(element)) return false;
28
+ }
29
+
30
+ return true;
31
+ }
32
+
33
+ function parseKeybindings(bindings: KeyBindings) {
34
+ const parsedKeybindings: KeybindingLookupItem[] = [];
35
+ for (const [sequence, handler] of Object.entries(bindings)) {
36
+ const parsedSequence = sequence
37
+ .toLowerCase()
38
+ .trim()
39
+ .split(/\s*\+\s*/);
40
+
41
+ if (parsedSequence.length === 1 && MODIFIER_KEYS.has(parsedSequence[0])) {
42
+ console.error(`[useKeyboardEvent] \`${sequence}\`: A key sequence cannot be only a modifier key.`);
43
+ }
44
+ else if (parsedSequence.includes('')) {
45
+ console.error(`[useKeyboardEvent] \`${sequence}\`: Unknown key defined in the sequence.`);
46
+ }
47
+ else {
48
+ parsedKeybindings.push({
49
+ sequence: new Set(parsedSequence),
50
+ handler,
51
+ });
52
+ }
53
+ }
54
+
55
+ return parsedKeybindings;
56
+ }
57
+
58
+ const defaultOptions: UseKeyboardEventOptions = {
59
+ eventKeyProp: 'key',
60
+ };
61
+
62
+ export function keyboardEventHandler(
63
+ bindings: KeyBindings,
64
+ options: UseKeyboardEventOptions = defaultOptions
65
+ ): KeyboardEventHandler {
66
+ const _options = { ...options, ...defaultOptions }
67
+
68
+ const keyBindings = parseKeybindings(bindings);
69
+
70
+ return (event: KeyboardEvent) => {
71
+ const keySequence = new Set<string>();
72
+ const eventKey = event[_options.eventKeyProp];
73
+
74
+ if (event.ctrlKey) keySequence.add('ctrl');
75
+
76
+ if (event.shiftKey) keySequence.add('shift');
77
+
78
+ if (event.altKey) keySequence.add('alt');
79
+
80
+ if (event.metaKey) keySequence.add('meta');
81
+
82
+ if (!KEY_MAPPINGS[eventKey]) keySequence.add(eventKey.toLowerCase());
83
+ else keySequence.add(KEY_MAPPINGS[eventKey]);
84
+
85
+ const matchedSequence = keyBindings.find(keyBinding => isSequenceEqual(keySequence, keyBinding.sequence));
86
+ if (matchedSequence) {
87
+ event.preventDefault();
88
+ event.stopPropagation();
89
+ matchedSequence.handler(event);
90
+ }
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Returns a `KeyboardEventHandler`
96
+ * that checks the defined key binding sequences against a keyboard event
97
+ * and executes the handler of the first matched key binding.
98
+ *
99
+ * Limitations:
100
+ * - Space character (` `) cannot be used in the key sequences.
101
+ * Use the `space` keyword instead.
102
+ * - Plus character (`+`) cannot be used in the key sequences.
103
+ * Use `shit + =` instead.
104
+ *
105
+ * @example
106
+ * ```
107
+ * export function Input({ value, onChange }: Props) {
108
+ * const [inputValue, setInputValue] = useState<string>('');
109
+ *
110
+ * const clearInput = () => {
111
+ * setInputValue('');
112
+ * };
113
+ *
114
+ * const addItem = () => {
115
+ * if (inputValue) {
116
+ * onChange([...value, inputValue]);
117
+ * clearInput();
118
+ * }
119
+ * };
120
+ *
121
+ * const deleteAll = () => {
122
+ * onChange([]);
123
+ * clearInput();
124
+ * };
125
+ *
126
+ * const handleKeyDown = useKeyboardEvent({
127
+ * 'enter': addItem,
128
+ * 'escape': clearInput,
129
+ * 'ctrl+c': clearInput,
130
+ * 'ctrl + shift + c': deleteAll,
131
+ * });
132
+ *
133
+ * return (
134
+ * <input
135
+ * value={inputValue}
136
+ * onChange={event => setInputValue(event.target.value)}
137
+ * onKeyDown={handleKeyDown}
138
+ * />;
139
+ * }
140
+ * ```
141
+ */
142
+ export function useKeyboardEvent(bindings: KeyBindings, options?: UseKeyboardEventOptions) {
143
+ return useMemo(() => keyboardEventHandler(bindings, options), [bindings, options]);
144
+ }
@@ -0,0 +1,48 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ export const get = <T extends object>(object: T, prop: string) => prop
4
+ .split('.')
5
+ .reduce<any>((reducedObject, key) => (reducedObject && key in reducedObject) ? reducedObject[key] : undefined, object);
6
+
7
+ export const set = <T extends object, V>(object: T, prop: string, value: V) => {
8
+ const propChunks = prop.split('.');
9
+ const lastChunk = propChunks.pop();
10
+ if (!lastChunk) return object;
11
+
12
+ const ref = propChunks.reduce<any>((reducedObject, key) => {
13
+ reducedObject[key] = {};
14
+ return reducedObject[key];
15
+ }, object);
16
+
17
+ ref[lastChunk] = value;
18
+
19
+ return object;
20
+ }
21
+
22
+ export const pick = <T extends object>(object: T, props: string[]) => {
23
+
24
+ return props.reduce<Record<string, unknown>>((result, key) => {
25
+
26
+ set(result, key, get(object, key));
27
+
28
+ return result;
29
+ }, {}) as Partial<T>;
30
+ }
31
+
32
+ export const omit = <T extends object>(object: T, props: string[]) => {
33
+ const result: Partial<T> = { ...object };
34
+
35
+ props.forEach(prop => {
36
+ const propChunks = prop.split('.');
37
+ const lastChunk = propChunks.pop();
38
+ if (lastChunk) {
39
+ const ref = propChunks.reduce<any>((reducedObject, key) => (reducedObject && key in reducedObject) ? reducedObject[key] : undefined, result);
40
+ if (ref && lastChunk in ref) delete ref[lastChunk];
41
+ }
42
+ })
43
+
44
+ return result;
45
+ }
46
+
47
+ export const isObject = (object: unknown) => (typeof object === 'object' && !Array.isArray(object) && object !== null);
48
+