Skip to content

Commit 01c9e91

Browse files
authored
feat: respect prefers-reduced-motion (#21530)
closes #19622
1 parent be452a5 commit 01c9e91

File tree

26 files changed

+386
-299
lines changed

26 files changed

+386
-299
lines changed

packages/docs/src/examples/v-list/usage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
5555
const code = computed(() => {
5656
return `<${name}${propsToString(props.value)}>
57-
<v-list-item${propsToString(itemProps.value, 2)}></v-list-item>
57+
<v-list-item${propsToString(itemProps.value, [], 2)}></v-list-item>
5858
</${name}>`
5959
})
6060
</script>

packages/vuetify/src/components/VBottomSheet/VBottomSheet.sass

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030

3131
@include tools.elevation($bottom-sheet-elevation)
3232

33+
@media (prefers-reduced-motion: reduce)
34+
transition: none
35+
3336
> .v-card,
3437
> .v-sheet
3538
border-radius: $bottom-sheet-border-radius

packages/vuetify/src/components/VExpansionPanel/VExpansionPanel.sass

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@
7878
transition-property: margin-top, border-radius, border, max-width
7979
border-radius: $expansion-panel-border-radius
8080

81+
@media (prefers-reduced-motion: reduce)
82+
transition-property: border-radius, border
83+
8184
&:not(:first-child)::after
8285
border-top-style: solid
8386
border-top-width: thin
@@ -124,10 +127,12 @@
124127
outline: none
125128
padding: $expansion-panel-title-padding
126129
position: relative
127-
transition: .3s min-height settings.$standard-easing
128130
width: 100%
129131
justify-content: space-between
130132

133+
@media (prefers-reduced-motion: no-preference)
134+
transition: .3s min-height settings.$standard-easing
135+
131136
@include tools.states('.v-expansion-panel-title__overlay', false)
132137

133138
&--focusable.v-expansion-panel-title--active

packages/vuetify/src/components/VField/VField.sass

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@
230230
transition: $field-transition-timing
231231
transition-property: opacity, transform, width
232232

233+
@media (prefers-reduced-motion: reduce)
234+
transition-property: opacity
235+
233236
.v-field--focused &,
234237
.v-field--persistent-clear &
235238
opacity: 1
@@ -253,10 +256,12 @@
253256
position: absolute
254257
top: var(--v-input-padding-top)
255258
transform-origin: left center
256-
transition: $field-transition-timing
257-
transition-property: opacity, transform
258259
z-index: 1
259260

261+
@media (prefers-reduced-motion: no-preference)
262+
transition: $field-transition-timing
263+
transition-property: opacity, transform
264+
260265
.v-field--variant-underlined &,
261266
.v-field--variant-plain &
262267
top: calc(var(--v-input-padding-top) + var(--v-field-padding-top))
@@ -373,7 +378,9 @@
373378
&__end
374379
border: 0 solid currentColor
375380
opacity: var(--v-field-border-opacity)
376-
transition: opacity $field-subtle-transition-timing
381+
382+
@media (prefers-reduced-motion: no-preference)
383+
transition: opacity $field-subtle-transition-timing
377384

378385
&__start
379386
flex: 0 0 $field-control-affixed-padding
@@ -413,7 +420,6 @@
413420
&::before,
414421
&::after
415422
opacity: var(--v-field-border-opacity)
416-
transition: opacity $field-subtle-transition-timing
417423

418424
@include tools.absolute(true)
419425

packages/vuetify/src/components/VField/VField.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
EventProp,
2626
genericComponent,
2727
nullifyTransforms,
28+
PREFERS_REDUCED_MOTION,
2829
propsFactory,
2930
standardEasing,
3031
useRender,
@@ -163,7 +164,7 @@ export const VField = genericComponent<new <T>(
163164
const { textColorClasses, textColorStyles } = useTextColor(color)
164165

165166
watch(isActive, val => {
166-
if (hasFloatingLabel.value) {
167+
if (hasFloatingLabel.value && !PREFERS_REDUCED_MOTION()) {
167168
const el: HTMLElement = labelRef.value!.$el
168169
const targetEl: HTMLElement = floatingLabelRef.value!.$el
169170

packages/vuetify/src/components/VMain/VMain.sass

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
padding-top: var(--v-layout-top)
1212
padding-bottom: var(--v-layout-bottom)
1313

14+
@media (prefers-reduced-motion: reduce)
15+
transition: none
16+
1417
&__scroller
1518
max-width: 100%
1619
position: relative

packages/vuetify/src/components/VNavigationDrawer/VNavigationDrawer.sass

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
@include tools.rounded($navigation-drawer-border-radius)
2121
@include tools.theme($navigation-drawer-theme...)
2222

23+
@media (prefers-reduced-motion: reduce)
24+
transition: none
25+
2326
&--rounded
2427
@include tools.rounded($navigation-drawer-rounded-border-radius)
2528

packages/vuetify/src/components/VParallax/VParallax.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useResizeObserver } from '@/composables/resizeObserver'
1212

1313
// Utilities
1414
import { computed, onBeforeUnmount, ref, watch, watchEffect } from 'vue'
15-
import { clamp, genericComponent, getScrollParent, propsFactory, useRender } from '@/util'
15+
import { clamp, genericComponent, getScrollParent, PREFERS_REDUCED_MOTION, propsFactory, useRender } from '@/util'
1616

1717
// Types
1818
import type { VImgSlots } from '@/components/VImg/VImg'
@@ -71,7 +71,7 @@ export const VParallax = genericComponent<VImgSlots>()({
7171

7272
let frame = -1
7373
function onScroll () {
74-
if (!isIntersecting.value) return
74+
if (!isIntersecting.value || PREFERS_REDUCED_MOTION()) return
7575

7676
cancelAnimationFrame(frame)
7777
frame = requestAnimationFrame(() => {

packages/vuetify/src/components/VProgressCircular/VProgressCircular.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { makeThemeProps, provideTheme } from '@/composables/theme'
1212

1313
// Utilities
1414
import { ref, toRef, watchEffect } from 'vue'
15-
import { clamp, convertToUnit, genericComponent, propsFactory, useRender } from '@/util'
15+
import { clamp, convertToUnit, genericComponent, PREFERS_REDUCED_MOTION, propsFactory, useRender } from '@/util'
1616

1717
// Types
1818
import type { PropType } from 'vue'
@@ -89,7 +89,8 @@ export const VProgressCircular = genericComponent<VProgressCircularSlots>()({
8989
{
9090
'v-progress-circular--indeterminate': !!props.indeterminate,
9191
'v-progress-circular--visible': isIntersecting.value,
92-
'v-progress-circular--disable-shrink': props.indeterminate === 'disable-shrink',
92+
'v-progress-circular--disable-shrink': props.indeterminate &&
93+
(props.indeterminate === 'disable-shrink' || PREFERS_REDUCED_MOTION()),
9394
},
9495
themeClasses.value,
9596
sizeClasses.value,

packages/vuetify/src/components/VSelect/__tests__/VSelect.spec.browser.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { VForm } from '@/components/VForm'
44
import { VListItem } from '@/components/VList'
55

66
// Utilities
7-
import { commands, generate, render, screen, userEvent } from '@test'
7+
import { commands, generate, render, screen, userEvent, waitForClickable } from '@test'
88
import { getAllByRole } from '@testing-library/vue'
99
import { cloneVNode, nextTick, ref } from 'vue'
1010

@@ -56,13 +56,11 @@ describe('VSelect', () => {
5656
expect(element).not.toHaveClass('v-select--active-menu')
5757

5858
await userEvent.click(menuIcon)
59-
await commands.waitStable('.v-list')
60-
expect(screen.queryAllByCSS('.v-list-item')).toHaveLength(2)
59+
await expect.poll(() => screen.queryAllByCSS('.v-list-item')).toHaveLength(2)
6160
expect(element).toHaveClass('v-select--active-menu')
6261

6362
await userEvent.click(menuIcon)
64-
await commands.waitStable('.v-list')
65-
expect(screen.queryAllByCSS('.v-list-item')).toHaveLength(0)
63+
await expect.poll(() => screen.queryAllByCSS('.v-list-item')).toHaveLength(0)
6664
expect(element).not.toHaveClass('v-select--active-menu')
6765
})
6866

@@ -153,7 +151,7 @@ describe('VSelect', () => {
153151
await expect(screen.findAllByRole('option', { selected: true })).resolves.toHaveLength(2)
154152

155153
const option = screen.getAllByRole('option')[2]
156-
await commands.waitStable('.v-list')
154+
await waitForClickable(option)
157155
await userEvent.click(option)
158156
expect(selectedItems.value).toStrictEqual(['California', 'Colorado', 'Florida'])
159157

@@ -204,8 +202,9 @@ describe('VSelect', () => {
204202

205203
await userEvent.click(element)
206204
await expect(screen.findAllByRole('option', { selected: true })).resolves.toHaveLength(2)
207-
await commands.waitStable('.v-list')
208-
await userEvent.click(screen.getAllByRole('option')[2])
205+
const option = screen.getAllByRole('option')[2]
206+
await waitForClickable(option)
207+
await userEvent.click(option)
209208
expect(selectedItems.value).toStrictEqual([
210209
{
211210
title: 'Item 1',
@@ -280,6 +279,7 @@ describe('VSelect', () => {
280279
expect(element).toHaveTextContent('Item 1')
281280
expect(element).toHaveTextContent('Item 2')
282281

282+
await waitForClickable(options[0])
283283
await userEvent.click(options[0])
284284
expect(selectedItems.value).toStrictEqual([{
285285
text: 'Item 2',
@@ -484,6 +484,7 @@ describe('VSelect', () => {
484484
expect(options).toHaveLength(2)
485485
expect(options[0]).toHaveTextContent('Item 2')
486486

487+
await waitForClickable(options[0])
487488
await userEvent.click(options[0])
488489
expect(selectedItem.value).toStrictEqual({ text: 'Item 2', id: 'item2' })
489490
expect(screen.queryAllByRole('option', { selected: true })).toHaveLength(0)

0 commit comments

Comments
 (0)