Skip to content

Commit a2dbf11

Browse files
authored
refactor(keybinding controller): Default options refactoring (#1815)
1 parent ee9a4d3 commit a2dbf11

File tree

14 files changed

+99
-33
lines changed

14 files changed

+99
-33
lines changed

src/components/accordion/accordion.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,7 @@ export default class IgcAccordionComponent extends LitElement {
5454

5555
addSafeEventListener(this, 'igcOpening' as any, this.handlePanelOpening);
5656

57-
addKeybindings(this, {
58-
skip: this.skipKeybinding,
59-
bindingDefaults: { preventDefault: true },
60-
})
57+
addKeybindings(this, { skip: this.skipKeybinding })
6158
.set(homeKey, () =>
6259
this.getPanelHeader(first(this.enabledPanels)).focus()
6360
)

src/components/calendar/days-view/days-view.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,7 @@ export default class IgcDaysViewComponent extends EventEmitterMixin<
140140
super();
141141

142142
addThemingController(this, all);
143-
144-
addKeybindings(this, {
145-
bindingDefaults: { preventDefault: true },
146-
}).setActivateHandler(this.handleInteraction);
147-
143+
addKeybindings(this).setActivateHandler(this.handleInteraction);
148144
addSafeEventListener(this, 'click', this.handleInteraction);
149145
}
150146

src/components/calendar/months-view/months-view.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,7 @@ export default class IgcMonthsViewComponent extends EventEmitterMixin<
8686
super();
8787

8888
addThemingController(this, all);
89-
90-
addKeybindings(this, {
91-
bindingDefaults: { preventDefault: true },
92-
}).setActivateHandler(this.handleInteraction);
93-
89+
addKeybindings(this).setActivateHandler(this.handleInteraction);
9490
addSafeEventListener(this, 'click', this.handleInteraction);
9591
}
9692

src/components/calendar/years-view/years-view.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,7 @@ export default class IgcYearsViewComponent extends EventEmitterMixin<
6767
super();
6868

6969
addThemingController(this, all);
70-
71-
addKeybindings(this, {
72-
bindingDefaults: { preventDefault: true },
73-
}).setActivateHandler(this.handleInteraction);
74-
70+
addKeybindings(this).setActivateHandler(this.handleInteraction);
7571
addSafeEventListener(this, 'click', this.handleInteraction);
7672
}
7773

src/components/carousel/carousel.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,6 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
338338

339339
addKeybindings(this, {
340340
ref: this._indicatorsContainerRef,
341-
bindingDefaults: { preventDefault: true },
342341
})
343342
.set(arrowLeft, this._handleArrowLeft)
344343
.set(arrowRight, this._handleArrowRight)
@@ -347,12 +346,10 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
347346

348347
addKeybindings(this, {
349348
ref: this._prevButtonRef,
350-
bindingDefaults: { preventDefault: true },
351349
}).setActivateHandler(this._handleNavigationInteractionPrevious);
352350

353351
addKeybindings(this, {
354352
ref: this._nextButtonRef,
355-
bindingDefaults: { preventDefault: true },
356353
}).setActivateHandler(this._handleNavigationInteractionNext);
357354

