Skip to content

Commit a0412f8

Browse files
authored
feat(tags): apply KbqFieldSizingContent directive to KbqTagInput (#DS-4204) (#1066)
1 parent b6fd734 commit a0412f8

11 files changed

Lines changed: 95 additions & 68 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ChangeDetectionStrategy, Component, Type } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { By } from '@angular/platform-browser';
4+
import { KbqFieldSizingContent } from './field-sizing-content';
5+
6+
const createComponent = <T>(component: Type<T>): ComponentFixture<T> => {
7+
TestBed.configureTestingModule({ imports: [component] }).compileComponents();
8+
const fixture = TestBed.createComponent<T>(component);
9+
10+
fixture.autoDetectChanges();
11+
12+
return fixture;
13+
};
14+
15+
const getInputNativeElement = ({ debugElement }: ComponentFixture<unknown>): HTMLInputElement => {
16+
return debugElement.query(By.directive(KbqFieldSizingContent)).nativeElement;
17+
};
18+
19+
@Component({
20+
selector: 'test-field-sizing-content',
21+
standalone: true,
22+
imports: [KbqFieldSizingContent],
23+
template: `
24+
<input kbqFieldSizingContent />
25+
`,
26+
changeDetection: ChangeDetectionStrategy.OnPush
27+
})
28+
export class TestFieldSizingContent {}
29+
30+
describe(KbqFieldSizingContent.name, () => {
31+
it('should apply kbq-field-sizing-content class', () => {
32+
const fixture = createComponent(TestFieldSizingContent);
33+
const input = getInputNativeElement(fixture);
34+
35+
expect(input.classList).toContain('kbq-field-sizing-content');
36+
});
37+
38+
it('should use native field-sizing when browser supports it', () => {
39+
(CSS.supports as jest.Mock).mockReturnValue(true);
40+
41+
const fixture = createComponent(TestFieldSizingContent);
42+
const input = getInputNativeElement(fixture);
43+
44+
expect(input.style['fieldSizing']).toBe('content');
45+
});
46+
});

packages/components/core/form-field/field-sizing-content.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const BOX_SIZING_BORDER_BOX_WIDTH_PROPERTIES = [
5151
'borderRightWidth'
5252
] as const satisfies Array<keyof CSSStyleDeclaration>;
5353

54-
const FIELD_RESIZE_EVENTS = ['input', 'change', 'focus'] as const;
54+
const FIELD_RESIZE_EVENTS = ['input', 'change', 'focus', 'blur'] as const;
5555

5656
/**
5757
* Emulates [`field-sizing: content`](https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing) CSS property for
@@ -97,6 +97,8 @@ export class KbqFieldSizingContent {
9797
const ruler = this.createRuler(computedStyle);
9898

9999
ruler.textContent = this.element.value || this.element.placeholder || '';
100+
// We should add space to prevent text truncation in Safari/Firefox
101+
if (ruler.textContent) ruler.textContent += ' ';
100102

101103
this.renderer.appendChild(this.document.body, ruler);
102104

packages/components/tags/tag-input.ts

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
import { coerceBooleanProperty } from '@angular/cdk/coercion';
2-
import {
3-
Directive,
4-
ElementRef,
5-
EventEmitter,
6-
Inject,
7-
Input,
8-
OnChanges,
9-
Optional,
10-
Output,
11-
Renderer2,
12-
Self
13-
} from '@angular/core';
2+
import { Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Optional, Output, Self } from '@angular/core';
143
import { NgControl } from '@angular/forms';
154
import { COMMA, ENTER, SEMICOLON, SPACE, TAB } from '@koobiq/cdk/keycodes';
165
import { KbqAutocompleteTrigger } from '@koobiq/components/autocomplete';
17-
import { isBoolean } from '@koobiq/components/core';
6+
import { isBoolean, KbqFieldSizingContent } from '@koobiq/components/core';
187
import { KbqTrim } from '@koobiq/components/form-field';
198
import { KBQ_TAGS_DEFAULT_OPTIONS, KbqTagsDefaultOptions } from './tag-default-options';
209
import { KbqTagList } from './tag-list.component';
@@ -63,7 +52,8 @@ let nextUniqueId = 0;
6352
'(focus)': 'onFocus()',
6453
'(input)': 'onInput()',
6554
'(paste)': 'onPaste($event)'
66-
}
55+
},
56+
hostDirectives: [KbqFieldSizingContent]
6757
})
6858
export class KbqTagInput implements KbqTagTextControl, OnChanges {
6959
/** Whether the control is focused. */
@@ -160,25 +150,25 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
160150
return !this.inputElement.value;
161151
}
162152

