Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-tokenizer): enable multiline mode #9964

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
114 changes: 114 additions & 0 deletions packages/main/cypress/specs/Tokenizer.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { html } from "lit";
import "../../src/Tokenizer.js";
import type Tokenizer from "../../src/Tokenizer.js";

describe("Tokenizer - multi-line and Clear All", () => {
it("'Clear All' link is rendered for multi-line tokenizer and show-clear-all set to true", () => {
cy.mount(html`<ui5-tokenizer show-clear-all multi-line>
<ui5-token text="Andora"></ui5-token>
<ui5-token text="Bulgaria"></ui5-token>
<ui5-token text="Canada"></ui5-token>
<ui5-token text="Denmark"></ui5-token>
<ui5-token text="Estonia"></ui5-token>
<ui5-token text="Finland"></ui5-token>
<ui5-token text="Germany"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--clear-all")
.should("exist");
});

it("'Clear All' link is rendered even for 1 token when in multi-line mode", () => {
cy.mount(html`<ui5-tokenizer show-clear-all multi-line>
<ui5-token text="Andora"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--clear-all")
.should("exist");
});

it("'Clear All' link is not rendered for single-line tokenizer even when show-clear-all is set to true", () => {
cy.mount(html`<ui5-tokenizer show-clear-all>
<ui5-token text="Andora"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--clear-all")
.should("not.exist");
});

it("'Clear All' link is not rendered for multi-line tokenizer when show-clear-all is set to false", () => {
cy.mount(html`<ui5-tokenizer multi-line>
<ui5-token text="Andora"></ui5-token>
<ui5-token text="Bulgaria"></ui5-token>
<ui5-token text="Canada"></ui5-token>
<ui5-token text="Denmark"></ui5-token>
<ui5-token text="Estonia"></ui5-token>
<ui5-token text="Finland"></ui5-token>
<ui5-token text="Germany"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--clear-all")
.should("not.exist");
});

it("'Clear All' link is not rendered for multi-line readonly tokenizer when show-clear-all 'true'", () => {
cy.mount(html`<ui5-tokenizer multi-line show-clear-all readonly>
<ui5-token text="Andora"></ui5-token>
<ui5-token text="Bulgaria"></ui5-token>
<ui5-token text="Canada"></ui5-token>
<ui5-token text="Denmark"></ui5-token>
<ui5-token text="Estonia"></ui5-token>
<ui5-token text="Finland"></ui5-token>
<ui5-token text="Germany"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--clear-all")
.should("not.exist");
});

it("'n-more' link is not rendered for multi-line tokenizer", () => {
cy.mount(html`<ui5-tokenizer multi-line style="width: 100px;">
<ui5-token text="Andora"></ui5-token>
<ui5-token text="Bulgaria"></ui5-token>
<ui5-token text="Canada"></ui5-token>
<ui5-token text="Denmark"></ui5-token>
<ui5-token text="Estonia"></ui5-token>
<ui5-token text="Finland"></ui5-token>
<ui5-token text="Germany"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--more-text")
.should("not.exist");
});

it("Pressing 'Clear All' link fires token-delete event", () => {
cy.mount(html`<ui5-tokenizer show-clear-all multi-line>
<ui5-token text="Andora"></ui5-token>
<ui5-token text="Bulgaria"></ui5-token>
<ui5-token text="Canada"></ui5-token>
</ui5-tokenizer>`);

cy.get<Tokenizer>("[ui5-tokenizer]").then($tokenizer => $tokenizer.get(0).addEventListener("token-delete", cy.stub().as("delete")));

cy.get<Tokenizer>("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--clear-all")
.eq(0)
.click();

cy.get("@delete")
.should("have.been.calledOnce");
});
});
45 changes: 28 additions & 17 deletions packages/main/src/Tokenizer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,37 @@
@focusout="{{_onfocusout}}"
@focusin="{{_onfocusin}}"
@ui5-select="{{onTokenSelect}}"
role="listbox"
aria-label="{{tokenizerLabel}}"
aria-description="{{tokenizerAriaDescription}}"
aria-disabled="{{_ariaDisabled}}"
aria-readonly="{{_ariaReadonly}}"
>
{{#each tokens}}
<slot name="{{this._individualSlot}}"></slot>
{{/each}}
<div class="ui5-tokenizer--list"
role="listbox"
aria-label="{{tokenizerLabel}}"
aria-description="{{tokenizerAriaDescription}}"
aria-disabled="{{_ariaDisabled}}"
aria-readonly="{{_ariaReadonly}}"
>
{{#each tokens}}
<slot name="{{this._individualSlot}}"></slot>
{{/each}}
</div>

{{#if showEffectiveClearAll}}
<span
role="button"
@click={{handleClearAll}}
class="ui5-tokenizer--clear-all">
{{_clearAllText}}</span>
{{/if}}
</div>

{{#if showNMore}}
<span
role="button"
aria-haspopup="dialog"
@click="{{_handleNMoreClick}}"
class="ui5-tokenizer-more-text"
part="n-more-text"
>{{_nMoreText}}</span>
{{/if}}
{{#if showNMore}}
<span
role="button"
aria-haspopup="dialog"
@click="{{_handleNMoreClick}}"
class="ui5-tokenizer-more-text"
part="n-more-text"
>{{_nMoreText}}</span>
{{/if}}
</div>

{{>include "./TokenizerPopover.hbs"}}
91 changes: 77 additions & 14 deletions packages/main/src/Tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
import getEffectiveScrollbarStyle from "@ui5/webcomponents-base/dist/util/getEffectiveScrollbarStyle.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
Expand Down Expand Up @@ -66,6 +67,7 @@ import {
TOKENIZER_ARIA_CONTAIN_ONE_TOKEN,
TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS,
TOKENIZER_SHOW_ALL_ITEMS,
TOKENIZER_CLEAR_ALL,
} from "./generated/i18n/i18n-defaults.js";

// Styles
Expand Down Expand Up @@ -144,6 +146,7 @@ enum ClipboardDataOperation {
ResponsivePopoverCommonCss,
SuggestionsCss,
TokenizerPopoverCss,
getEffectiveScrollbarStyle(),
],
dependencies: [
ResponsivePopover,
Expand Down Expand Up @@ -211,6 +214,26 @@ class Tokenizer extends UI5Element {
@property({ type: Boolean })
readonly = false;

/**
* Defines whether tokens are displayed on multiple lines.
*
* **Note:** The `multiLine` property is in an experimental state and is a subject to change.
* @default false
* @public
*/
@property({ type: Boolean })
multiLine = false;

/**
* Defines whether "Clear All" button is present. Ensure `multiLine` is enabled, otherwise `showClearAll` will have no effect.
*
* **Note:** The `showClearAll` property is in an experimental state and is a subject to change.
* @default false
* @public
*/
@property({ type: Boolean })
showClearAll = false;

/**
* Defines whether the component is disabled.
*
Expand Down Expand Up @@ -324,7 +347,7 @@ class Tokenizer extends UI5Element {
static i18nBundle: I18nBundle;
_resizeHandler: ResizeObserverCallback;
_itemNav: ItemNavigation;
_scrollEnablement: ScrollEnablement;
_scrollEnablement: ScrollEnablement | undefined;
_expandedScrollWidth?: number;
_tokenDeleting = false;
_preventCollapse = false;
Expand All @@ -347,16 +370,23 @@ class Tokenizer extends UI5Element {
getItemsCallback: this._getVisibleTokens.bind(this),
});

this._scrollEnablement = new ScrollEnablement(this);
this._deletedDialogItems = [];
}

handleClearAll() {
this.fireDecoratorEvent<TokenizerTokenDeleteEventDetail>("token-delete", { tokens: this._tokens });
}

onBeforeRendering() {
if (!this.multiLine) {
this._scrollEnablement = new ScrollEnablement(this);
}

const tokensLength = this._tokens.length;
this._tokensCount = tokensLength;

this._tokens.forEach(token => {
token.singleToken = tokensLength === 1;
token.singleToken = (tokensLength === 1) || this.multiLine;
token.readonly = this.readonly;
});
}
Expand Down Expand Up @@ -406,13 +436,19 @@ class Tokenizer extends UI5Element {
}
}

onTokenSelect() {
onTokenSelect(e: CustomEvent) {
const tokens = this._tokens;
const firstToken = tokens[0];
const targetToken = e.target as Token;

if (tokens.length === 1 && firstToken.isTruncatable) {
this.open = firstToken.selected;
}

if (this.multiLine && targetToken.isTruncatable) {
this.opener = targetToken;
this.open = targetToken.selected;
}
}

_getVisibleTokens() {
Expand All @@ -435,7 +471,9 @@ class Tokenizer extends UI5Element {
firstToken.forcedTabIndex = "0";
}

this._scrollEnablement.scrollContainer = this.contentDom;
if (this._scrollEnablement) {
this._scrollEnablement.scrollContainer = this.contentDom;
}

if (this.expanded) {
this._expandedScrollWidth = this.contentDom.scrollWidth;
Expand Down Expand Up @@ -575,9 +613,16 @@ class Tokenizer extends UI5Element {
}

handleBeforeOpen() {
this._tokens.forEach(token => {
token._isVisible = true;
});
if (this.multiLine) {
this._resetTokensVisibility();

const focusedToken = this._tokens.find(token => token.focused);
focusedToken!._isVisible = true;
} else {
this._tokens.forEach(token => {
token._isVisible = true;
});
}

const list = this._getList();
const firstListItem = list.querySelectorAll("[ui5-li]")[0]! as ListItem;
Expand Down Expand Up @@ -938,6 +983,20 @@ class Tokenizer extends UI5Element {
}
}

_resetTokensVisibility() {
this._tokens.forEach(token => {
token._isVisible = false;
});
}

get hasTokens() {
return this._tokens.length > 0;
}

get showEffectiveClearAll() {
return this.showClearAll && this.hasTokens && this.multiLine && !this.readonly;
}

_fillClipboard(shortcutName: ClipboardDataOperation, tokens: Array<IToken>) {
const tokensTexts = tokens.filter(token => token.selected).map(token => token.text).join("\r\n");

Expand All @@ -960,8 +1019,8 @@ class Tokenizer extends UI5Element {
* @protected
*/
scrollToStart() {
if (this._scrollEnablement.scrollContainer) {
this._scrollEnablement.scrollTo(0, 0);
if (this._scrollEnablement?.scrollContainer) {
this._scrollEnablement?.scrollTo(0, 0);
}
}

Expand All @@ -972,8 +1031,8 @@ class Tokenizer extends UI5Element {
*/
scrollToEnd() {
const expandedTokenizerScrollWidth = this.contentDom && (this.effectiveDir !== "rtl" ? this.contentDom.scrollWidth : -this.contentDom.scrollWidth);
if (this._scrollEnablement.scrollContainer) {
this._scrollEnablement.scrollTo(expandedTokenizerScrollWidth, 0, 5, 10);
if (this._scrollEnablement?.scrollContainer) {
this._scrollEnablement?.scrollTo(expandedTokenizerScrollWidth, 0, 5, 10);
}
}

Expand All @@ -991,9 +1050,9 @@ class Tokenizer extends UI5Element {
const tokenContainerRect = this.contentDom.getBoundingClientRect();

if (tokenRect.left < tokenContainerRect.left) {
this._scrollEnablement.scrollTo(this.contentDom.scrollLeft - (tokenContainerRect.left - tokenRect.left + 5), 0);
this._scrollEnablement?.scrollTo(this.contentDom.scrollLeft - (tokenContainerRect.left - tokenRect.left + 5), 0);
} else if (tokenRect.right > tokenContainerRect.right) {
this._scrollEnablement.scrollTo(this.contentDom.scrollLeft + (tokenRect.right - tokenContainerRect.right + 5), 0);
this._scrollEnablement?.scrollTo(this.contentDom.scrollLeft + (tokenRect.right - tokenContainerRect.right + 5), 0);
}
}

Expand Down Expand Up @@ -1025,6 +1084,10 @@ class Tokenizer extends UI5Element {
return Tokenizer.i18nBundle.getText(TOKENIZER_SHOW_ALL_ITEMS, this._nMoreCount);
}

get _clearAllText() {
return Tokenizer.i18nBundle.getText(TOKENIZER_CLEAR_ALL);
}

get showNMore() {
return !this.expanded && !!this.overflownTokens.length;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@ TOKENIZER_POPOVER_REMOVE=All items
#XFLD: Token number indicator which is used to show all tokens in Tokenizer when all of the tokens are hidden
TOKENIZER_SHOW_ALL_ITEMS={0} Items

#XFLD: Clear All link text in Tokenizer
TOKENIZER_CLEAR_ALL=Clear All

#XACT: Label text for TreeListItem
TREE_ITEM_ARIA_LABEL=Tree Item

Expand Down
Loading
Loading