diff --git a/packages/api-generator/src/locale/en/VRow.json b/packages/api-generator/src/locale/en/VRow.json index 0572fc964f2..de91049b4b8 100644 --- a/packages/api-generator/src/locale/en/VRow.json +++ b/packages/api-generator/src/locale/en/VRow.json @@ -21,7 +21,12 @@ "justifySm": "Deprecated, use **justify-sm-\\*** class", "justifyXl": "Deprecated, use **justify-xl-\\*** class", "justifyXxl": "Deprecated, use **justify-xxl-\\*** class", - "gap": "Sets the gap between `v-col`s. Can be a single value for both row and column gap, or the array of two values [column gap, row gap].", + "gap": "Gap between columns, expects value in pixels. One value for both axes, or [column gap, row gap].", + "gapSm": "Overrides **gap** from the **sm** breakpoint up, in pixels.", + "gapMd": "Overrides **gap** from the **md** breakpoint up, in pixels.", + "gapLg": "Overrides **gap** from the **lg** breakpoint up, in pixels.", + "gapXl": "Overrides **gap** from the **xl** breakpoint up, in pixels.", + "gapXxl": "Overrides **gap** from the **xxl** breakpoint up, in pixels.", "noGutters": "Removes the gutter between `v-col`s (same as `gap='0'`).", "size": "Sets the number of columns." } diff --git a/packages/docs/src/data/new-in.json b/packages/docs/src/data/new-in.json index a509bff1bc4..da7396d289b 100644 --- a/packages/docs/src/data/new-in.json +++ b/packages/docs/src/data/new-in.json @@ -516,6 +516,11 @@ }, "VRow": { "props": { + "gapSm": "4.3.0", + "gapMd": "4.3.0", + "gapLg": "4.3.0", + "gapXl": "4.3.0", + "gapXxl": "4.3.0", "size": "4.0.0" } }, diff --git a/packages/vuetify/src/components/VGrid/VGrid.sass b/packages/vuetify/src/components/VGrid/VGrid.sass index 2946a76184f..946e3311f5a 100644 --- a/packages/vuetify/src/components/VGrid/VGrid.sass +++ b/packages/vuetify/src/components/VGrid/VGrid.sass @@ -66,6 +66,11 @@ $false: ' ' margin-inline-start: calc($offset * (100% + $gap) / $offsetCols) @include make-grid-columns using ($infix) + // must stay after density and no-gutters rules to override them + .v-row--gap#{$infix} + --v-col-gap-x: var(--v-row-gap-x#{$infix}) + --v-col-gap-y: var(--v-row-gap-y#{$infix}) + @if $infix != '' .v-col-#{$infix} --v-col-is-size: #{$false} diff --git a/packages/vuetify/src/components/VGrid/VRow.ts b/packages/vuetify/src/components/VGrid/VRow.ts index 8a830092108..b6f8f587e92 100644 --- a/packages/vuetify/src/components/VGrid/VRow.ts +++ b/packages/vuetify/src/components/VGrid/VRow.ts @@ -4,6 +4,7 @@ import './VGrid.sass' // Composables import { makeComponentProps } from '@/composables/component' import { makeDensityProps } from '@/composables/density' +import { breakpoints } from '@/composables/display' import { makeTagProps } from '@/composables/tag' // Utilities @@ -32,6 +33,10 @@ const propMap = { alignContent: ['alignContent', 'alignContentSm', 'alignContentMd', 'alignContentLg', 'alignContentXl', 'alignContentXxl'], } +const gapProps = ['gap', 'gapSm', 'gapMd', 'gapLg', 'gapXl', 'gapXxl'] as const + +type Gap = number | string | (string | number)[] + const classMap = { align: 'align', justify: 'justify', @@ -94,7 +99,12 @@ export const makeVRowProps = propsFactory({ alignContentXxl: { type: String as PropType, default: null, validator: alignContentValidator }, noGutters: Boolean, - gap: [Number, String, Array] as PropType, + gap: [Number, String, Array] as PropType, + gapSm: [Number, String, Array] as PropType, + gapMd: [Number, String, Array] as PropType, + gapLg: [Number, String, Array] as PropType, + gapXl: [Number, String, Array] as PropType, + gapXxl: [Number, String, Array] as PropType, size: [Number, String], ...makeComponentProps(), ...makeDensityProps(), @@ -137,30 +147,30 @@ export const VRow = genericComponent()({ return classList }) - const horizontalGap = computed(() => { - return (Array.isArray(props.gap)) - ? convertToUnit(props.gap[0] || 0) - : convertToUnit(props.gap) - }) - - const verticalGap = computed(() => { - return (Array.isArray(props.gap)) - ? convertToUnit(props.gap[1] || 0) - : horizontalGap.value - }) + const gaps = computed(() => gapProps.flatMap((prop, i) => { + const value = props[prop] + if (value == null) return [] + const infix = i ? `-${breakpoints[i - 1]}` : '' + const [x, y] = Array.isArray(value) ? [value[0] || 0, value[1] || 0] : [value, value] + return [{ + class: `v-row--gap${infix}`, + style: { + [`--v-row-gap-x${infix}`]: convertToUnit(x), + [`--v-row-gap-y${infix}`]: convertToUnit(y), + }, + }] + })) return () => h(props.tag, { class: [ 'v-row', classes.value, + gaps.value.map(gap => gap.class), props.class, ], style: [ - { - '--v-col-gap-x': horizontalGap.value, - '--v-col-gap-y': verticalGap.value, - '--v-row-columns': props.size, - }, + { '--v-row-columns': props.size }, + gaps.value.map(gap => gap.style), props.style, ], }, slots.default?.()) diff --git a/packages/vuetify/src/components/VGrid/__tests__/VRow.spec.browser.tsx b/packages/vuetify/src/components/VGrid/__tests__/VRow.spec.browser.tsx new file mode 100644 index 00000000000..48e63f3e501 --- /dev/null +++ b/packages/vuetify/src/components/VGrid/__tests__/VRow.spec.browser.tsx @@ -0,0 +1,56 @@ +import { VRow } from '../VRow' +import { VCol } from '../VCol' + +// Utilities +import { page, render, screen } from '@test' + +describe('VRow', () => { + describe('responsive gap', () => { + function renderRow () { + render(() => ( + + + + + + )) + return [1, 2, 3].map(i => screen.getByTestId(`col-${i}`).getBoundingClientRect()) + } + + it('uses base gap below the breakpoint', async () => { + await page.viewport(500, 800) + const [first, second, third] = renderRow() + + expect(second.top).toBe(first.top) + expect(second.left - first.right).toBeCloseTo(8) + expect(third.top - first.bottom).toBeCloseTo(8) + }) + + it('switches gap at the breakpoint and keeps columns on one line', async () => { + await page.viewport(1400, 800) + const [first, second, third] = renderRow() + + expect(second.top).toBe(first.top) + expect(second.left - first.right).toBeCloseTo(32) + expect(third.top - first.bottom).toBeCloseTo(16) + }) + + it('does not leak into nested rows', async () => { + await page.viewport(1400, 800) + render(() => ( + + + + + + + + + )) + const first = screen.getByTestId('nested-1').getBoundingClientRect() + const second = screen.getByTestId('nested-2').getBoundingClientRect() + + expect(second.left - first.right).toBeCloseTo(24) + }) + }) +})