From dda9a7bb9e8ca0b926951c144bda25c83eaf463c Mon Sep 17 00:00:00 2001 From: J-Sek Date: Sun, 31 May 2026 01:09:36 +0200 Subject: [PATCH] feat(VSwitch): add `size` prop --- .../api-generator/src/locale/en/VSwitch.json | 5 +- packages/docs/src/data/new-in.json | 4 + .../src/examples/v-switch/prop-colors.vue | 191 +++++++----------- .../docs/src/examples/v-switch/prop-flat.vue | 24 --- .../docs/src/examples/v-switch/prop-inset.vue | 30 --- .../src/examples/v-switch/prop-states.vue | 65 ++++++ packages/docs/src/examples/v-switch/usage.vue | 102 +++++++++- .../docs/src/pages/en/components/switches.md | 12 -- .../src/components/VSwitch/VSwitch.sass | 39 ++-- .../src/components/VSwitch/VSwitch.tsx | 28 ++- .../__tests__/VSwitch.spec.browser.tsx | 25 ++- .../src/components/VSwitch/_variables.scss | 16 ++ 12 files changed, 335 insertions(+), 206 deletions(-) delete mode 100644 packages/docs/src/examples/v-switch/prop-flat.vue delete mode 100644 packages/docs/src/examples/v-switch/prop-inset.vue diff --git a/packages/api-generator/src/locale/en/VSwitch.json b/packages/api-generator/src/locale/en/VSwitch.json index b9de3b611e4..231f8d14138 100644 --- a/packages/api-generator/src/locale/en/VSwitch.json +++ b/packages/api-generator/src/locale/en/VSwitch.json @@ -3,9 +3,10 @@ "props": { "flat": "Display component without elevation. Default elevation for thumb is 4dp, `flat` resets it.", "indeterminate": "Sets an indeterminate state for the switch.", - "inset": "Controls the track and thumb styling\n- **tonal** (or `true`) enlarges the track to encompass the thumb\n- **material** applies the Material Design 3 treatment: an outlined track that fills when on and a thumb that morphs between states\n- **square** is the **material** variant with less round corners.", + "inset": "Controls the track and thumb styling\n- **tonal** (or `true`) enlarges the track to encompass the thumb\n- **material** applies the Material Design 3 treatment: an outlined track that fills when on and a thumb that morphs between states\n- **square** is the **material** variant with less round corners.\n\nNon-boolean values were introduced in v4.1.0.", "loading": "Displays circular progress bar. Can either be a String which specifies which color is applied to the progress bar (any material color or theme color - primary, secondary, success, info, warning, error) or a Boolean which uses the component color (set by color prop - if it's supported by the component) or the primary color.", - "multiple": "Changes expected model to an array." + "multiple": "Changes expected model to an array.", + "size": "Scales the track and thumb. Accepts the predefined sizes **x-small**, **small**, **default**, **large**, and **x-large**, or a numeric value for a custom scale." }, "events": { "update:indeterminate": "Event that is emitted when the component's indeterminate state changes." diff --git a/packages/docs/src/data/new-in.json b/packages/docs/src/data/new-in.json index 63a04208188..aabca09c81a 100644 --- a/packages/docs/src/data/new-in.json +++ b/packages/docs/src/data/new-in.json @@ -526,6 +526,10 @@ } }, "VSwitch": { + "props": { + "size": "4.1.0", + "thumbColor": "4.1.0" + }, "slots": { "thumb": "3.5.0", "track-false": "3.5.0", diff --git a/packages/docs/src/examples/v-switch/prop-colors.vue b/packages/docs/src/examples/v-switch/prop-colors.vue index cb2dfe5f201..a25a199bb96 100644 --- a/packages/docs/src/examples/v-switch/prop-colors.vue +++ b/packages/docs/src/examples/v-switch/prop-colors.vue @@ -1,129 +1,47 @@ diff --git a/packages/docs/src/examples/v-switch/prop-flat.vue b/packages/docs/src/examples/v-switch/prop-flat.vue deleted file mode 100644 index 661d91a1656..00000000000 --- a/packages/docs/src/examples/v-switch/prop-flat.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - - - diff --git a/packages/docs/src/examples/v-switch/prop-inset.vue b/packages/docs/src/examples/v-switch/prop-inset.vue deleted file mode 100644 index 78e3e43413d..00000000000 --- a/packages/docs/src/examples/v-switch/prop-inset.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - { - "figma": "https://www.figma.com/design/5f4g4pbbBsk9TTWX4Xvlx1/PRO-v3.0---Official-Vuetify-3-UI-Kit?node-id=2723-45692&t=tC3y53U3XKPv8ZyJ-4" - } - diff --git a/packages/docs/src/examples/v-switch/prop-states.vue b/packages/docs/src/examples/v-switch/prop-states.vue index 0e75326f1b5..a18c11eb749 100644 --- a/packages/docs/src/examples/v-switch/prop-states.vue +++ b/packages/docs/src/examples/v-switch/prop-states.vue @@ -1,8 +1,29 @@ + + + + diff --git a/packages/docs/src/examples/v-switch/usage.vue b/packages/docs/src/examples/v-switch/usage.vue index 9f99a872006..af8179ea8c7 100644 --- a/packages/docs/src/examples/v-switch/usage.vue +++ b/packages/docs/src/examples/v-switch/usage.vue @@ -5,12 +5,78 @@ :name="name" :options="options" > -
+
+ + + +
@@ -19,12 +85,40 @@ const name = 'v-switch' const model = ref('default') const indeterminate = ref(false) - const options = ['inset'] + const icons = ref(true) + const loading = ref(false) + const options = ['inset', 'material', 'square'] + + const colors = ['primary', '#ac46ff', 'orange-darken-2'] + const color = ref() + const baseColor = ref() + const thumbColor = ref() + + const sizes = ['x-small', 'small', 'default', 'large', 'x-large'] + const sizeIndex = ref(2) + const sizeTicks = { 0: 'xs', 1: 'sm', 2: 'md', 3: 'lg', 4: 'xl' } + const size = computed(() => sizes[sizeIndex.value]) + + const requiresLatest = computed(() => { + return size.value !== 'default' || + ['material', 'square'].includes(model.value) || + !!thumbColor.value + }) + const props = computed(() => { return { label: 'Switch', - inset: model.value === 'inset' || undefined, + inset: model.value === 'default' ? undefined + : model.value === 'inset' ? true + : model.value, + size: size.value === 'default' ? undefined : size.value, + 'true-icon': icons.value ? 'mdi-check' : undefined, + 'false-icon': icons.value ? 'mdi-close' : undefined, + color: color.value || undefined, + 'base-color': baseColor.value || undefined, + 'thumb-color': thumbColor.value || undefined, indeterminate: indeterminate.value || undefined, + loading: loading.value || undefined, } }) diff --git a/packages/docs/src/pages/en/components/switches.md b/packages/docs/src/pages/en/components/switches.md index dff5bc8b185..8f7974e2743 100644 --- a/packages/docs/src/pages/en/components/switches.md +++ b/packages/docs/src/pages/en/components/switches.md @@ -47,18 +47,6 @@ Switches can be colored by using any of the builtin colors and contextual names - - -#### Inset - -You can make switch render in inset mode. - - - #### Model as array Multiple `v-switch`'s can share the same **v-model** by using an array. diff --git a/packages/vuetify/src/components/VSwitch/VSwitch.sass b/packages/vuetify/src/components/VSwitch/VSwitch.sass index 4286b5c1f87..ca7b2e13552 100644 --- a/packages/vuetify/src/components/VSwitch/VSwitch.sass +++ b/packages/vuetify/src/components/VSwitch/VSwitch.sass @@ -1,3 +1,5 @@ +@use 'sass:list' +@use 'sass:math' @use 'sass:selector' @use '../../styles/settings' @use '../../styles/tools' @@ -5,9 +7,25 @@ @include tools.layer('components') .v-switch + --v-switch-scale: 1 + .v-label padding-inline-start: $switch-label-margin-inline-start + @each $name, $size in $switch-sizes + .v-switch--size-#{$name} + --v-switch-scale: #{math.div(list.nth($size, 1), $switch-track-height)} + --v-switch-track-height: #{list.nth($size, 1)} + --v-switch-thumb-height: #{list.nth($size, 2)} + --v-switch-thumb-width: #{list.nth($size, 3)} + + @each $name, $size in $switch-inset-sizes + .v-switch--inset.v-switch--size-#{$name} + --v-switch-scale: #{math.div(list.nth($size, 1), $switch-inset-track-height)} + --v-switch-track-height: #{list.nth($size, 1)} + --v-switch-thumb-height: #{list.nth($size, 2)} + --v-switch-thumb-width: #{list.nth($size, 3)} + .v-switch__loader display: flex @@ -41,17 +59,16 @@ padding: 0 5px background-color: $switch-track-background border-radius: $switch-track-radius - height: $switch-track-height + height: var(--v-switch-track-height) opacity: $switch-track-opacity - min-width: $switch-track-width + min-width: calc(#{$switch-track-width} * var(--v-switch-scale)) cursor: pointer transition: $switch-track-transition .v-switch--inset & border-radius: $switch-inset-track-border-radius font-size: .75rem - height: $switch-inset-track-height - min-width: $switch-inset-track-width + min-width: calc(#{$switch-inset-track-width} * var(--v-switch-scale)) .v-switch__thumb align-items: center @@ -60,9 +77,9 @@ border-radius: $switch-thumb-radius display: flex font-size: .75rem - height: $switch-thumb-height + height: var(--v-switch-thumb-height) justify-content: center - width: $switch-thumb-width + width: var(--v-switch-thumb-width) pointer-events: none transition: $switch-thumb-transition position: relative @@ -78,8 +95,6 @@ @include tools.elevation(0) .v-switch--inset & - height: $switch-inset-thumb-height - width: $switch-inset-thumb-width transform: scale(var(--v-switch-inset-thumb-off-scale, #{$switch-inset-thumb-off-scale})) &--filled @@ -175,9 +190,9 @@ width: calc(var(--v-switch-thumb-height) * 1.666666667) @include tools.ltr() - transform: translateX(-$switch-thumb-transform) + transform: translateX(calc(#{-$switch-thumb-transform} * var(--v-switch-scale))) @include tools.rtl() - transform: translateX($switch-thumb-transform) + transform: translateX(calc(#{$switch-thumb-transform} * var(--v-switch-scale))) .v-icon position: absolute @@ -188,9 +203,9 @@ .v-selection-control--dirty .v-selection-control__input @include tools.ltr() - transform: translateX($switch-thumb-transform) + transform: translateX(calc(#{$switch-thumb-transform} * var(--v-switch-scale))) @include tools.rtl() - transform: translateX(-$switch-thumb-transform) + transform: translateX(calc(#{-$switch-thumb-transform} * var(--v-switch-scale))) &.v-switch--indeterminate .v-selection-control__input diff --git a/packages/vuetify/src/components/VSwitch/VSwitch.tsx b/packages/vuetify/src/components/VSwitch/VSwitch.tsx index 5df664a984d..e53187b6128 100644 --- a/packages/vuetify/src/components/VSwitch/VSwitch.tsx +++ b/packages/vuetify/src/components/VSwitch/VSwitch.tsx @@ -15,6 +15,7 @@ import { useFocus } from '@/composables/focus' import { forwardRefs } from '@/composables/forwardRefs' import { LoaderSlot, useLoader } from '@/composables/loader' import { useProxiedModel } from '@/composables/proxiedModel' +import { makeSizeProps } from '@/composables/size' // Utilities import { ref, toRef, useId } from 'vue' @@ -57,8 +58,18 @@ export const makeVSwitchProps = propsFactory({ ...omit(makeVInputProps(), ['glow']), ...makeVSelectionControlProps(), + ...makeSizeProps(), }, 'VSwitch') +const predefinedSizes = ['x-small', 'small', 'default', 'large', 'x-large'] +const iconSizes: Record = { + 'x-small': 11, + small: 14, + default: 16, + large: 18, + 'x-large': 22, +} + export const VSwitch = genericComponent( props: { modelValue?: T | null @@ -100,6 +111,11 @@ export const VSwitch = genericComponent( const uid = useId() const id = toRef(() => props.id || `switch-${uid}`) + const isPredefinedSize = toRef(() => predefinedSizes.includes(props.size as string)) + const iconSize = toRef(() => { + return isPredefinedSize.value ? iconSizes[props.size as string] : Math.round(16 * Number(props.size) / 32) + }) + function onChange () { if (indeterminate.value) { indeterminate.value = false @@ -128,6 +144,7 @@ export const VSwitch = genericComponent( { 'v-switch--inset-material': isMaterial }, { 'v-switch--inset-square': props.inset === 'square' }, { 'v-switch--indeterminate': indeterminate.value }, + isPredefinedSize.value ? `v-switch--size-${props.size}` : undefined, loaderClasses.value, props.class, ]} @@ -136,7 +153,10 @@ export const VSwitch = genericComponent( v-model={ model.value } id={ id.value } focused={ isFocused.value } - style={ props.style } + style={[ + { '--v-switch-scale': isPredefinedSize.value ? undefined : Number(props.size) / 32 }, + props.style, + ]} > {{ ...slots, @@ -228,7 +248,7 @@ export const VSwitch = genericComponent( defaults={{ VIcon: { icon, - size: isMaterial ? 16 : 'x-small', + size: isMaterial ? iconSize.value : 'x-small', }, }} > @@ -243,7 +263,7 @@ export const VSwitch = genericComponent( class={ isMaterial ? textColorClasses.value : undefined } style={ isMaterial ? textColorStyles.value : undefined } icon={ icon } - size={ isMaterial ? 16 : 'x-small' } + size={ isMaterial ? iconSize.value : 'x-small' } /> ))) : ( ( active={ slotProps.isActive } color={ slotProps.color } indeterminate - size="16" + size={ iconSize.value } width="2" /> ) diff --git a/packages/vuetify/src/components/VSwitch/__tests__/VSwitch.spec.browser.tsx b/packages/vuetify/src/components/VSwitch/__tests__/VSwitch.spec.browser.tsx index c60d75ca635..0d20f01aedc 100644 --- a/packages/vuetify/src/components/VSwitch/__tests__/VSwitch.spec.browser.tsx +++ b/packages/vuetify/src/components/VSwitch/__tests__/VSwitch.spec.browser.tsx @@ -5,6 +5,9 @@ import { gridOn, showcase } from '@test' const contextColor = 'rgb(0, 0, 255)' const color = 'rgb(255, 0, 0)' +const thumbColor = 'rgb(0, 255, 0)' +const sizes = ['x-small', 'small', 'default', 'large', 'x-large'] as const + const stories = { 'Explicit color': gridOn([undefined], [true, false], (_, active) => (
@@ -19,10 +22,30 @@ const stories = { 'No color': gridOn([undefined], [true, false], (_, active) => ( )), + 'Inset tonal': gridOn([color, undefined], [true, false], (color, active) => ( + + )), + 'Inset material': gridOn([color, undefined], [true, false], (color, active) => ( + + )), + 'Inset square': gridOn([color, undefined], [true, false], (color, active) => ( + + )), + 'Icons (no color)': gridOn([false, 'material'] as const, [true, false], (inset, active) => ( + + )), + 'Icons (color)': gridOn([false, 'tonal', 'material'] as const, [true, false], (inset, active) => ( + + )), + 'Thumb color': gridOn([false, 'tonal', 'material'] as const, [true, false], (inset, active) => ( + + )), + Sizes: gridOn(sizes, [true, false], (size, active) => ( + + )), } const props = { loading: [true], - inset: [true], indeterminate: [true], } diff --git a/packages/vuetify/src/components/VSwitch/_variables.scss b/packages/vuetify/src/components/VSwitch/_variables.scss index a8a0eca8aa9..6436f271e09 100644 --- a/packages/vuetify/src/components/VSwitch/_variables.scss +++ b/packages/vuetify/src/components/VSwitch/_variables.scss @@ -59,3 +59,19 @@ $switch-track-width: 36px !default; $switch-track-height: 14px !default; $switch-track-opacity: .6 !default; $switch-track-transition: .2s background-color settings.$standard-easing !default; + +$switch-sizes: ( + 'x-small': ($switch-track-height - 5px, 13px, 13px), + 'small': ($switch-track-height - 3px, 16px, 16px), + 'default': ($switch-track-height, $switch-thumb-height, $switch-thumb-width), + 'large': ($switch-track-height + 3px, 24px, 24px), + 'x-large': ($switch-track-height + 5px, 28px, 28px), +) !default; + +$switch-inset-sizes: ( + 'x-small': ($switch-inset-track-height - 12px, 15px, 15px), + 'small': ($switch-inset-track-height - 6px, 20px, 20px), + 'default': ($switch-inset-track-height, $switch-inset-thumb-height, $switch-inset-thumb-width), + 'large': ($switch-inset-track-height + 6px, 30px, 30px), + 'x-large': ($switch-inset-track-height + 12px, 34px, 34px), +) !default; \ No newline at end of file