358355
createMutationController(this, {

src/components/common/controllers/key-bindings.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { ReactiveController, ReactiveControllerHost } from 'lit';
22
import type { Ref } from 'lit/directives/ref.js';
33
import { createAbortHandle } from '../abort-handler.js';
4-
import { asArray, findElementFromEventPath, isFunction } from '../util.js';
4+
import {
5+
asArray,
6+
findElementFromEventPath,
7+
isFunction,
8+
toMerged,
9+
} from '../util.js';
510

611
//#region Keys and modifiers
712

@@ -179,6 +184,7 @@ class KeyBindingController implements ReactiveController {
179184

180185
private static readonly _defaultOptions: KeyBindingControllerOptions = {
181186
skip: ['input', 'textarea', 'select'],
187+
bindingDefaults: { preventDefault: true },
182188
};
183189

184190
private readonly _host: ReactiveControllerHost & Element;
@@ -209,7 +215,10 @@ class KeyBindingController implements ReactiveController {
209215
) {
210216
this._host = host;
211217
this._ref = options?.ref;
212-
this._options = { ...KeyBindingController._defaultOptions, ...options };
218+
this._options = toMerged(
219+
KeyBindingController._defaultOptions,
220+
options ?? {}
221+
);
213222

214223
if (Array.isArray(this._options.skip)) {
215224
this._skipSelector = this._options.skip.join(',');
@@ -343,7 +352,10 @@ class KeyBindingController implements ReactiveController {
343352
) {
344353
const { keys, modifiers } = parseKeys(key);
345354
const combination = createCombinationKey(keys, modifiers);
346-
const options = { ...this._options?.bindingDefaults, ...bindingOptions };
355+
const options = toMerged(
356+
this._options.bindingDefaults!,
357+
bindingOptions ?? {}
358+
);
347359

348360
for (const each of [...keys, ...modifiers]) {
349361
this._allowedKeys.add(each);

src/components/common/util.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,29 @@ export function isObject(value: unknown): value is object {
262262
return value != null && typeof value === 'object';
263263
}
264264

265+
export function isPlainObject(
266+
value: unknown
267+
): value is Record<PropertyKey, unknown> {
268+
if (!isObject(value)) {
269+
return false;
270+
}
271+
272+
const proto = Object.getPrototypeOf(value) as typeof Object.prototype | null;
273+
274+
const hasObjectPrototype =
275+
proto === null ||
276+
proto === Object.prototype ||
277+
Object.getPrototypeOf(proto) === null;
278+
279+
return hasObjectPrototype
280+
? Object.prototype.toString.call(value) === '[object Object]'
281+
: false;
282+
}
283+
284+
function isUnsafeProperty(key: PropertyKey) {
285+
return key === '__proto__' || key === 'constructor' || key === 'prototype';
286+
}
287+
265288
export function isEventListenerObject(x: unknown): x is EventListenerObject {
266289
return isObject(x) && 'handleEvent' in x;
267290
}
@@ -501,5 +524,60 @@ export function setStyles(
501524
element: HTMLElement,
502525
styles: Partial<CSSStyleDeclaration>
503526
): void {
504-
Object.assign(element.style, styles);
527+
merge(element.style, styles);
528+
}
529+
530+
/**
531+
* Merges the properties of `source` into `target` performing a recursive deep merge over POJOs and arrays.
532+
*
533+
* @remarks
534+
* This function mutates the `target` object.
535+
* If that is not the desired outcome, see {@link toMerged} for another approach.
536+
*/
537+
export function merge<
538+
T extends Record<PropertyKey, any>,
539+
S extends Record<PropertyKey, any>,
540+
>(target: T, source: S): T & S {
541+
const sourceKeys = Object.keys(source) as Array<keyof S>;
542+
const length = sourceKeys.length;
543+
544+
for (let i = 0; i < length; i++) {
545+
const key = sourceKeys[i];
546+
547+
if (isUnsafeProperty(key)) {
548+
continue;
549+
}
550+
551+
const sourceValue = source[key];
552+
const targetValue = target[key];
553+
554+
if (Array.isArray(sourceValue)) {
555+
if (Array.isArray(targetValue)) {
556+
target[key] = merge(targetValue, sourceValue);
557+
} else {
558+
target[key] = merge([], sourceValue);
559+
}
560+
} else if (isPlainObject(sourceValue)) {
561+
if (isPlainObject(targetValue)) {
562+
target[key] = merge(targetValue, sourceValue);
563+
} else {
564+
target[key] = merge({}, sourceValue);
565+
}
566+
} else if (targetValue === undefined || sourceValue !== undefined) {
567+
target[key] = sourceValue;
568+
}
569+
}
570+
571+
return target;
572+
}
573+
574+
/**
575+
* Just like {@link merge} but it does not mutate the `target` object instead
576+
* mutating a structured clone of it.
577+
*/
578+
export function toMerged<
579+
T extends Record<PropertyKey, any>,
580+
S extends Record<PropertyKey, any>,
581+
>(target: T, source: S): T & S {
582+
return merge(structuredClone(target), source);
505583
}

src/components/date-picker/date-picker.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,6 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin(
461461

462462
addKeybindings(this, {
463463
skip: () => this.disabled || this.readOnly,
464-
bindingDefaults: { preventDefault: true },
465464
})
466465
.set([altKey, arrowDown], this.handleAnchorClick)
467466
.set([altKey, arrowUp], this._onEscapeKey)

src/components/date-range-picker/date-range-picker.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM
579579

580580
addKeybindings(this, {
581581
skip: () => this.disabled || this.readOnly,
582-
bindingDefaults: { preventDefault: true },
583582
})
584583
.set([altKey, arrowDown], this.handleAnchorClick)
585584
.set([altKey, arrowUp], this._onEscapeKey)

src/components/date-time-input/date-time-input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
281281

282282
addKeybindings(this, {
283283
skip: () => this.readOnly,
284-
bindingDefaults: { preventDefault: true, triggers: ['keydownRepeat'] },
284+
bindingDefaults: { triggers: ['keydownRepeat'] },
285285
})
286286
.set([ctrlKey, ';'], this.setToday)
287287
.set(arrowUp, this.keyboardSpin.bind(this, 'up'))

0 commit comments

Comments
 (0)