153+
/**
154+
* @docs-private
155+
*
156+
* @deprecated Unused. Will be removed in next major release.
157+
*/
163158
countOfSymbolsForUpdateWidth: number = 3;
164159

165-
private oneSymbolWidth: number;
166-
167160
/** The native input element to which this directive is attached. */
168161
private inputElement: HTMLInputElement;
169162

170163
constructor(
171164
private elementRef: ElementRef<HTMLInputElement>,
172-
private renderer: Renderer2,
173165
@Inject(KBQ_TAGS_DEFAULT_OPTIONS) private defaultOptions: KbqTagsDefaultOptions,
174166
@Optional() @Self() private trimDirective: KbqTrim,
175167
@Optional() @Self() public ngControl: NgControl,
176168
@Optional() @Self() public autocompleteTrigger?: KbqAutocompleteTrigger
177169
) {
178170
this.inputElement = this.elementRef.nativeElement as HTMLInputElement;
179171

180-
this.setDefaultInputWidth();
181-
182172
this._separators = this.defaultOptions.separators || KbqTagInputDefaultSeparators;
183173
this._addOnPaste = isBoolean(this.defaultOptions.addOnPaste) ? this.defaultOptions.addOnPaste : true;
184174
}
@@ -231,7 +221,6 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
231221
if (this.distinct && this.hasDuplicates) return;
232222

233223
this.tagEnd.emit({ input: this.inputElement, value: this.trimValue(this.inputElement.value) });
234-
this.updateInputWidth();
235224
}
236225
}
237226

@@ -242,7 +231,6 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
242231
}
243232

244233
onInput() {
245-
this.updateInputWidth();
246234
// Let tag list know whenever the value changes.
247235
this._tagList.stateChanges.next();
248236
}
@@ -277,25 +265,16 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
277265
.filter((item) => !tagValues.includes(item))
278266
.forEach((item) => this.tagEnd.emit({ input: this.inputElement, value: item }));
279267

280-
this.updateInputWidth();
281-
282268
$event.preventDefault();
283269
$event.stopPropagation();
284270
}
285271

286-
updateInputWidth(): void {
287-
const length = this.inputElement.value.length;
288-
289-
this.renderer.setStyle(this.inputElement, 'max-width', 0);
290-
this.oneSymbolWidth = this.inputElement.scrollWidth / length;
291-
this.renderer.setStyle(this.inputElement, 'max-width', '');
292-
293-
if (length > this.countOfSymbolsForUpdateWidth) {
294-
this.renderer.setStyle(this.inputElement, 'width', `${length * this.oneSymbolWidth}px`);
295-
} else {
296-
this.setDefaultInputWidth();
297-
}
298-
}
272+
/**
273+
* @docs-private
274+
*
275+
* @deprecated Unused. Will be removed in next major release.
276+
*/
277+
updateInputWidth(): void {}
299278

300279
/** @docs-private */
301280
onFocus(): void {
@@ -333,10 +312,6 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
333312
return !!this.ngControl;
334313
}
335314

