diff --git a/libs/cdk/forms/cva/cva.directive.ts b/libs/cdk/forms/cva/cva.directive.ts index 61a0bf1aa82..85f36d2c1fc 100644 --- a/libs/cdk/forms/cva/cva.directive.ts +++ b/libs/cdk/forms/cva/cva.directive.ts @@ -77,7 +77,7 @@ export class CvaDirective } /** - * readOnly Value to Mark component read only + * readOnly value to mark component read only */ @Input() readonly: boolean; diff --git a/libs/core/input-group/input-group.component.scss b/libs/core/input-group/input-group.component.scss index 987be295d62..7704f533521 100644 --- a/libs/core/input-group/input-group.component.scss +++ b/libs/core/input-group/input-group.component.scss @@ -385,6 +385,7 @@ fd-input-group { pointer-events: none; opacity: var(--sapContent_DisabledOpacity); } + .fd-input-group[aria-disabled='true']::-webkit-input-placeholder, .fd-input-group.is-disabled::-webkit-input-placeholder, .fd-input-group:disabled::-webkit-input-placeholder { diff --git a/libs/core/multi-input/multi-input.component.html b/libs/core/multi-input/multi-input.component.html index a366a978842..dcba7c1e081 100644 --- a/libs/core/multi-input/multi-input.component.html +++ b/libs/core/multi-input/multi-input.component.html @@ -6,6 +6,17 @@ [ngTemplateOutlet]="control" [ngTemplateOutletContext]="{ displayAddonButton: displayAddonButton }" > + } @else if (display()) { + + @for (token of viewModel.selectedOptions; track token; let last = $last) { + {{ token.label }} + } + } @else { { expect(element.style.pointerEvents).toBe('auto'); expect(element.tabIndex).toBe(0); }); + + describe('when in display mode', () => { + beforeEach(() => { + fixture.componentRef.setInput('display', true); + fixture.detectChanges(); + }); + + it('should render a tokenizer in display mode', () => { + const tokenizerElement = fixture.nativeElement.querySelector('fd-tokenizer'); + expect(tokenizerElement).toBeTruthy(); + expect(component.tokenizer.display()).toBe(true); + }); + }); }); diff --git a/libs/core/multi-input/multi-input.component.ts b/libs/core/multi-input/multi-input.component.ts index 0911677fcb6..b7cc690b2b4 100644 --- a/libs/core/multi-input/multi-input.component.ts +++ b/libs/core/multi-input/multi-input.component.ts @@ -9,6 +9,7 @@ import { forwardRef, HostListener, Injector, + input, Input, isDevMode, OnChanges, @@ -418,6 +419,9 @@ export class MultiInputComponent @ViewChild(TokenizerComponent) tokenizer: TokenizerComponent; + /** Whether the input is display-only */ + display = input(false); + /** @hidden */ get _optionItems(): _OptionItem[] { return this.optionItems$.value; diff --git a/libs/core/token/token.component.html b/libs/core/token/token.component.html index 74f71e754de..8b7557aa7d4 100644 --- a/libs/core/token/token.component.html +++ b/libs/core/token/token.component.html @@ -9,6 +9,8 @@ [attr.aria-setsize]="_totalCount" [attr.aria-posinset]="_itemPosition" [class.fd-token__disabled]="disabled" + [class.fd-token--display]="display" + [class.fd-token--isLast]="isLast" [class.fd-token--selected]="selected" [class.fd-token--readonly]="readOnly" [attr.aria-selected]="selected" @@ -16,7 +18,7 @@ - @if (!readOnly) { + @if (!readOnly && !display) { { expect(fixture.nativeElement.querySelector('.fd-token__close')).toBeFalsy(); }); + it('should not render close icon when in display-only mode', async () => { + component.display = false; + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.nativeElement.querySelector('.fd-token__close')).toBeTruthy(); + component.display = true; + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.nativeElement.querySelector('.fd-token__close')).toBeFalsy(); + }); + it('should fire onCloseClick when clicking x', () => { jest.spyOn(component.onCloseClick, 'emit'); const content = fixture.nativeElement.querySelector('.fd-token__close'); diff --git a/libs/core/token/token.component.ts b/libs/core/token/token.component.ts index 2c842d12ee1..00bc7644c64 100644 --- a/libs/core/token/token.component.ts +++ b/libs/core/token/token.component.ts @@ -44,6 +44,13 @@ export class TokenComponent implements AfterViewInit, OnDestroy { @Input() disabled = false; + /** Whether the token is display-only */ + @Input() + display = false; + + @Input() + isLast = false; + /** @hidden */ @ViewChild('tokenWrapperElement') tokenWrapperElement: ElementRef; diff --git a/libs/core/token/tokenizer.component.html b/libs/core/token/tokenizer.component.html index 757f0d4e24b..a7f5804b9e3 100644 --- a/libs/core/token/tokenizer.component.html +++ b/libs/core/token/tokenizer.component.html @@ -1,25 +1,41 @@ -
+
- @if (showOverflowPopover && (_contentDensityObserver.isCompactSignal() || compactCollapse)) { + @if ( + showOverflowPopover && + _hiddenTokens.length > 0 && + (_contentDensityObserver.isCompactSignal() || compactCollapse || display()) + ) { } @else { @if (_showMoreElement && _hiddenTokens.length > 0) { - - @if (_contentDensityObserver.isCompactSignal() || compactCollapse) { + + @if (_contentDensityObserver.isCompactSignal() || compactCollapse || display()) { {{ 'coreTokenizer.moreLabel' | fdTranslate: { count: moreTokensLeft.length + moreTokensRight.length } }} } - @if (!_contentDensityObserver.isCompactSignal() && !compactCollapse) { + @if (!_contentDensityObserver.isCompactSignal() && !compactCollapse && !display()) { {{ 'coreTokenizer.moreLabel' | fdTranslate: { count: hiddenCozyTokenCount } }} } @@ -41,16 +57,26 @@ }
- +
    @for (token of _hiddenTokens; track token) { -
  • +
  • - @if (!token.readOnly) { + @if (!token.readOnly && !token.display) { @if (_showMoreElement && _hiddenTokens.length > 0) { - - @if (_contentDensityObserver.isCompactSignal() || compactCollapse) { + + @if (_contentDensityObserver.isCompactSignal() || compactCollapse || display()) { {{ 'coreTokenizer.moreLabel' | fdTranslate: { count: moreTokensLeft.length + moreTokensRight.length } }} } - @if (!_contentDensityObserver.isCompactSignal() && !compactCollapse) { + @if (!_contentDensityObserver.isCompactSignal() && !compactCollapse && !display()) { {{ 'coreTokenizer.moreLabel' | fdTranslate: { count: hiddenCozyTokenCount } }} } diff --git a/libs/core/token/tokenizer.component.scss b/libs/core/token/tokenizer.component.scss index 350616045c0..c02bb06ab40 100644 --- a/libs/core/token/tokenizer.component.scss +++ b/libs/core/token/tokenizer.component.scss @@ -38,4 +38,69 @@ input { .fd-tokenizer__overflow-list-item { display: flex; justify-content: space-between; + + &-display { + pointer-events: none; + border: none; + } +} + +.fd-tokenizer { + &--display { + border: none; + box-shadow: none; + overflow: visible; + pointer-events: none; + background: transparent; + + .fd-tokenizer__inner { + justify-content: flex-start; + overflow: visible; + } + + .fd-tokenizer__inner-show-dot:has(+ fd-popover), + .fd-tokenizer__inner-show-dot:has(+ .fd-tokenizer-more) { + &::after { + content: '\00B7'; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1rem; + } + } + + .fd-token { + margin-inline-end: 0; + + &--display { + &::after { + content: '\00B7'; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1rem; + } + } + + &--isLast { + &::after { + content: ''; + min-width: 0; + } + } + } + + .fd-tokenizer__input { + display: none; + } + } +} + +.fd-tokenizer.fd-tokenizer--display.is-hover, +.fd-tokenizer.fd-tokenizer--display:hover { + background: transparent; +} + +.fd-tokenizer-allow-pointer { + pointer-events: all; } diff --git a/libs/core/token/tokenizer.component.ts b/libs/core/token/tokenizer.component.ts index 9d454a7bf90..1fd2641d0ee 100644 --- a/libs/core/token/tokenizer.component.ts +++ b/libs/core/token/tokenizer.component.ts @@ -27,7 +27,8 @@ import { ViewContainerRef, ViewEncapsulation, forwardRef, - inject + inject, + input } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -50,6 +51,8 @@ import { BehaviorSubject, Observable, Subscription, firstValueFrom, fromEvent, m import { debounceTime, filter, map } from 'rxjs/operators'; import { TokenComponent } from './token.component'; +let tokenizerId = 0; + @Component({ selector: 'fd-tokenizer', templateUrl: './tokenizer.component.html', @@ -67,7 +70,12 @@ import { TokenComponent } from './token.component'; ListComponent, ListItemComponent, FdTranslatePipe - ] + ], + host: { + '[attr.id]': 'id()', + '[attr.aria-labelledby]': 'ariaLabelledBy()', + '[style.display]': 'this.display() ? "block" : null' + } }) export class TokenizerComponent implements AfterViewInit, OnDestroy, CssClassBuilder, OnInit, OnChanges { /** user's custom classes */ @@ -177,6 +185,16 @@ export class TokenizerComponent implements AfterViewInit, OnDestroy, CssClassBui /** @hidden */ _tokensContainerWidth = 'auto'; + /** Whether the tokenizer is display-only */ + display = input(false); + + /** Tokenizer ID + * Default value is provided if not set */ + id = input('fd-tokenizer-id-' + ++tokenizerId); + + /** Tokenizer aria-labelledby attribute binding. */ + ariaLabelledBy = input(); + /** @hidden */ private _translationResolver = new TranslationResolver(); @@ -306,7 +324,7 @@ export class TokenizerComponent implements AfterViewInit, OnDestroy, CssClassBui }); }); - if (!this._contentDensityObserver.isCompact && !this.compactCollapse) { + if (!this._contentDensityObserver.isCompact && !this.compactCollapse && !this.display()) { this._handleCozyTokenCount(); } this._listenElementEvents(); @@ -380,7 +398,7 @@ export class TokenizerComponent implements AfterViewInit, OnDestroy, CssClassBui const elementWidth = this.elementRef.nativeElement.getBoundingClientRect().width; this._resetTokens(); this.previousElementWidth = elementWidth; - if (!this._contentDensityObserver.isCompact && !this.compactCollapse) { + if (!this._contentDensityObserver.isCompact && !this.compactCollapse && !this.display()) { this._handleCozyTokenCount(); } } @@ -560,7 +578,7 @@ export class TokenizerComponent implements AfterViewInit, OnDestroy, CssClassBui if (this._forceAllTokensToDisplay) { return; } - if (!this._contentDensityObserver.isCompact && !this.compactCollapse) { + if (!this._contentDensityObserver.isCompact && !this.compactCollapse && !this.display()) { this._getHiddenCozyTokenCount(); return; } @@ -624,7 +642,12 @@ export class TokenizerComponent implements AfterViewInit, OnDestroy, CssClassBui private _resetTokens(): void { this.moreTokensLeft = []; this.moreTokensRight = []; - if (this._contentDensityObserver.isCompact || this.compactCollapse || this._forceAllTokensToDisplay) { + if ( + this._contentDensityObserver.isCompact || + this.compactCollapse || + this.display() || + this._forceAllTokensToDisplay + ) { this.tokenList.forEach((token) => { this._makeElementVisible(token.elementRef); }); diff --git a/libs/docs/core/multi-input/examples/multi-input-example/multi-input-example.component.html b/libs/docs/core/multi-input/examples/multi-input-example/multi-input-example.component.html index 22ca1fa6ee3..edfb24676bc 100644 --- a/libs/docs/core/multi-input/examples/multi-input-example/multi-input-example.component.html +++ b/libs/docs/core/multi-input/examples/multi-input-example/multi-input-example.component.html @@ -24,6 +24,22 @@

    +
    + + +
    + +Selected: {{ displayOnlySelected | json }} + +

    + Bibendum +Lorem +Dolor +Filter diff --git a/libs/docs/core/token/examples/token-display-example/token-display-example.component.ts b/libs/docs/core/token/examples/token-display-example/token-display-example.component.ts new file mode 100644 index 00000000000..058aa1cf3bd --- /dev/null +++ b/libs/docs/core/token/examples/token-display-example/token-display-example.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { TokenComponent } from '@fundamental-ngx/core/token'; + +@Component({ + selector: 'fd-token-display-example', + templateUrl: './token-display-example.component.html', + styles: [ + ` + fd-token { + padding-right: 4px; + } + ` + ], + imports: [TokenComponent] +}) +export class TokenDisplayExampleComponent {} diff --git a/libs/docs/core/token/examples/tokenizer-display-example/tokenizer-display-example.component.html b/libs/docs/core/token/examples/tokenizer-display-example/tokenizer-display-example.component.html new file mode 100644 index 00000000000..0058fbfcfe3 --- /dev/null +++ b/libs/docs/core/token/examples/tokenizer-display-example/tokenizer-display-example.component.html @@ -0,0 +1,15 @@ +
    +
    + + @for (token of tokens; track token; let last = $last) { + {{ token.text }} + } + + +
    +
    diff --git a/libs/docs/core/token/examples/tokenizer-display-example/tokenizer-display-example.component.ts b/libs/docs/core/token/examples/tokenizer-display-example/tokenizer-display-example.component.ts new file mode 100644 index 00000000000..d4c3854ab59 --- /dev/null +++ b/libs/docs/core/token/examples/tokenizer-display-example/tokenizer-display-example.component.ts @@ -0,0 +1,52 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ContentDensityDirective } from '@fundamental-ngx/core/content-density'; +import { FormControlComponent, FormItemComponent } from '@fundamental-ngx/core/form'; +import { TokenComponent, TokenizerComponent, TokenizerInputDirective } from '@fundamental-ngx/core/token'; + +@Component({ + selector: 'fd-tokenizer-display-example', + templateUrl: './tokenizer-display-example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + FormItemComponent, + TokenComponent, + TokenizerComponent, + TokenizerInputDirective, + ContentDensityDirective, + FormControlComponent + ] +}) +export class TokenizerDisplayExampleComponent implements OnInit { + tokenizerExampleForm: FormGroup; + + inputValue = 'New token'; + + tokens = [ + { text: 'One', display: true }, + { text: 'Two', display: true }, + { text: 'Three', display: true }, + { text: 'Four', display: true }, + { text: 'Five', display: true }, + { text: 'Six', display: true }, + { text: 'Seven', display: true }, + { text: 'Eight', display: true }, + { text: 'Nine', display: true }, + { text: 'Ten', display: true }, + { text: 'Eleven', display: true }, + { text: 'Twelve', display: true }, + { text: 'Thirteen', display: true }, + { text: 'Fourteen', display: true }, + { text: 'Fifteen', display: true }, + { text: 'Sixteen', display: true } + ]; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.tokenizerExampleForm = this.fb.group({ + inputControl: new FormControl('', Validators.required) + }); + } +} diff --git a/libs/docs/core/token/token-docs.component.html b/libs/docs/core/token/token-docs.component.html index 7a883b86f0a..9ad73ba9b1b 100644 --- a/libs/docs/core/token/token-docs.component.html +++ b/libs/docs/core/token/token-docs.component.html @@ -16,6 +16,12 @@ + Display Only Token + + + + + Compact Token @@ -42,3 +48,12 @@ + + Display Only Tokenizer + + The display only tokenizer is for tokenizers that only display data, no user interaction is allowed. + + + + + diff --git a/libs/docs/core/token/token-docs.component.ts b/libs/docs/core/token/token-docs.component.ts index ef71381ed98..6f51243d286 100644 --- a/libs/docs/core/token/token-docs.component.ts +++ b/libs/docs/core/token/token-docs.component.ts @@ -9,10 +9,12 @@ import { getAssetFromModuleAssets } from '@fundamental-ngx/docs/shared'; import { TokenCompactExampleComponent } from './examples/token-compact-example/token-compact-example.component'; +import { TokenDisplayExampleComponent } from './examples/token-display-example/token-display-example.component'; import { TokenExampleComponent } from './examples/token-example/token-example.component'; import { TokenReadOnlyExampleComponent } from './examples/token-readonly-example/token-readonly-example.component'; import { TokenSelectedExampleComponent } from './examples/token-selected-example/token-selected-example.component'; import { TokenizerCompactExampleComponent } from './examples/tokenizer-compact-example/tokenizer-compact-example.component'; +import { TokenizerDisplayExampleComponent } from './examples/tokenizer-display-example/tokenizer-display-example.component'; import { TokenizerExampleComponent } from './examples/tokenizer-example/tokenizer-example.component'; const basicTokenH = 'token-example/token-example.component.html'; @@ -24,6 +26,9 @@ const selectedTokenTs = 'token-selected-example/token-selected-example.component const readOnlyTokenH = 'token-readonly-example/token-readonly-example.component.html'; const readOnlyTokenTs = 'token-readonly-example/token-readonly-example.component.ts'; +const displayTokenH = 'token-display-example/token-display-example.component.html'; +const displayTokenTs = 'token-display-example/token-display-example.component.ts'; + const compactTokenH = 'token-compact-example/token-compact-example.component.html'; const compactTokenTs = 'token-compact-example/token-compact-example.component.ts'; @@ -33,6 +38,9 @@ const tokenizerTsCode = 'tokenizer-example/tokenizer-example.component.ts'; const tokenizerCompactH = 'tokenizer-compact-example/tokenizer-compact-example.component.html'; const tokenizerCompactTsCode = 'tokenizer-compact-example/tokenizer-compact-example.component.ts'; +const tokenizerDisplayH = 'tokenizer-display-example/tokenizer-display-example.component.html'; +const tokenizerDisplayTsCode = 'tokenizer-display-example/tokenizer-display-example.component.ts'; + @Component({ selector: 'app-token-docs', templateUrl: './token-docs.component.html', @@ -45,10 +53,12 @@ const tokenizerCompactTsCode = 'tokenizer-compact-example/tokenizer-compact-exam CodeExampleComponent, TokenSelectedExampleComponent, TokenReadOnlyExampleComponent, + TokenDisplayExampleComponent, TokenCompactExampleComponent, DescriptionComponent, TokenizerExampleComponent, - TokenizerCompactExampleComponent + TokenizerCompactExampleComponent, + TokenizerDisplayExampleComponent ] }) export class TokenDocsComponent { @@ -79,6 +89,15 @@ export class TokenDocsComponent { typescriptFileCode: getAssetFromModuleAssets(readOnlyTokenTs) } ]; + displayToken: ExampleFile[] = [ + { + language: 'html', + code: getAssetFromModuleAssets(displayTokenH), + fileName: 'token-display-example', + component: 'TokenDisplayExampleComponent', + typescriptFileCode: getAssetFromModuleAssets(displayTokenTs) + } + ]; compactToken: ExampleFile[] = [ { language: 'html', @@ -114,4 +133,17 @@ export class TokenDocsComponent { fileName: 'tokenizer-compact-example' } ]; + tokenizerDisplay: ExampleFile[] = [ + { + language: 'html', + code: getAssetFromModuleAssets(tokenizerDisplayH), + fileName: 'tokenizer-display-example' + }, + { + language: 'typescript', + component: 'TokenizerDisplayExampleComponent', + code: getAssetFromModuleAssets(tokenizerDisplayTsCode), + fileName: 'tokenizer-display-example' + } + ]; }