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
239 changes: 239 additions & 0 deletions packages/main/cypress/specs/Tokenizer.cy.tsx
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,245 @@ describe("Accessibility", () => {
});
});

describe("Scrolling Behavior", () => {
it("should scroll to end when tokenizer is expanded without focused token", () => {
cy.mount(
<div style={{ width: "150px" }}>
<Tokenizer>
<Token text="Very long token text that will definitely cause overflow in this narrow container"></Token>
<Token text="Another very long token text that also causes overflow"></Token>
<Token text="Third very long token text for overflow"></Token>
<Token text="Fourth very long token text for overflow"></Token>
<Token text="Fifth very long token text for overflow"></Token>
</Tokenizer>
</div>
);

// Verify there's actually overflow to test
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
const element = $content[0];
// Only proceed if there's actual overflow
if (element.scrollWidth > element.clientWidth) {
expect(element.scrollLeft).to.equal(0);
}
});

// Click on tokenizer container to expand it
cy.get("[ui5-tokenizer]")
.realClick();

// Verify tokenizer is expanded
cy.get("[ui5-tokenizer]")
.should("have.attr", "expanded");

// Should scroll to end when expanded without focused token (only if there's overflow)
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
const element = $content[0];
const maxScroll = element.scrollWidth - element.clientWidth;
if (maxScroll > 0) {
// Should be scrolled to the end (or close to it)
expect(element.scrollLeft).to.be.greaterThan(maxScroll * 0.5);
}
});
});

it("should scroll to specific token when token is clicked", () => {
cy.mount(
<div style={{ width: "150px" }}>
<Tokenizer>
<Token text="Very long token text that will definitely cause overflow in this narrow container"></Token>
<Token text="Another very long token text that also causes overflow"></Token>
<Token text="Third very long token text for overflow"></Token>
<Token text="Fourth very long token text for overflow"></Token>
<Token text="Fifth very long token text for overflow"></Token>
</Tokenizer>
</div>
);

// Click on the third token (middle token)
cy.get("[ui5-token]")
.eq(2)
.as("thirdToken")
.realClick();

// Verify token is selected and focused (separate assertions)
cy.get("@thirdToken")
.should("have.attr", "selected");

cy.get("@thirdToken")
.should("have.attr", "focused");

// Verify tokenizer is expanded
cy.get("[ui5-tokenizer]")
.should("have.attr", "expanded");

// Should scroll to show the selected token
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
const element = $content[0];
const maxScroll = element.scrollWidth - element.clientWidth;
if (maxScroll > 0) {
// Should be scrolled some amount to show the token
expect(element.scrollLeft).to.be.greaterThan(0);
}
});
});

it("should scroll when navigating with Home and End keys", () => {
cy.mount(
<div style={{ width: "150px" }}>
<Tokenizer>
<Token text="Very long token text that will definitely cause overflow in this narrow container"></Token>
<Token text="Another very long token text that also causes overflow"></Token>
<Token text="Third very long token text for overflow"></Token>
<Token text="Fourth very long token text for overflow"></Token>
<Token text="Fifth very long token text for overflow"></Token>
</Tokenizer>
</div>
);

// Click on first token
cy.get("[ui5-token]")
.eq(0)
.realClick();

// Navigate to the last token using End key
cy.realPress("End");

cy.get("[ui5-token]")
.eq(4)
.should("have.attr", "focused");

// Should scroll to end for last token
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
const element = $content[0];
const maxScroll = element.scrollWidth - element.clientWidth;
if (maxScroll > 0) {
expect(element.scrollLeft).to.be.closeTo(maxScroll, 20);
}
});

// Navigate back to first token using Home key
cy.realPress("Home");

cy.get("[ui5-token]")
.eq(0)
.should("have.attr", "focused");

// Should scroll back to start for first token
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
const element = $content[0];
expect(element.scrollLeft).to.be.closeTo(0, 20);
});
});

it("should maintain scroll position when token selection is toggled", () => {
cy.mount(
<div style={{ width: "150px" }}>
<Tokenizer>
<Token text="Very long token text that will definitely cause overflow in this narrow container"></Token>
<Token text="Another very long token text that also causes overflow"></Token>
<Token text="Third very long token text for overflow"></Token>
<Token text="Fourth very long token text for overflow"></Token>
<Token text="Fifth very long token text for overflow"></Token>
</Tokenizer>
</div>
);

// Click on middle token to select it
cy.get("[ui5-token]")
.eq(2)
.as("middleToken")
.realClick();

// Get scroll position after clicking middle token
let scrollAfterClick;
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
scrollAfterClick = $content[0].scrollLeft;
});

// Toggle selection with space (should deselect but maintain focus)
cy.realPress("Space");

cy.get("@middleToken")
.should("not.have.attr", "selected");