336-
private setDefaultInputWidth() {
337-
this.renderer.setStyle(this.inputElement, 'width', '30px');
338-
}
339-
340315
/** Checks whether a keycode is one of the configured separators. */
341316
private isSeparatorKey(event: KeyboardEvent) {
342317
return this.separators.some((separator) => separator.key === event.key && !event.shiftKey);

packages/components/tags/tag-list.scss

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@use '../core/styles/common/tokens' as *;
2+
@use '../core/styles/common/input';
23

34
.kbq-tag-list {
45
display: flex;
@@ -17,22 +18,24 @@
1718
}
1819

1920
.kbq-tag-input {
20-
border: none;
21-
outline: none;
22-
background: transparent;
23-
24-
text-overflow: ellipsis;
25-
26-
min-height: unset;
21+
@include input.kbq-reset-input();
2722

2823
padding-top: var(--kbq-tag-size-padding-vertical);
2924
padding-bottom: var(--kbq-tag-size-padding-vertical);
30-
padding-left: 4px;
25+
padding-left: var(--kbq-size-xxs);
3126
padding-right: unset;
3227

28+
min-width: var(--kbq-size-3xl);
29+
max-width: 100%;
30+
flex-grow: 1;
31+
3332
&.kbq-input {
34-
padding: unset;
35-
min-height: unset;
33+
--kbq-input-size-padding-vertical: 0;
34+
--kbq-form-field-size-border-radius: 0;
35+
36+
padding-left: var(--kbq-size-xxs);
37+
min-height: unset !important;
38+
width: auto;
3639
}
3740
}
3841

@@ -48,10 +51,6 @@
4851
gap: var(--kbq-tag-list-size-content-gap);
4952

5053
.kbq-tag-input {
51-
max-width: 100%;
52-
53-
flex: 1 1 auto;
54-
5554
// indicating tag list is empty
5655
&:first-child {
5756
padding-left: var(--kbq-size-s);

packages/docs-examples/components/tag/tag-autocomplete-option-operations/tag-autocomplete-option-operations-example.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { AsyncPipe } from '@angular/common';
22
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, ViewChild } from '@angular/core';
33
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
44
import { KbqAutocomplete, KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
5+
import { kbqDisableLegacyValidationDirectiveProvider } from '@koobiq/components/core';
56
import { KbqFormFieldModule } from '@koobiq/components/form-field';
67
import { KbqIconModule } from '@koobiq/components/icon';
8+
import { KbqInputModule } from '@koobiq/components/input';
79
import { KbqTag, KbqTagInput, KbqTagInputEvent, KbqTagList, KbqTagsModule } from '@koobiq/components/tags';
810
import { Observable, merge } from 'rxjs';
911
import { map } from 'rxjs/operators';
@@ -24,8 +26,10 @@ const autocompleteValueCoercion = (value): string => (value?.new ? value.value :
2426
ReactiveFormsModule,
2527
KbqAutocompleteModule,
2628
KbqIconModule,
27-
AsyncPipe
29+
AsyncPipe,
30+
KbqInputModule
2831
],
32+
providers: [kbqDisableLegacyValidationDirectiveProvider()],
2933
template: `
3034
<kbq-form-field>
3135
<kbq-tag-list #tagList="kbqTagList">
@@ -38,7 +42,8 @@ const autocompleteValueCoercion = (value): string => (value?.new ? value.value :
3842
<input
3943
#tagInput
4044
autocomplete="off"
41-
placeholder="Placeholder"
45+
placeholder="New tag"
46+
kbqInput
4247
[distinct]="true"
4348
[formControl]="control"
4449
[kbqAutocomplete]="autocomplete"

packages/docs-examples/components/tag/tag-input-draggable/tag-input-draggable-example.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, model, viewChild } from
33
import { kbqDisableLegacyValidationDirectiveProvider } from '@koobiq/components/core';
44
import { KbqFormFieldModule } from '@koobiq/components/form-field';
55
import { KbqIconModule } from '@koobiq/components/icon';
6+
import { KbqInputModule } from '@koobiq/components/input';
67
import {
78
KbqTagEvent,
89
KbqTagInput,
@@ -19,7 +20,7 @@ const getTags = () => Array.from({ length: 3 }, (_, id) => ({ id, value: `Dragga
1920
@Component({
2021
standalone: true,
2122
selector: 'tag-input-draggable-example',
22-
imports: [KbqTagsModule, KbqFormFieldModule, KbqIconModule],
23+
imports: [KbqTagsModule, KbqFormFieldModule, KbqIconModule, KbqInputModule],
2324
providers: [kbqDisableLegacyValidationDirectiveProvider()],
2425
template: `
2526
<kbq-form-field>

packages/docs-examples/components/tag/tag-input-with-form-control-validators/tag-input-with-form-control-validators-example.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const customMaxLengthValidator = (max: number): ValidatorFn => {
4848
<input
4949
autocomplete="off"
5050
kbqInput
51-
placeholder="New keyword..."
51+
placeholder="New tag"
5252
[kbqTagInputFor]="tagList"
5353
(kbqTagInputTokenEnd)="createTag($event)"
5454
/>

packages/docs-examples/components/tag/tags-autocomplete-onpaste-off/tags-autocomplete-onpaste-off-example.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<input
1212
#tagInput
1313
autocomplete="off"
14-
placeholder="New tag..."
14+
placeholder="New tag"
1515
[distinct]="true"
1616
[formControl]="control"
1717
[kbqAutocomplete]="autocomplete"

packages/docs-examples/components/tag/tags-input-onpaste-off/tags-input-onpaste-off-example.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<!-- turn off tag add on paste with Input property -->
1111
<input
1212
autocomplete="off"
13-
placeholder="New tag..."
13+
placeholder="New tag"
1414
[formControl]="control"
1515
[kbqTagInputAddOnPaste]="false"
1616
[kbqTagInputFor]="tagList"

tools/jest/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,5 @@ global.DragEvent = class extends MouseEvent {
4545
} as typeof DragEvent;
4646

4747
global.CSS = {
48-
supports: (..._args: any[]) => false
48+
supports: jest.fn().mockReturnValue(false) as typeof CSS.supports
4949
} as typeof CSS;

0 commit comments

Comments
 (0)