-
Notifications
You must be signed in to change notification settings - Fork 205
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
fix(tabs): scroll exceeding tabs limit #4722
Changes from 2 commits
5182d9e
36b1e19
80d1a67
7ec37a9
67cf3ef
4cc9f6d
9ee6ce3
434e63f
ad939e5
258a11f
b24869c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,14 +25,18 @@ import { | |
} from '@spectrum-web-components/tabs'; | ||
import { ActionButton } from '@spectrum-web-components/action-button'; | ||
|
||
import { elementUpdated, expect, fixture } from '@open-wc/testing'; | ||
import { elementUpdated, expect, fixture, waitUntil } from '@open-wc/testing'; | ||
import { | ||
ElementSize, | ||
ElementSizes, | ||
html, | ||
nothing, | ||
} from '@spectrum-web-components/base'; | ||
import { repeat } from 'lit/directives/repeat.js'; | ||
import { sendKeys, setUserAgent, setViewport } from '@web/test-runner-commands'; | ||
|
||
const RIGHT_BUTTON_SELECTOR = '.right-scroll'; | ||
const LEFT_BUTTON_SELECTOR = '.left-scroll'; | ||
|
||
type OverflowProperties = { | ||
count: number; | ||
|
@@ -41,46 +45,52 @@ type OverflowProperties = { | |
selected?: number; | ||
labelPrev?: string; | ||
labelNext?: string; | ||
dir?: 'ltr' | 'rtl'; | ||
}; | ||
|
||
const renderTabsOverflow = async ({ | ||
count, | ||
size, | ||
includeTabPanel, | ||
selected = 1, | ||
dir = 'ltr', | ||
}: OverflowProperties): Promise<HTMLDivElement> => { | ||
const tabsContainer = await fixture<HTMLDivElement>(html` | ||
<div class="container" style="width: 200px; height: 150px;"> | ||
<sp-tabs-overflow> | ||
<sp-tabs size=${size} selected=${selected}> | ||
${repeat( | ||
new Array(count), | ||
(item) => item, | ||
(_item, index) => html` | ||
<sp-tab | ||
label=${`Tab Item ${index + 1}`} | ||
value=${index + 1} | ||
></sp-tab> | ||
` | ||
)} | ||
${includeTabPanel | ||
? html` | ||
${repeat( | ||
new Array(count), | ||
(item) => item, | ||
(_item, index) => html` | ||
<sp-tab-panel value=${index + 1}> | ||
Content for Tab Item ${index + 1} | ||
</sp-tab-panel> | ||
` | ||
)} | ||
` | ||
: nothing} | ||
</sp-tabs> | ||
</sp-tabs-overflow> | ||
</div> | ||
const theme = await fixture<HTMLDivElement>(html` | ||
<sp-theme dir=${dir} system="spectrum" scale="medium" color="light"> | ||
<div class="container" style="width: 200px; height: 150px;"> | ||
<sp-tabs-overflow> | ||
<sp-tabs size=${size} selected=${selected}> | ||
${repeat( | ||
new Array(count), | ||
(item) => item, | ||
(_item, index) => html` | ||
<sp-tab | ||
label=${`Tab Item ${index + 1}`} | ||
value=${index + 1} | ||
></sp-tab> | ||
` | ||
)} | ||
${includeTabPanel | ||
? html` | ||
${repeat( | ||
new Array(count), | ||
(item) => item, | ||
(_item, index) => html` | ||
<sp-tab-panel value=${index + 1}> | ||
Content for Tab Item ${index + 1} | ||
</sp-tab-panel> | ||
` | ||
)} | ||
` | ||
: nothing} | ||
</sp-tabs> | ||
</sp-tabs-overflow> | ||
</div> | ||
</sp-theme> | ||
`); | ||
await elementUpdated(tabsContainer); | ||
await elementUpdated(theme); | ||
const tabsContainer = theme.querySelector('.container') as HTMLDivElement; | ||
|
||
return tabsContainer; | ||
}; | ||
|
||
|
@@ -169,6 +179,74 @@ describe('TabsOverflow', () => { | |
expect(finalLeft).to.be.lessThanOrEqual(initialLeft); | ||
}); | ||
|
||
xit('should scroll up to the last item and back in LTR', async () => { | ||
// setUserAgent is not currently supported by Playwright | ||
await setUserAgent( | ||
'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148' | ||
); | ||
|
||
const el = await renderTabsOverflow({ | ||
count: 8, | ||
size: ElementSizes.L, | ||
includeTabPanel: true, | ||
dir: 'ltr', | ||
}); | ||
await elementUpdated(el); | ||
await setViewport({ width: 360, height: 640 }); | ||
await nextFrame(); | ||
|
||
const tabsOverflow = el.querySelector( | ||
'sp-tabs-overflow' | ||
) as TabsOverflow; | ||
|
||
expect(tabsOverflow['overflowState'].canScrollLeft).to.be.false; | ||
expect(tabsOverflow['overflowState'].canScrollRight).to.be.true; | ||
|
||
await scrollToEnd(el, RIGHT_BUTTON_SELECTOR, 'ltr'); | ||
|
||
expect(tabsOverflow['overflowState'].canScrollLeft).to.be.true; | ||
expect(tabsOverflow['overflowState'].canScrollRight).to.be.false; | ||
|
||
await scrollToEnd(el, LEFT_BUTTON_SELECTOR, 'ltr'); | ||
|
||
expect(tabsOverflow['overflowState'].canScrollLeft).to.be.false; | ||
expect(tabsOverflow['overflowState'].canScrollRight).to.be.true; | ||
}); | ||
|
||
xit('should scroll up to the last item and back in RTL', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it work and make sense to let these tests run, even without setting the user agent? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can run the tests if we remove the The thing with this approach is that they wouldn't fail even if this bug is reproducible on iOS and I think is good to signal in some way that these tests must be enabled once we have a way to run them on iPad/iOS/Android. I'm thinking to enable them in the current tests contexts and commenting the // TODO: run on iPhone as per https://github.com/adobe/spectrum-web-components/pull/4722 What do you think, do you have any suggestions on this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think that approach is good. While this bug is iOS-only, this test is running in a mobile viewport which still helps with our testing coverage 👍 |
||
// setUserAgent is not currently supported by Playwright | ||
await setUserAgent( | ||
'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148' | ||
); | ||
|
||
const el = await renderTabsOverflow({ | ||
count: 8, | ||
size: ElementSizes.L, | ||
includeTabPanel: true, | ||
dir: 'rtl', | ||
}); | ||
await elementUpdated(el); | ||
await setViewport({ width: 360, height: 640 }); | ||
await nextFrame(); | ||
|
||
const tabsOverflow = el.querySelector( | ||
'sp-tabs-overflow' | ||
) as TabsOverflow; | ||
|
||
expect(tabsOverflow['overflowState'].canScrollLeft).to.be.true; | ||
expect(tabsOverflow['overflowState'].canScrollRight).to.be.false; | ||
mizgaionutalexandru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
await scrollToEnd(el, LEFT_BUTTON_SELECTOR, 'rtl'); | ||
|
||
expect(tabsOverflow['overflowState'].canScrollLeft).to.be.false; | ||
expect(tabsOverflow['overflowState'].canScrollRight).to.be.true; | ||
|
||
await scrollToEnd(el, RIGHT_BUTTON_SELECTOR, 'rtl'); | ||
|
||
expect(tabsOverflow['overflowState'].canScrollLeft).to.be.true; | ||
expect(tabsOverflow['overflowState'].canScrollRight).to.be.false; | ||
}); | ||
|
||
it('should fail properly if slot is not sp-tabs', async () => { | ||
const el = await fixture<TabsOverflow>(html` | ||
<sp-tabs-overflow> | ||
|
@@ -362,3 +440,83 @@ describe('calculateScrollTargetForLeftSide', () => { | |
).to.equal(0); | ||
}); | ||
}); | ||
|
||
async function repeatScroll( | ||
options: { | ||
times: number; | ||
elementToUpdate: TabsOverflow; | ||
elementToScroll: HTMLElement; | ||
distanceToReachInIteration: (iteration: number) => number; | ||
}, | ||
iteration = 1 | ||
): Promise<void> { | ||
const { | ||
times, | ||
elementToUpdate, | ||
elementToScroll, | ||
distanceToReachInIteration, | ||
} = options; | ||
if (iteration > times) return; | ||
|
||
const distanceToReach = distanceToReachInIteration(iteration); | ||
|
||
await sendKeys({ press: 'Enter' }); | ||
await elementUpdated(elementToUpdate); | ||
await waitUntil( | ||
mizgaionutalexandru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
() => | ||
Math.ceil(Math.abs(elementToScroll.scrollLeft)) - | ||
Math.abs(distanceToReach) === | ||
mizgaionutalexandru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
0, | ||
`scroll to ${distanceToReach}` | ||
); | ||
return await repeatScroll(options, iteration + 1); | ||
} | ||
|
||
async function scrollToEnd( | ||
tabsContainer: HTMLDivElement, | ||
buttonSelector: string, | ||
direction: 'ltr' | 'rtl' = 'ltr' | ||
): Promise<void> { | ||
const tabs = tabsContainer.querySelector('sp-tabs') as Tabs; | ||
const tabsList = tabs.shadowRoot!.querySelector('#list') as HTMLElement; | ||
const tabsOverflow = tabsContainer.querySelector( | ||
'sp-tabs-overflow' | ||
) as TabsOverflow; | ||
const button = tabsOverflow.shadowRoot.querySelector( | ||
buttonSelector | ||
) as ActionButton; | ||
|
||
const { scrollWidth, clientWidth } = tabsList; | ||
const distPerScroll = clientWidth * tabsOverflow['scrollFactor']; | ||
const totalScrollDist = scrollWidth - clientWidth; | ||
const scrollsToEnd = Math.ceil(totalScrollDist / distPerScroll); | ||
let distanceToReachInIteration: (iteration: number) => number; | ||
|
||
if (direction === 'ltr') { | ||
distanceToReachInIteration = | ||
buttonSelector === LEFT_BUTTON_SELECTOR | ||
? (iteration: number) => | ||
Math.max(totalScrollDist - iteration * distPerScroll, 0) | ||
: (iteration: number) => | ||
Math.min(iteration * distPerScroll, totalScrollDist); | ||
} else { | ||
distanceToReachInIteration = | ||
buttonSelector === LEFT_BUTTON_SELECTOR | ||
? (iteration: number) => | ||
Math.max(-1 * iteration * distPerScroll, -totalScrollDist) | ||
: (iteration: number) => | ||
-Math.max(totalScrollDist - iteration * distPerScroll, 0); | ||
} | ||
|
||
button.focus(); | ||
return await repeatScroll({ | ||
times: scrollsToEnd, | ||
elementToUpdate: tabsOverflow, | ||
elementToScroll: tabsList, | ||
distanceToReachInIteration, | ||
}); | ||
} | ||
|
||
function nextFrame(): Promise<void> { | ||
return new Promise((res) => requestAnimationFrame(() => res())); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a documentation block on these methods!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for pointing that out. Indeed this method especially seemed somewhat ambiguous. I changed it's implementation so that it does the same thing but in a cleaner manner. What are your thoughts on this?