cy.get("@middleToken")
.should("have.attr", "focused");

// Scroll position should remain the same since token is still focused
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
expect($content[0].scrollLeft).to.equal(scrollAfterClick);
});
});

it("should scroll to end when tokenizer regains focus without focused token", () => {
cy.mount(
<div style={{ width: "150px" }}>
<Tokenizer>
<Token text="Very long token text that will definitely cause overflow in this narrow container"></Token>
<Token text="Another very long token text that also causes overflow"></Token>
<Token text="Third very long token text for overflow"></Token>
<Token text="Fourth very long token text for overflow"></Token>
<Token text="Fifth very long token text for overflow"></Token>
</Tokenizer>
<button>External button</button>
</div>
);

// Click on a token to expand tokenizer
cy.get("[ui5-token]")
.eq(1)
.realClick();

// Tab to external button (lose focus)
cy.realPress("Tab");

// Verify tokenizer is collapsed
cy.get("[ui5-tokenizer]")
.should("not.have.attr", "expanded");

// Tab back to tokenizer (regain focus)
cy.realPress(["Shift", "Tab"]);

// Verify tokenizer is expanded again
cy.get("[ui5-tokenizer]")
.should("have.attr", "expanded");

// Should scroll to end when regaining focus (as per _scrollToEndIfNeeded logic)
cy.get("[ui5-tokenizer]")
.shadow()
.find(".ui5-tokenizer--content")
.then($content => {
const element = $content[0];
const maxScroll = element.scrollWidth - element.clientWidth;
if (maxScroll > 0) {
expect(element.scrollLeft).to.be.greaterThan(maxScroll * 0.5);
}
});
});
});

describe("Keyboard Handling", () => {
beforeEach(() => {
cy.mount(
Expand Down
8 changes: 3 additions & 5 deletions packages/main/src/MultiComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
_showFilteredItems() {
this.filterSelected = true;
this._showMorePressed = true;
this._tokenizer._scrollToEndOnExpand = true;

this._toggleTokenizerPopover();
}
Expand Down Expand Up @@ -1778,13 +1779,10 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
this._tokenizer.preventInitialFocus = true;

if (this.open && !isPhone()) {
this._tokenizer._scrollToEndOnExpand = true;
this._tokenizer.expanded = true;
}

if (this._tokenizer.expanded && this.hasAttribute("focused")) {
this._tokenizer.scrollToEnd();
}

if (!arraysAreEqual(this._valueStateLinks, this.linksInAriaValueStateHiddenText)) {
this._removeLinksEventListeners();
this._addLinksEventListeners();
Expand Down Expand Up @@ -1882,8 +1880,8 @@ class MultiComboBox extends UI5Element implements IFormInputElement {
inputFocusIn(e: FocusEvent) {
if (!isPhone()) {
this.focused = true;
this._tokenizer._scrollToEndOnExpand = true;
this._tokenizer.expanded = true;
this._tokenizer.scrollToEnd();
} else {
this._innerInput.blur();
}
Expand Down
13 changes: 5 additions & 8 deletions packages/main/src/MultiInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ class MultiInput extends Input implements IFormInputElement {
_tokenizerFocusOut(e: FocusEvent) {
if (!this.contains(e.relatedTarget as HTMLElement) && !this.shadowRoot!.contains(e.relatedTarget as HTMLElement)) {
this.tokenizer._tokens.forEach(token => { token.selected = false; });
this.tokenizer.scrollToStart();
}
}

Expand All @@ -210,15 +209,19 @@ class MultiInput extends Input implements IFormInputElement {
}

innerFocusIn() {
this.tokenizer._scrollToEndOnExpand = true;
this.tokenizer.expanded = true;
this.focused = true;
this.tokenizer.scrollToEnd();

this.tokens.forEach(token => {
token.selected = false;
});
}

_showMoreItemsPress() {
this.tokenizer._scrollToEndOnExpand = true;
}

_onkeydown(e: KeyboardEvent) {
!this._isComposing && super._onkeydown(e);

Expand Down Expand Up @@ -346,12 +349,6 @@ class MultiInput extends Input implements IFormInputElement {
super.onAfterRendering();

this.tokenizer.preventInitialFocus = true;

if (this.tokenizer.expanded) {
this.tokenizer.scrollToEnd();
} else {
this.tokenizer.scrollToStart();
}
}

get iconsCount() {
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/MultiInputTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function preContent(this: MultiInput) {
onKeyDown={this._onTokenizerKeydown}
onTokenDelete={this.tokenDelete}
onFocusOut={this._tokenizerFocusOut}
onShowMoreItemsPress={this._showMoreItemsPress}
>
{ this.tokens.map(token => <slot name={token._individualSlot}></slot>)}
</Tokenizer>
Expand Down
Loading
Loading