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

fix(VTabs): rtl buttons and scroll behaviour #15701

Merged
merged 2 commits into from Aug 30, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 42 additions & 35 deletions packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx
Expand Up @@ -141,7 +141,8 @@ export const VSlideGroup = defineComponent({

function onTouchstart (e: TouchEvent) {
const sizeProperty = isHorizontal.value ? 'clientX' : 'clientY'
startOffset = scrollOffset.value
const sign = isRtl.value && isHorizontal.value ? -1 : 1
startOffset = sign * scrollOffset.value
startTouch = e.touches[0][sizeProperty]
disableTransition.value = true
}
Expand All @@ -150,31 +151,26 @@ export const VSlideGroup = defineComponent({
if (!isOverflowing.value) return

const sizeProperty = isHorizontal.value ? 'clientX' : 'clientY'
scrollOffset.value = startOffset + startTouch - e.touches[0][sizeProperty]
const sign = isRtl.value && isHorizontal.value ? -1 : 1
scrollOffset.value = sign * (startOffset + startTouch - e.touches[0][sizeProperty])
}

function onTouchend (e: TouchEvent) {
const maxScrollOffset = contentSize.value - containerSize.value

if (isRtl.value) {
if (scrollOffset.value > 0 || !isOverflowing.value) {
scrollOffset.value = 0
} else if (scrollOffset.value <= -maxScrollOffset) {
scrollOffset.value = -maxScrollOffset
}
} else {
if (scrollOffset.value < 0 || !isOverflowing.value) {
scrollOffset.value = 0
} else if (scrollOffset.value >= maxScrollOffset) {
scrollOffset.value = maxScrollOffset
}
if (scrollOffset.value < 0 || !isOverflowing.value) {
scrollOffset.value = 0
} else if (scrollOffset.value >= maxScrollOffset) {
scrollOffset.value = maxScrollOffset
}

disableTransition.value = false
}

function onScroll () {
containerRef.value && (containerRef.value.scrollLeft = 0)
if (!containerRef.value) return

containerRef.value[isHorizontal.value ? 'scrollLeft' : 'scrollTop'] = 0
}

const isFocused = ref(false)
Expand Down Expand Up @@ -216,11 +212,21 @@ export const VSlideGroup = defineComponent({
function onKeydown (e: KeyboardEvent) {
if (!contentRef.value) return

if (e.key === (isHorizontal.value ? 'ArrowRight' : 'ArrowDown')) {
focus('next')
} else if (e.key === (isHorizontal.value ? 'ArrowLeft' : 'ArrowUp')) {
focus('prev')
} else if (e.key === 'Home') {
if (isHorizontal.value) {
if (e.key === 'ArrowRight') {
focus(isRtl.value ? 'prev' : 'next')
} else if (e.key === 'ArrowLeft') {
focus(isRtl.value ? 'next' : 'prev')
}
} else {
if (e.key === 'ArrowDown') {
focus('next')
} else if (e.key === 'ArrowUp') {
focus('prev')
}
}

if (e.key === 'Home') {
focus('first')
} else if (e.key === 'End') {
focus('last')
Expand Down Expand Up @@ -252,22 +258,25 @@ export const VSlideGroup = defineComponent({
}

function scrollTo (location: 'prev' | 'next') {
const sign = isRtl.value ? -1 : 1
const newAbosluteOffset = sign * scrollOffset.value +
(location === 'prev' ? -1 : 1) * containerSize.value
const newAbsoluteOffset = scrollOffset.value + (location === 'prev' ? -1 : 1) * containerSize.value

scrollOffset.value = sign * clamp(newAbosluteOffset, 0, contentSize.value - containerSize.value)
scrollOffset.value = clamp(newAbsoluteOffset, 0, contentSize.value - containerSize.value)
}

const contentStyles = computed(() => {
const scrollAmount = scrollOffset.value <= 0
? bias(-scrollOffset.value)
: scrollOffset.value > contentSize.value - containerSize.value
? -(contentSize.value - containerSize.value) + bias(contentSize.value - containerSize.value - scrollOffset.value)
: -scrollOffset.value
// This adds friction when scrolling the 'wrong' way when at max offset
let scrollAmount = scrollOffset.value > contentSize.value - containerSize.value
? -(contentSize.value - containerSize.value) + bias(contentSize.value - containerSize.value - scrollOffset.value)
: -scrollOffset.value

// This adds friction when scrolling the 'wrong' way when at min offset
if (scrollOffset.value <= 0) {
scrollAmount = bias(-scrollOffset.value)
}

const sign = isRtl.value && isHorizontal.value ? -1 : 1
return {
transform: `translate${isHorizontal.value ? 'X' : 'Y'}(${scrollAmount}px)`,
transform: `translate${isHorizontal.value ? 'X' : 'Y'}(${sign * scrollAmount}px)`,
transition: disableTransition.value ? 'none' : '',
willChange: disableTransition.value ? 'transform' : '',
}
Expand Down Expand Up @@ -309,12 +318,10 @@ export const VSlideGroup = defineComponent({
})

const hasPrev = computed(() => {
return hasAffixes.value && scrollOffset.value > 0
return Math.abs(scrollOffset.value) > 0
})

const hasNext = computed(() => {
if (!hasAffixes.value) return false

// Check one scroll ahead to know the width of right-most item
return contentSize.value > Math.abs(scrollOffset.value) + containerSize.value
})
Expand Down Expand Up @@ -343,7 +350,7 @@ export const VSlideGroup = defineComponent({
>
{ slots.prev?.(slotProps.value) ?? (
<VFadeTransition>
<VIcon icon={ props.prevIcon }></VIcon>
<VIcon icon={ isRtl.value ? props.nextIcon : props.prevIcon }></VIcon>
</VFadeTransition>
) }
</div>
Expand Down Expand Up @@ -381,7 +388,7 @@ export const VSlideGroup = defineComponent({
>
{ slots.next?.(slotProps.value) ?? (
<VFadeTransition>
<VIcon icon={ props.nextIcon }></VIcon>
<VIcon icon={ isRtl.value ? props.prevIcon : props.nextIcon }></VIcon>
</VFadeTransition>
) }
</div>
Expand Down
Expand Up @@ -227,4 +227,31 @@ describe('VSlideGroup', () => {
cy.get('.item-1').should('not.be.visible')
cy.get('.item-7').should('be.visible')
})

it('should support rtl', () => {
cy.mount(() => (
<Application rtl>
<CenteredGrid width="400px">
<VSlideGroup selectedClass="bg-primary" showArrows>
{ createRange(8).map(i => (
<VSlideGroupItem key={ i } value={ i }>
{{
default: props => <VCard color="grey" width="50" height="100" class={ ['ma-4', props.selectedClass, `item-${i}`] }>{ i }</VCard>,
}}
</VSlideGroupItem>
))}
</VSlideGroup>
</CenteredGrid>
</Application>
))

cy.get('.item-7').should('exist').should('not.be.visible')

cy.get('.v-slide-group__prev--disabled').should('exist')
cy.get('.v-slide-group__next--disabled').should('not.exist')

cy.get('.v-slide-group__next').click().click()

cy.get('.item-7').should('exist').should('be.visible')
})
})
20 changes: 7 additions & 13 deletions packages/vuetify/src/components/VSlideGroup/helpers.ts
Expand Up @@ -21,11 +21,7 @@ export function calculateUpdatedOffset ({
}): number {
const clientSize = isHorizontal ? selectedElement.clientWidth : selectedElement.clientHeight
const offsetStart = isHorizontal ? selectedElement.offsetLeft : selectedElement.offsetTop
const adjustedOffsetStart = isRtl ? (contentSize - offsetStart - clientSize) : offsetStart

if (isRtl) {
currentScrollOffset = -currentScrollOffset
}
const adjustedOffsetStart = isRtl && isHorizontal ? (contentSize - offsetStart - clientSize) : offsetStart

const totalSize = containerSize + currentScrollOffset
const itemOffset = clientSize + adjustedOffsetStart
Expand All @@ -37,7 +33,7 @@ export function calculateUpdatedOffset ({
currentScrollOffset = Math.min(currentScrollOffset - (totalSize - itemOffset - additionalOffset), contentSize - containerSize)
}

return isRtl ? -currentScrollOffset : currentScrollOffset
return currentScrollOffset
}

export function calculateCenteredOffset ({
Expand All @@ -56,11 +52,9 @@ export function calculateCenteredOffset ({
const clientSize = isHorizontal ? selectedElement.clientWidth : selectedElement.clientHeight
const offsetStart = isHorizontal ? selectedElement.offsetLeft : selectedElement.offsetTop

if (isRtl) {
const offsetCentered = contentSize - offsetStart - clientSize / 2 - containerSize / 2
return -Math.min(contentSize - containerSize, Math.max(0, offsetCentered))
} else {
const offsetCentered = offsetStart + clientSize / 2 - containerSize / 2
return Math.min(contentSize - containerSize, Math.max(0, offsetCentered))
}
const offsetCentered = isRtl && isHorizontal
? contentSize - offsetStart - clientSize / 2 - containerSize / 2
: offsetStart + clientSize / 2 - containerSize / 2

return Math.min(contentSize - containerSize, Math.max(0, offsetCentered))
}
4 changes: 3 additions & 1 deletion packages/vuetify/src/components/VTabs/VTab.sass
Expand Up @@ -2,9 +2,11 @@

.v-tab
position: relative
max-width: $tab-max-width
min-width: $tab-min-width

.v-slide-group--horizontal &
max-width: $tab-max-width

// override density specificity
&.v-tab.v-tab
height: var(--v-tabs-height)
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/components/VTabs/VTabs.sass
Expand Up @@ -46,7 +46,7 @@
margin-inline-end: 0

@media #{map-get(settings.$display-breakpoints, 'md-and-down')}
.v-tabs.v-slide-group--is-overflowing:not(.v-slide-group--has-affixes)
.v-tabs.v-slide-group--is-overflowing.v-slide-group--horizontal:not(.v-slide-group--has-affixes)
.v-tab:first-child
margin-inline-start: 52px
.v-tab:last-child
Expand Down