Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ChangeDetectionStrategy, Component, Type } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { KbqFieldSizingContent } from './field-sizing-content';

const createComponent = <T>(component: Type<T>): ComponentFixture<T> => {
TestBed.configureTestingModule({ imports: [component] }).compileComponents();
const fixture = TestBed.createComponent<T>(component);

fixture.autoDetectChanges();

return fixture;
};

const getInputNativeElement = ({ debugElement }: ComponentFixture<unknown>): HTMLInputElement => {
return debugElement.query(By.directive(KbqFieldSizingContent)).nativeElement;
};

@Component({
selector: 'test-field-sizing-content',
standalone: true,
imports: [KbqFieldSizingContent],
template: `
<input kbqFieldSizingContent />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestFieldSizingContent {}

describe(KbqFieldSizingContent.name, () => {
it('should apply kbq-field-sizing-content class', () => {
const fixture = createComponent(TestFieldSizingContent);
const input = getInputNativeElement(fixture);

expect(input.classList).toContain('kbq-field-sizing-content');
});

it('should use native field-sizing when browser supports it', () => {
(CSS.supports as jest.Mock).mockReturnValue(true);

const fixture = createComponent(TestFieldSizingContent);
const input = getInputNativeElement(fixture);

expect(input.style['fieldSizing']).toBe('content');
Comment thread
artembelik marked this conversation as resolved.
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const BOX_SIZING_BORDER_BOX_WIDTH_PROPERTIES = [
'borderRightWidth'
] as const satisfies Array<keyof CSSStyleDeclaration>;

const FIELD_RESIZE_EVENTS = ['input', 'change', 'focus'] as const;
const FIELD_RESIZE_EVENTS = ['input', 'change', 'focus', 'blur'] as const;

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

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

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

Expand Down
55 changes: 15 additions & 40 deletions packages/components/tags/tag-input.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
Directive,
ElementRef,
EventEmitter,
Inject,
Input,
OnChanges,
Optional,
Output,
Renderer2,
Self
} from '@angular/core';
import { Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Optional, Output, Self } from '@angular/core';
import { NgControl } from '@angular/forms';
import { COMMA, ENTER, SEMICOLON, SPACE, TAB } from '@koobiq/cdk/keycodes';
import { KbqAutocompleteTrigger } from '@koobiq/components/autocomplete';
import { isBoolean } from '@koobiq/components/core';
import { isBoolean, KbqFieldSizingContent } from '@koobiq/components/core';
import { KbqTrim } from '@koobiq/components/form-field';
import { KBQ_TAGS_DEFAULT_OPTIONS, KbqTagsDefaultOptions } from './tag-default-options';
import { KbqTagList } from './tag-list.component';
Expand Down Expand Up @@ -63,7 +52,8 @@ let nextUniqueId = 0;
'(focus)': 'onFocus()',
'(input)': 'onInput()',
'(paste)': 'onPaste($event)'
}
},
hostDirectives: [KbqFieldSizingContent]
})
export class KbqTagInput implements KbqTagTextControl, OnChanges {
/** Whether the control is focused. */
Expand Down Expand Up @@ -160,25 +150,25 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
return !this.inputElement.value;
}

/**
* @docs-private
*
* @deprecated Unused. Will be removed in next major release.
*/
countOfSymbolsForUpdateWidth: number = 3;

private oneSymbolWidth: number;

/** The native input element to which this directive is attached. */
private inputElement: HTMLInputElement;

constructor(
private elementRef: ElementRef<HTMLInputElement>,
private renderer: Renderer2,
@Inject(KBQ_TAGS_DEFAULT_OPTIONS) private defaultOptions: KbqTagsDefaultOptions,
@Optional() @Self() private trimDirective: KbqTrim,
@Optional() @Self() public ngControl: NgControl,
@Optional() @Self() public autocompleteTrigger?: KbqAutocompleteTrigger
) {
this.inputElement = this.elementRef.nativeElement as HTMLInputElement;

this.setDefaultInputWidth();

this._separators = this.defaultOptions.separators || KbqTagInputDefaultSeparators;
this._addOnPaste = isBoolean(this.defaultOptions.addOnPaste) ? this.defaultOptions.addOnPaste : true;
}
Expand Down Expand Up @@ -231,7 +221,6 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
if (this.distinct && this.hasDuplicates) return;

this.tagEnd.emit({ input: this.inputElement, value: this.trimValue(this.inputElement.value) });
this.updateInputWidth();
}
}

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

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

this.updateInputWidth();

$event.preventDefault();
$event.stopPropagation();
}

updateInputWidth(): void {
const length = this.inputElement.value.length;

this.renderer.setStyle(this.inputElement, 'max-width', 0);
this.oneSymbolWidth = this.inputElement.scrollWidth / length;
this.renderer.setStyle(this.inputElement, 'max-width', '');

if (length > this.countOfSymbolsForUpdateWidth) {
this.renderer.setStyle(this.inputElement, 'width', `${length * this.oneSymbolWidth}px`);
} else {
this.setDefaultInputWidth();
}
}
/**
* @docs-private
*
* @deprecated Unused. Will be removed in next major release.
*/
updateInputWidth(): void {}

/** @docs-private */
onFocus(): void {
Expand Down Expand Up @@ -333,10 +312,6 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
return !!this.ngControl;
}

private setDefaultInputWidth() {
this.renderer.setStyle(this.inputElement, 'width', '30px');
}

/** Checks whether a keycode is one of the configured separators. */
private isSeparatorKey(event: KeyboardEvent) {
return this.separators.some((separator) => separator.key === event.key && !event.shiftKey);
Expand Down
27 changes: 13 additions & 14 deletions packages/components/tags/tag-list.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use '../core/styles/common/tokens' as *;
@use '../core/styles/common/input';

.kbq-tag-list {
display: flex;
Expand All @@ -17,22 +18,24 @@
}

.kbq-tag-input {
border: none;
outline: none;
background: transparent;

text-overflow: ellipsis;

min-height: unset;
@include input.kbq-reset-input();

padding-top: var(--kbq-tag-size-padding-vertical);
padding-bottom: var(--kbq-tag-size-padding-vertical);
padding-left: 4px;
padding-left: var(--kbq-size-xxs);
padding-right: unset;

min-width: var(--kbq-size-3xl);
max-width: 100%;
flex-grow: 1;

&.kbq-input {
padding: unset;
min-height: unset;
--kbq-input-size-padding-vertical: 0;
--kbq-form-field-size-border-radius: 0;

padding-left: var(--kbq-size-xxs);
min-height: unset !important;
Comment thread
artembelik marked this conversation as resolved.
width: auto;
}
}

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

.kbq-tag-input {
max-width: 100%;

flex: 1 1 auto;

// indicating tag list is empty
&:first-child {
padding-left: var(--kbq-size-s);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { AsyncPipe } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, ViewChild } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { KbqAutocomplete, KbqAutocompleteModule, KbqAutocompleteSelectedEvent } from '@koobiq/components/autocomplete';
import { kbqDisableLegacyValidationDirectiveProvider } from '@koobiq/components/core';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqTag, KbqTagInput, KbqTagInputEvent, KbqTagList, KbqTagsModule } from '@koobiq/components/tags';
import { Observable, merge } from 'rxjs';
import { map } from 'rxjs/operators';
Expand All @@ -24,8 +26,10 @@ const autocompleteValueCoercion = (value): string => (value?.new ? value.value :
ReactiveFormsModule,
KbqAutocompleteModule,
KbqIconModule,
AsyncPipe
AsyncPipe,
KbqInputModule
],
providers: [kbqDisableLegacyValidationDirectiveProvider()],
template: `
<kbq-form-field>
<kbq-tag-list #tagList="kbqTagList">
Expand All @@ -38,7 +42,8 @@ const autocompleteValueCoercion = (value): string => (value?.new ? value.value :
<input
#tagInput
autocomplete="off"
placeholder="Placeholder"
placeholder="New tag"
kbqInput
[distinct]="true"
[formControl]="control"
[kbqAutocomplete]="autocomplete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, model, viewChild } from
import { kbqDisableLegacyValidationDirectiveProvider } from '@koobiq/components/core';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
KbqTagEvent,
KbqTagInput,
Expand All @@ -19,7 +20,7 @@ const getTags = () => Array.from({ length: 3 }, (_, id) => ({ id, value: `Dragga
@Component({
standalone: true,
selector: 'tag-input-draggable-example',
imports: [KbqTagsModule, KbqFormFieldModule, KbqIconModule],
imports: [KbqTagsModule, KbqFormFieldModule, KbqIconModule, KbqInputModule],
providers: [kbqDisableLegacyValidationDirectiveProvider()],
template: `
<kbq-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const customMaxLengthValidator = (max: number): ValidatorFn => {
<input
autocomplete="off"
kbqInput
placeholder="New keyword..."
placeholder="New tag"
[kbqTagInputFor]="tagList"
(kbqTagInputTokenEnd)="createTag($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<input
#tagInput
autocomplete="off"
placeholder="New tag..."
placeholder="New tag"
[distinct]="true"
[formControl]="control"
[kbqAutocomplete]="autocomplete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<!-- turn off tag add on paste with Input property -->
<input
autocomplete="off"
placeholder="New tag..."
placeholder="New tag"
[formControl]="control"
[kbqTagInputAddOnPaste]="false"
[kbqTagInputFor]="tagList"
Expand Down
2 changes: 1 addition & 1 deletion tools/jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ global.DragEvent = class extends MouseEvent {
} as typeof DragEvent;

global.CSS = {
supports: (..._args: any[]) => false
supports: jest.fn().mockReturnValue(false) as typeof CSS.supports
} as typeof CSS;
11 changes: 5 additions & 6 deletions tools/public_api_guard/components/tags.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { QueryList } from '@angular/core';
import { Renderer2 } from '@angular/core';
import { SelectionModel } from '@angular/cdk/collections';
import { Subject } from 'rxjs';
import { WritableSignal } from '@angular/core';
Expand Down Expand Up @@ -177,15 +176,15 @@ export interface KbqTagEvent {
//
// @public
export class KbqTagInput implements KbqTagTextControl, OnChanges {
constructor(elementRef: ElementRef<HTMLInputElement>, renderer: Renderer2, defaultOptions: KbqTagsDefaultOptions, trimDirective: KbqTrim, ngControl: NgControl, autocompleteTrigger?: KbqAutocompleteTrigger | undefined);
constructor(elementRef: ElementRef<HTMLInputElement>, defaultOptions: KbqTagsDefaultOptions, trimDirective: KbqTrim, ngControl: NgControl, autocompleteTrigger?: KbqAutocompleteTrigger | undefined);
get addOnBlur(): boolean;
set addOnBlur(value: boolean);
get addOnPaste(): boolean;
set addOnPaste(value: boolean);
// (undocumented)
autocompleteTrigger?: KbqAutocompleteTrigger | undefined;
blur(event: FocusEvent): void;
// (undocumented)
// @deprecated
countOfSymbolsForUpdateWidth: number;
get disabled(): boolean;
set disabled(value: boolean);
Expand Down Expand Up @@ -216,12 +215,12 @@ export class KbqTagInput implements KbqTagTextControl, OnChanges {
set tagList(value: KbqTagList);
// (undocumented)
triggerValidation(): void;
// (undocumented)
// @deprecated
updateInputWidth(): void;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<KbqTagInput, "input[kbqTagInputFor]", ["kbqTagInput", "kbqTagInputFor"], { "separatorKeyCodes": { "alias": "kbqTagInputSeparatorKeyCodes"; "required": false; }; "distinct": { "alias": "distinct"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "id": { "alias": "id"; "required": false; }; "tagList": { "alias": "kbqTagInputFor"; "required": false; }; "addOnBlur": { "alias": "kbqTagInputAddOnBlur"; "required": false; }; "addOnPaste": { "alias": "kbqTagInputAddOnPaste"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "tagEnd": "kbqTagInputTokenEnd"; }, never, never, false, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<KbqTagInput, "input[kbqTagInputFor]", ["kbqTagInput", "kbqTagInputFor"], { "separatorKeyCodes": { "alias": "kbqTagInputSeparatorKeyCodes"; "required": false; }; "distinct": { "alias": "distinct"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "id": { "alias": "id"; "required": false; }; "tagList": { "alias": "kbqTagInputFor"; "required": false; }; "addOnBlur": { "alias": "kbqTagInputAddOnBlur"; "required": false; }; "addOnPaste": { "alias": "kbqTagInputAddOnPaste"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "tagEnd": "kbqTagInputTokenEnd"; }, never, never, false, [{ directive: typeof i1.KbqFieldSizingContent; inputs: {}; outputs: {}; }]>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<KbqTagInput, [null, null, null, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }]>;
static ɵfac: i0.ɵɵFactoryDeclaration<KbqTagInput, [null, null, { optional: true; self: true; }, { optional: true; self: true; }, { optional: true; self: true; }]>;
}

// @public
Expand Down