From 86d0c32260b67995e034dc832846a20e91bd7ebf Mon Sep 17 00:00:00 2001
From: John Leider <9064066+johnleider@users.noreply.github.com>
Date: Thu, 29 Feb 2024 13:42:51 -0600
Subject: [PATCH] feat(VFab): add new component (#18083)
fixes #1852
fixes #2553
fixes #7407
---
.../api-generator/src/locale/en/VFab.json | 9 +
packages/docs/src/data/nav.json | 4 +
packages/docs/src/data/page-to-api.json | 1 +
.../v-btn-fab/misc-display-animation.vue | 70 --------
.../v-btn-fab/misc-lateral-screens.vue | 106 -----------
.../src/examples/v-btn-fab/misc-small.vue | 168 ------------------
.../docs/src/examples/v-btn-fab/usage.vue | 75 --------
.../examples/v-fab/misc-display-animation.vue | 61 +++++++
.../examples/v-fab/misc-lateral-screens.vue | 87 +++++++++
.../docs/src/examples/v-fab/misc-small.vue | 123 +++++++++++++
.../{v-btn-fab => v-fab}/misc-speed-dial.vue | 0
packages/docs/src/examples/v-fab/usage.vue | 51 ++++++
packages/docs/src/pages/en/components/all.md | 6 +
.../en/components/floating-action-buttons.md | 66 ++++---
.../src/components/VToolbar/VToolbar.sass | 3 +-
packages/vuetify/src/composables/layout.ts | 2 +-
packages/vuetify/src/labs/VFab/VFab.sass | 82 +++++++++
packages/vuetify/src/labs/VFab/VFab.tsx | 145 +++++++++++++++
packages/vuetify/src/labs/VFab/_mixins.scss | 22 +++
.../vuetify/src/labs/VFab/_variables.scss | 33 ++++
packages/vuetify/src/labs/VFab/index.ts | 1 +
packages/vuetify/src/labs/components.ts | 1 +
22 files changed, 673 insertions(+), 443 deletions(-)
create mode 100644 packages/api-generator/src/locale/en/VFab.json
delete mode 100644 packages/docs/src/examples/v-btn-fab/misc-display-animation.vue
delete mode 100644 packages/docs/src/examples/v-btn-fab/misc-lateral-screens.vue
delete mode 100644 packages/docs/src/examples/v-btn-fab/misc-small.vue
delete mode 100644 packages/docs/src/examples/v-btn-fab/usage.vue
create mode 100644 packages/docs/src/examples/v-fab/misc-display-animation.vue
create mode 100644 packages/docs/src/examples/v-fab/misc-lateral-screens.vue
create mode 100644 packages/docs/src/examples/v-fab/misc-small.vue
rename packages/docs/src/examples/{v-btn-fab => v-fab}/misc-speed-dial.vue (100%)
create mode 100644 packages/docs/src/examples/v-fab/usage.vue
create mode 100644 packages/vuetify/src/labs/VFab/VFab.sass
create mode 100644 packages/vuetify/src/labs/VFab/VFab.tsx
create mode 100644 packages/vuetify/src/labs/VFab/_mixins.scss
create mode 100644 packages/vuetify/src/labs/VFab/_variables.scss
create mode 100644 packages/vuetify/src/labs/VFab/index.ts
diff --git a/packages/api-generator/src/locale/en/VFab.json b/packages/api-generator/src/locale/en/VFab.json
new file mode 100644
index 00000000000..d40545ee82b
--- /dev/null
+++ b/packages/api-generator/src/locale/en/VFab.json
@@ -0,0 +1,9 @@
+{
+ "props": {
+ "app": "If true, attaches to the closest layout and positions according to the value of **location**.",
+ "appear": "Used to control the animation of the FAB.",
+ "extended": "An alternate style for the FAB that expects text.",
+ "location": "The location of the fab relative to the layout. Only works when using **app**.",
+ "offset": "Translates the Fab up or down, depending on if location is set to **top** or **bottom**."
+ }
+}
diff --git a/packages/docs/src/data/nav.json b/packages/docs/src/data/nav.json
index f54f319fc12..94496499f9c 100644
--- a/packages/docs/src/data/nav.json
+++ b/packages/docs/src/data/nav.json
@@ -227,6 +227,10 @@
"title": "empty-states",
"subfolder": "components"
},
+ {
+ "title": "floating-action-buttons",
+ "subfolder": "components"
+ },
{
"title": "sparklines",
"subfolder": "components"
diff --git a/packages/docs/src/data/page-to-api.json b/packages/docs/src/data/page-to-api.json
index a05b502371f..54f377f2521 100644
--- a/packages/docs/src/data/page-to-api.json
+++ b/packages/docs/src/data/page-to-api.json
@@ -81,6 +81,7 @@
"VExpansionPanelTitle"
],
"components/file-inputs": ["VFileInput"],
+ "components/floating-action-buttons": ["VFab"],
"components/footers": ["VFooter"],
"components/forms": ["VForm"],
"components/grids": ["VCol", "VContainer", "VRow", "VSpacer"],
diff --git a/packages/docs/src/examples/v-btn-fab/misc-display-animation.vue b/packages/docs/src/examples/v-btn-fab/misc-display-animation.vue
deleted file mode 100644
index cdd3d9edba9..00000000000
--- a/packages/docs/src/examples/v-btn-fab/misc-display-animation.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- mdi-plus
-
-
-
-
-
-
- {{ hidden ? 'Show' : 'Hide' }}
-
-
-
-
-
- mdi-plus
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/docs/src/examples/v-btn-fab/misc-lateral-screens.vue b/packages/docs/src/examples/v-btn-fab/misc-lateral-screens.vue
deleted file mode 100644
index b00a131c8be..00000000000
--- a/packages/docs/src/examples/v-btn-fab/misc-lateral-screens.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
- Page title
-
-
- mdi-magnify
-
-
- mdi-dots-vertical
-
-
-
-
- Item One
-
-
- Item Two
-
-
- Item Three
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ activeFab.icon }}
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/docs/src/examples/v-btn-fab/misc-small.vue b/packages/docs/src/examples/v-btn-fab/misc-small.vue
deleted file mode 100644
index 7e15abaa50c..00000000000
--- a/packages/docs/src/examples/v-btn-fab/misc-small.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-
-
-
-
-
-
-
- My files
-
-
-
- mdi-magnify
-
-
- mdi-view-module
-
-
-
- mdi-plus
-
-
-
-
-
- Folders
-
-
-
-
-
- {{ item.icon }}
-
-
-
-
- {{ item.title }}
-
- {{ item.subtitle }}
-
-
-
-
-
- mdi-information
-
-
-
-
-
-
-
-
- Files
-
-
-
-
-
- {{ item.icon }}
-
-
-
-
- {{ item.title }}
-
- {{ item.subtitle }}
-
-
-
-
-
- mdi-information
-
-
-
-
-
-
-
-
-
-
-
- * This doesn't actually save.
-
-
-
-
-
-
- Submit
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/docs/src/examples/v-btn-fab/usage.vue b/packages/docs/src/examples/v-btn-fab/usage.vue
deleted file mode 100644
index ad02bbfea84..00000000000
--- a/packages/docs/src/examples/v-btn-fab/usage.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
- $mdiVuetify
-
-
- Click Me
-
-
-
-
-
diff --git a/packages/docs/src/examples/v-fab/misc-display-animation.vue b/packages/docs/src/examples/v-fab/misc-display-animation.vue
new file mode 100644
index 00000000000..48ab3a2fcee
--- /dev/null
+++ b/packages/docs/src/examples/v-fab/misc-display-animation.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-fab/misc-lateral-screens.vue b/packages/docs/src/examples/v-fab/misc-lateral-screens.vue
new file mode 100644
index 00000000000..a57d7790387
--- /dev/null
+++ b/packages/docs/src/examples/v-fab/misc-lateral-screens.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ Page title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-fab/misc-small.vue b/packages/docs/src/examples/v-fab/misc-small.vue
new file mode 100644
index 00000000000..6d3e8cbd714
--- /dev/null
+++ b/packages/docs/src/examples/v-fab/misc-small.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+ My files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+ {{ item.subtitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+ {{ item.subtitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ * This doesn't actually save.
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-btn-fab/misc-speed-dial.vue b/packages/docs/src/examples/v-fab/misc-speed-dial.vue
similarity index 100%
rename from packages/docs/src/examples/v-btn-fab/misc-speed-dial.vue
rename to packages/docs/src/examples/v-fab/misc-speed-dial.vue
diff --git a/packages/docs/src/examples/v-fab/usage.vue b/packages/docs/src/examples/v-fab/usage.vue
new file mode 100644
index 00000000000..0c4715ff2bd
--- /dev/null
+++ b/packages/docs/src/examples/v-fab/usage.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/pages/en/components/all.md b/packages/docs/src/pages/en/components/all.md
index 9a4ea8f405f..8838230c280 100644
--- a/packages/docs/src/pages/en/components/all.md
+++ b/packages/docs/src/pages/en/components/all.md
@@ -114,6 +114,12 @@ Navigation components are used to navigate between different views or pages.
+
+
+ The floating action button is used for a promoted actions within an application
+
+
+
Navigation drawers contain primary application navigation links
diff --git a/packages/docs/src/pages/en/components/floating-action-buttons.md b/packages/docs/src/pages/en/components/floating-action-buttons.md
index e44c16e8228..976d834ccdc 100644
--- a/packages/docs/src/pages/en/components/floating-action-buttons.md
+++ b/packages/docs/src/pages/en/components/floating-action-buttons.md
@@ -1,6 +1,7 @@
---
-disabled: true
+emphasized: true
meta:
+ nav: Floating Action Buttons
title: FAB component
description: The floating action button (or FAB) component is a promoted action that is elevated above the UI or attached to an element such as a card.
keywords: floating action button, fab, vuetify fab component, vue fab component
@@ -8,17 +9,42 @@ related:
- /components/buttons/
- /components/icons/
- /styles/transitions/
+features:
+ report: true
+ spec: https://m2.material.io/components/buttons-floating-action-button
---
-# Buttons: Floating Action Button
+# Floating Action Buttons
-The `v-btn` component can be used as a floating action button. This provides an application with a main point of action. Combined with the `v-speed-dial` component, you can create a diverse set of functions available for your users.
+The `v-fab` component can be used as a floating action button. This provides an application with a main point of action.
+
+
+
+::: warning
+
+This feature requires [v3.5.7](/getting-started/release-notes/?version=v3.5.7)
+
+:::
+
+## Installation
+
+Labs components require a manual import and installation of the component.
+
+```js { resource="src/plugins/vuetify.js" }
+import { VFab } from 'vuetify/labs/VFab'
+
+export default createVuetify({
+ components: {
+ VFab,
+ },
+})
+```
## Usage
Floating action buttons can be attached to material to signify a promoted action in your application. The default size will be used in most cases, whereas the `small` variant can be used to maintain continuity with similar sized elements.
-
+
@@ -26,40 +52,36 @@ Floating action buttons can be attached to material to signify a promoted action
| Component | Description |
| - | - |
-| [v-btn](/api/v-btn/) | Primary Component |
+| [v-fab](/api/v-fab/) | Primary Component |
-
+The `v-fab` component has a multitude of props that allow you to customize its appearance and behavior. -->
## Examples
-### Misc
+The following are a collection of examples that demonstrate more advanced and real world use of the `v-fab` component.
-#### Display animation
+### Display animation
When displaying for the first time, a floating action button should animate onto the screen. Here we use the `v-fab-transition` with v-show. You can also use any custom transition provided by Vuetify or your own.
-
+
-#### Lateral screens
+### Lateral screens
-When changing the default action of your button, it is recommended that you display a transition to signify a change. We do this by binding the `key` prop to a piece of data that can properly signal a change in action to the Vue transition system. While you can use a custom transition for this, ensure that you set the `mode` prop to **out-in**.
+When changing the default action of your button, it is recommended that you display a transition to signify a change. We do this by binding the `key` prop to a piece of data that can properly signal a change in action to the Vue transition system.
-
+
-#### Small variant
+### Small variant
For better visual appeal, we use a small button to match our list avatars.
-
-
-#### Speed dial
-
-The speed-dial component has a very robust api for customizing your FAB experience exactly how you want.
-
-
+
diff --git a/packages/vuetify/src/components/VToolbar/VToolbar.sass b/packages/vuetify/src/components/VToolbar/VToolbar.sass
index 5bf9eb2f9c4..6c131c2598a 100644
--- a/packages/vuetify/src/components/VToolbar/VToolbar.sass
+++ b/packages/vuetify/src/components/VToolbar/VToolbar.sass
@@ -9,7 +9,6 @@
flex-direction: column
justify-content: space-between
max-width: 100%
- overflow: hidden
position: relative
transition: $toolbar-transition
transition-property: height, width, transform, max-width, left, right, top, bottom, box-shadow
@@ -50,6 +49,8 @@
width: 100%
.v-toolbar__content
+ overflow: hidden
+
> .v-btn:first-child
margin-inline-start: $toolbar-prepend-btn-margin-start
diff --git a/packages/vuetify/src/composables/layout.ts b/packages/vuetify/src/composables/layout.ts
index 1ca78909390..bed2ad145b8 100644
--- a/packages/vuetify/src/composables/layout.ts
+++ b/packages/vuetify/src/composables/layout.ts
@@ -19,7 +19,7 @@ import { convertToUnit, findChildrenWithProvide, getCurrentInstance, getUid, pro
// Types
import type { ComponentInternalInstance, CSSProperties, InjectionKey, Prop, Ref } from 'vue'
-type Position = 'top' | 'left' | 'right' | 'bottom'
+export type Position = 'top' | 'left' | 'right' | 'bottom'
interface Layer {
top: number
diff --git a/packages/vuetify/src/labs/VFab/VFab.sass b/packages/vuetify/src/labs/VFab/VFab.sass
new file mode 100644
index 00000000000..60261367da1
--- /dev/null
+++ b/packages/vuetify/src/labs/VFab/VFab.sass
@@ -0,0 +1,82 @@
+@use '../../styles/tools'
+@use '../../styles/settings'
+@use 'sass:math'
+@use 'sass:map'
+@use './variables' as *
+@use './mixins' as *
+
+.v-fab
+ align-items: center
+ display: inline-flex
+ flex: 1 1 auto
+ pointer-events: none
+ position: relative
+ transition-duration: $fab-transition-duration
+ transition-timing-function: $fab-transition-timing-function
+ vertical-align: middle
+
+ .v-btn
+ pointer-events: auto
+
+ &--variant-elevated
+ @include tools.elevation(3)
+
+ &--app,
+ &--absolute
+ display: flex
+
+ &--start,
+ &--left
+ justify-content: flex-start
+
+ &--center
+ align-items: center
+ justify-content: center
+
+ &--end,
+ &--right
+ justify-content: flex-end
+
+ &--bottom
+ align-items: flex-end
+
+ &--top
+ align-items: flex-start
+
+ &--extended
+ .v-btn
+ // min-height: 56px
+ // min-width: 80px
+ border-radius: 9999px !important
+
+.v-fab__container
+ align-self: center
+ display: inline-flex
+ vertical-align: middle
+
+ .v-fab--app &
+ margin: 4px
+
+ .v-fab--absolute &
+ position: absolute
+ z-index: 4
+
+ .v-fab--offset.v-fab--top &
+ transform: translateY(-50%)
+
+ .v-fab--offset.v-fab--bottom &
+ transform: translateY(50%)
+
+ .v-fab--top &
+ top: 0
+
+ .v-fab--bottom &
+ bottom: 0
+
+ .v-fab--left &,
+ .v-fab--start &
+ left: 0
+
+ .v-fab--right &,
+ .v-fab--end &
+ right: 0
diff --git a/packages/vuetify/src/labs/VFab/VFab.tsx b/packages/vuetify/src/labs/VFab/VFab.tsx
new file mode 100644
index 00000000000..dd2f84ae57a
--- /dev/null
+++ b/packages/vuetify/src/labs/VFab/VFab.tsx
@@ -0,0 +1,145 @@
+// Styles
+import './VFab.sass'
+
+// Components
+import { makeVBtnProps, VBtn } from '@/components/VBtn/VBtn'
+
+// Composables
+import { makeLayoutItemProps, useLayoutItem } from '@/composables/layout'
+import { useProxiedModel } from '@/composables/proxiedModel'
+import { useResizeObserver } from '@/composables/resizeObserver'
+import { useToggleScope } from '@/composables/toggleScope'
+import { makeTransitionProps, MaybeTransition } from '@/composables/transition'
+
+// Utilities
+import { computed, ref, shallowRef, toRef, watchEffect } from 'vue'
+import { genericComponent, omit, propsFactory, useRender } from '@/util'
+
+// Types
+import type { ComputedRef, PropType } from 'vue'
+import type { Position } from '@/composables/layout'
+
+const locations = ['start', 'end', 'left', 'right', 'top', 'bottom'] as const
+
+export const makeVFabProps = propsFactory({
+ app: Boolean,
+ appear: Boolean,
+ extended: Boolean,
+ location: {
+ type: String as PropType,
+ default: 'bottom end',
+ },
+ offset: Boolean,
+ modelValue: {
+ type: Boolean,
+ default: true,
+ },
+
+ ...omit(makeVBtnProps({ active: true }), ['location']),
+ ...makeLayoutItemProps(),
+ ...makeTransitionProps({ transition: 'fab-transition' }),
+}, 'VFab')
+
+export const VFab = genericComponent()({
+ name: 'VFab',
+
+ props: makeVFabProps(),
+
+ emits: {
+ 'update:modelValue': (value: boolean) => true,
+ },
+
+ setup (props, { slots }) {
+ const model = useProxiedModel(props, 'modelValue')
+ const height = shallowRef(56)
+ const layoutItemStyles = ref()
+
+ const { resizeRef } = useResizeObserver(entries => {
+ if (!entries.length) return
+ height.value = entries[0].target.clientHeight
+ })
+
+ const hasPosition = computed(() => props.app || props.absolute)
+
+ const position = computed(() => {
+ if (!hasPosition.value) return false
+
+ return props.location.split(' ').shift()
+ }) as ComputedRef
+
+ const orientation = computed(() => {
+ if (!hasPosition.value) return false
+
+ return props.location.split(' ')[1] ?? 'end'
+ })
+
+ useToggleScope(() => props.app, () => {
+ const layout = useLayoutItem({
+ id: props.name,
+ order: computed(() => parseInt(props.order, 10)),
+ position,
+ layoutSize: height,
+ elementSize: computed(() => height.value + 32),
+ active: computed(() => props.app && model.value),
+ absolute: toRef(props, 'absolute'),
+ })
+
+ watchEffect(() => {
+ layoutItemStyles.value = layout.layoutItemStyles.value
+ })
+ })
+
+ const vFabRef = ref()
+
+ useRender(() => {
+ const btnProps = VBtn.filterProps(props)
+
+ return (
+
+ )
+ })
+
+ return {}
+ },
+})
+
+export type VFab = InstanceType
diff --git a/packages/vuetify/src/labs/VFab/_mixins.scss b/packages/vuetify/src/labs/VFab/_mixins.scss
new file mode 100644
index 00000000000..46aab798656
--- /dev/null
+++ b/packages/vuetify/src/labs/VFab/_mixins.scss
@@ -0,0 +1,22 @@
+@use 'sass:math';
+@use 'sass:map';
+@use 'sass:meta';
+@use '../../styles/settings';
+@use '../../styles/tools';
+@use './variables' as *;
+
+@mixin fab-sizes ($map: $fab-sizes, $immediate: false) {
+ @each $sizeName, $multiplier in $fab-size-scales {
+ $size: map.get($map, 'font-size') + math.div(2 * $multiplier, 16);
+ $height: map.get($map, 'height') + (12px * $multiplier);
+
+ // .v-fab .v-btn--size-#{$sizeName} {
+ // --v-btn-size: #{$size};
+ // --v-btn-height: #{$height};
+ // }
+
+ .v-fab--bottom .v-btn--size-#{$sizeName} {
+ bottom: -1 * $height / 2;
+ }
+ }
+}
diff --git a/packages/vuetify/src/labs/VFab/_variables.scss b/packages/vuetify/src/labs/VFab/_variables.scss
new file mode 100644
index 00000000000..d5cb2732ddc
--- /dev/null
+++ b/packages/vuetify/src/labs/VFab/_variables.scss
@@ -0,0 +1,33 @@
+@use 'sass:math';
+@use 'sass:map';
+@use '../../styles/settings';
+@use '../../styles/tools';
+
+$fab-border-radius: map.get(settings.$rounded, 'circle') !default;
+$fab-border-radius-multiplier: 0 !default; // 2.4 for MD3
+$fab-height: 56px !default;
+$fab-font-size: tools.map-deep-get(settings.$typography, 'button', 'size') !default;
+$fab-font-weight: tools.map-deep-get(settings.$typography, 'button', 'weight') !default;
+$fab-transition-duration: 0.2s !default;
+$fab-transition-timing-function: settings.$standard-easing !default;
+$fab-width-ratio: math.div(16, 9) !default;
+$fab-padding-ratio: 2.25 !default;
+
+$fab-size-scales: (
+ 'x-small': -2,
+ 'small': -1,
+ 'default': 0,
+ 'large': 2,
+ 'x-large': 5
+) !default;
+
+$fab-sizes: () !default;
+$fab-sizes: map.merge(
+ (
+ 'height': $fab-height,
+ 'font-size': $fab-font-size,
+ 'width-ratio': $fab-width-ratio,
+ 'padding-ratio': $fab-padding-ratio
+ ),
+ $fab-sizes
+);
diff --git a/packages/vuetify/src/labs/VFab/index.ts b/packages/vuetify/src/labs/VFab/index.ts
new file mode 100644
index 00000000000..a2c3347dfea
--- /dev/null
+++ b/packages/vuetify/src/labs/VFab/index.ts
@@ -0,0 +1 @@
+export { VFab } from './VFab'
diff --git a/packages/vuetify/src/labs/components.ts b/packages/vuetify/src/labs/components.ts
index c0e9a648c6f..07c03b89c5e 100644
--- a/packages/vuetify/src/labs/components.ts
+++ b/packages/vuetify/src/labs/components.ts
@@ -1,5 +1,6 @@
export * from './VConfirmEdit'
export * from './VCalendar'
+export * from './VFab'
export * from './VPicker'
export * from './VSparkline'
export * from './VEmptyState'