Skip to content

Commit ed36f79

Browse files
authored
refactor: Input-based inheritance chain for several components (#2058)
1 parent 7ec4d2d commit ed36f79

12 files changed

Lines changed: 383 additions & 262 deletions

File tree

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

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { html } from 'lit';
33
import { eventOptions, property } from 'lit/decorators.js';
44
import { ifDefined } from 'lit/directives/if-defined.js';
55
import { live } from 'lit/directives/live.js';
6+
import { addThemingController } from '../../theming/theming-controller.js';
67
import { convertToDate } from '../calendar/helpers.js';
78
import {
89
addKeybindings,
@@ -12,6 +13,7 @@ import {
1213
arrowUp,
1314
ctrlKey,
1415
} from '../common/controllers/key-bindings.js';
16+
import { addSlotController, setSlots } from '../common/controllers/slot.js';
1517
import { watch } from '../common/decorators/watch.js';
1618
import { registerComponent } from '../common/definitions/register.js';
1719
import { addI18nController } from '../common/i18n/i18n-controller.js';
@@ -21,6 +23,9 @@ import { FormValueDateTimeTransformers } from '../common/mixins/forms/form-trans
2123
import { createFormValueState } from '../common/mixins/forms/form-value.js';
2224
import { partMap } from '../common/part-map.js';
2325
import type { IgcInputComponentEventMap } from '../input/input-base.js';
26+
import { styles } from '../input/themes/input.base.css.js';
27+
import { styles as shared } from '../input/themes/shared/input.common.css.js';
28+
import { all } from '../input/themes/themes.js';
2429
import {
2530
IgcMaskInputBaseComponent,
2631
type MaskSelection,
@@ -42,6 +47,17 @@ export interface IgcDateTimeInputComponentEventMap extends Omit<
4247
igcChange: CustomEvent<Date | null>;
4348
}
4449

50+
const Slots = setSlots(
51+
'prefix',
52+
'suffix',
53+
'helper-text',
54+
'value-missing',
55+
'range-overflow',
56+
'range-underflow',
57+
'custom-error',
58+
'invalid'
59+
);
60+
4561
/**
4662
* A date time input is an input field that lets you set and edit the date and time in a chosen input element
4763
* using customizable display and input formats.
@@ -72,9 +88,10 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
7288
AbstractConstructor<IgcMaskInputBaseComponent>
7389
>(IgcMaskInputBaseComponent) {
7490
public static readonly tagName = 'igc-date-time-input';
91+
public static styles = [styles, shared];
7592

7693
/* blazorSuppress */
77-
public static register() {
94+
public static register(): void {
7895
registerComponent(
7996
IgcDateTimeInputComponent,
8097
IgcValidationContainerComponent
@@ -85,6 +102,12 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
85102
return dateTimeInputValidators;
86103
}
87104

105+
protected override readonly _themes = addThemingController(this, all);
106+
107+
protected override readonly _slots = addSlotController(this, {
108+
slots: Slots,
109+
});
110+
88111
protected override readonly _formValue = createFormValueState(this, {
89112
initialValue: null,
90113
transformers: FormValueDateTimeTransformers,
@@ -216,7 +239,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
216239
this._i18nController.locale = value;
217240
}
218241

219-
public get locale() {
242+
public get locale(): string {
220243
return this._i18nController.locale;
221244
}
222245

@@ -331,7 +354,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
331354
const { start, end } = this._inputSelection;
332355
const newValue = this.trySpinValue(targetPart, delta);
333356
this.value = newValue;
334-
this.updateComplete.then(() => this.input.setSelectionRange(start, end));
357+
this.updateComplete.then(() => this._input?.setSelectionRange(start, end));
335358
}
336359

337360
/** Decrements a date/time portion. */
@@ -345,7 +368,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
345368
const { start, end } = this._inputSelection;
346369
const newValue = this.trySpinValue(targetPart, delta, true);
347370
this.value = newValue;
348-
this.updateComplete.then(() => this.input.setSelectionRange(start, end));
371+
this.updateComplete.then(() => this._input?.setSelectionRange(start, end));
349372
}
350373

351374
/** Clears the input element of user input. */
@@ -409,7 +432,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
409432
}
410433

411434
await this.updateComplete;
412-
this.input.setSelectionRange(result.end, result.end);
435+
this._input?.setSelectionRange(result.end, result.end);
413436
}
414437

415438
private trySpinValue(
@@ -640,7 +663,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
640663
}
641664

642665
protected navigateParts(delta: number) {
643-
const position = this.getNewPosition(this.input.value, delta);
666+
const position = this.getNewPosition(this._input?.value ?? '', delta);
644667
this.setSelectionRange(position, position);
645668
}
646669

@@ -651,11 +674,11 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin<
651674
this.setSelectionRange(this._maskSelection.start, this._maskSelection.end);
652675
}
653676

654-
protected override renderInput() {
677+
protected override _renderInput() {
655678
return html`
656679
<input
657680
type="text"
658-
part=${partMap(this.resolvePartNames('input'))}
681+
part=${partMap(this._resolvePartNames('input'))}
659682
name=${ifDefined(this.name)}
660683
.value=${live(this._maskedValue)}
661684
.placeholder=${this.placeholder || this._parser.emptyMask}

src/components/file-input/file-input.ts

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ import { html } from 'lit';
66
import { property, state } from 'lit/decorators.js';
77
import { addThemingController } from '../../theming/theming-controller.js';
88
import IgcButtonComponent from '../button/button.js';
9+
import { addSlotController, setSlots } from '../common/controllers/slot.js';
910
import { registerComponent } from '../common/definitions/register.js';
1011
import { addI18nController } from '../common/i18n/i18n-controller.js';
1112
import type { AbstractConstructor } from '../common/mixins/constructor.js';
1213
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
1314
import { FormValueFileListTransformers } from '../common/mixins/forms/form-transformers.js';
1415
import { createFormValueState } from '../common/mixins/forms/form-value.js';
1516
import { partMap } from '../common/part-map.js';
16-
import { bindIf, hasFiles, isEmpty } from '../common/util.js';
17+
import { bindIf, hasFiles } from '../common/util.js';
1718
import {
1819
IgcInputBaseComponent,
1920
type IgcInputComponentEventMap,
2021
} from '../input/input-base.js';
22+
import { styles as baseStyle } from '../input/themes/input.base.css.js';
23+
import { styles as shared } from '../input/themes/shared/input.common.css.js';
2124
import IgcValidationContainerComponent from '../validation-container/validation-container.js';
2225
import { styles } from './themes/file-input.base.css.js';
2326
import { all } from './themes/themes.js';
@@ -31,6 +34,17 @@ export interface IgcFileInputComponentEventMap extends Omit<
3134
igcChange: CustomEvent<FileList>;
3235
}
3336

37+
const Slots = setSlots(
38+
'prefix',
39+
'suffix',
40+
'helper-text',
41+
'file-selector-text',
42+
'file-missing-text',
43+
'value-missing',
44+
'custom-error',
45+
'invalid'
46+
);
47+
3448
/* blazorSuppress */
3549
/**
3650
* @element igc-file-input
@@ -61,7 +75,7 @@ export default class IgcFileInputComponent extends EventEmitterMixin<
6175
AbstractConstructor<IgcInputBaseComponent>
6276
>(IgcInputBaseComponent) {
6377
public static readonly tagName = 'igc-file-input';
64-
public static styles = [...IgcInputBaseComponent.styles, styles];
78+
public static styles = [baseStyle, shared, styles];
6579

6680
/* blazorSuppress */
6781
public static register(): void {
@@ -72,45 +86,63 @@ export default class IgcFileInputComponent extends EventEmitterMixin<
7286
);
7387
}
7488

75-
protected readonly _i18nController =
76-
addI18nController<IFileInputResourceStrings>(this, {
77-
defaultEN: FileInputResourceStringsEN,
78-
});
89+
//#region Internal attributes and properties
7990

80-
protected override get __validators() {
81-
return fileValidators;
82-
}
91+
protected override readonly _themes = addThemingController(this, all);
92+
93+
protected override readonly _slots = addSlotController(this, {
94+
slots: Slots,
95+
});
8396

8497
protected override readonly _formValue = createFormValueState(this, {
8598
initialValue: null,
8699
transformers: FormValueFileListTransformers,
87100
});
88101

89-
@state()
90-
private _hasActivation = false;
102+
protected readonly _i18nController = addI18nController(this, {
103+
defaultEN: FileInputResourceStringsEN,
104+
});
105+
106+
protected override get __validators() {
107+
return fileValidators;
108+
}
91109

92110
private get _fileNames(): string | null {
93-
return hasFiles(this)
94-
? Array.from(this.files!)
95-
.map((file) => file.name)
96-
.join(', ')
97-
: null;
111+
if (!hasFiles(this)) {
112+
return null;
113+
}
114+
115+
return Array.from(this.files)
116+
.map((file) => file.name || 'unnamed')
117+
.join(', ');
98118
}
99119

120+
/**
121+
* Indicates whether the file picker dialog is currently active.
122+
* Used to manage validation on blur.
123+
*/
124+
@state()
125+
private _filePickerActive = false;
126+
127+
//#endregion
128+
129+
//#region Public attributes and properties
130+
100131
/* @tsTwoWayProperty(true, "igcChange", "detail", false) */
101132
/**
102133
* The value of the control.
134+
* Similar to native file input, this property is read-only and cannot be set programmatically.
103135
* @attr
104136
*/
105137
@property()
106138
public set value(value: string) {
107-
if (value === '' && this.input) {
108-
this.input.value = value;
139+
if (value === '' && this._input) {
140+
this._input.value = value;
109141
}
110142
}
111143

112144
public get value(): string {
113-
return this.input?.value ?? '';
145+
return this._input?.value ?? '';
114146
}
115147

116148
/**
@@ -134,24 +166,26 @@ export default class IgcFileInputComponent extends EventEmitterMixin<
134166
this._i18nController.locale = value;
135167
}
136168

137-
public get locale() {
169+
public get locale(): string {
138170
return this._i18nController.locale;
139171
}
140172

141173
/**
142174
* The multiple attribute of the control.
143175
* Used to indicate that a file input allows the user to select more than one file.
176+
*
144177
* @attr
178+
* @default false
145179
*/
146-
@property({ type: Boolean })
180+
@property({ type: Boolean, reflect: true })
147181
public multiple = false;
148182

149183
/**
150184
* The accept attribute of the control.
151185
* Defines the file types as a list of comma-separated values that the file input should accept.
152186
* @attr
153187
*/
154-
@property({ type: String })
188+
@property()
155189
public accept = '';
156190

157191
/**
@@ -161,64 +195,57 @@ export default class IgcFileInputComponent extends EventEmitterMixin<
161195
@property({ type: Boolean })
162196
public override autofocus!: boolean;
163197

164-
/** @hidden */
165-
@property({ type: Boolean, attribute: false, noAccessor: true })
166-
public override readonly readOnly = false;
167-
168-
/** Returns the selected files, if any; otherwise returns null. */
169-
public get files(): FileList | null {
170-
return this.input?.files ?? null;
198+
/** Returns the list of selected files. */
199+
public get files(): FileList {
200+
return this._input?.files ?? new DataTransfer().files;
171201
}
172202

173-
constructor() {
174-
super();
175-
addThemingController(this, all);
176-
}
203+
//#endregion
204+
205+
//#region Internal methods
177206

178207
protected override _restoreDefaultValue(): void {
179-
this.input.value = '';
208+
if (this._input) {
209+
this._input.value = '';
210+
}
180211
super._restoreDefaultValue();
181212
}
182213

183-
/* c8 ignore next 2 */
184-
/** @hidden */
185-
public override setSelectionRange(): void {}
214+
//#endregion
186215

187-
/* c8 ignore next 2 */
188-
/** @hidden */
189-
public override setRangeText(): void {}
216+
//#region Event Handlers
190217

191218
private _handleChange(): void {
192-
this._hasActivation = false;
219+
this._filePickerActive = false;
193220
this._setTouchedState();
194221
this._formValue.setValueAndFormState(this.files);
195222

196223
this.requestUpdate();
197-
this.emitEvent('igcChange', { detail: this.files! });
224+
this.emitEvent('igcChange', { detail: this.files });
198225
}
199226

200227
private _handleCancel(): void {
201-
this._hasActivation = false;
228+
this._filePickerActive = false;
202229
this._setTouchedState();
203230
this._validate();
204231

205-
this.emitEvent('igcCancel', {
206-
detail: this.files!,
207-
});
232+
this.emitEvent('igcCancel', { detail: this.files });
208233
}
209234

210235
protected override _handleBlur(): void {
211-
this._hasActivation ? this._validate() : super._handleBlur();
236+
this._filePickerActive ? this._validate() : super._handleBlur();
212237
}
213238

214239
/* c8 ignore next 3 */
215240
protected _handleClick(): void {
216-
this._hasActivation = true;
241+
this._filePickerActive = true;
217242
}
218243

219-
protected override renderFileParts() {
244+
//#endregion
245+
246+
protected override _renderFileParts() {
220247
const emptyText =
221-
this.placeholder ?? this.resourceStrings.file_input_placeholder!;
248+
this.placeholder ?? this.resourceStrings.file_input_placeholder;
222249

223250
return html`
224251
<div part="file-parts">
@@ -239,14 +266,14 @@ export default class IgcFileInputComponent extends EventEmitterMixin<
239266
`;
240267
}
241268

242-
protected renderInput() {
269+
protected override _renderInput() {
243270
const hasNegativeTabIndex = this.getAttribute('tabindex') === '-1';
244-
const hasHelperText = !isEmpty(this._helperText);
271+
const hasHelperText = this._slots.hasAssignedElements('helper-text');
245272

246273
return html`
247274
<input
248-
id=${this.inputId}
249-
part=${partMap(this.resolvePartNames('input'))}
275+
id=${this._inputId}
276+
part=${partMap(this._resolvePartNames('input'))}
250277
type="file"
251278
?disabled=${this.disabled}
252279
?required=${this.required}

0 commit comments

Comments
 (0)