From 350f00cbf06e98f84e97836ec6c32b2fe9db00bf 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 f9d47e7cafb..4ecbbb6778b 100644 --- a/packages/docs/src/data/nav.json +++ b/packages/docs/src/data/nav.json @@ -228,6 +228,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 @@ - - - - - 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 @@ - - - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ + + + + + 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 @@ + + + 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 f851fbb036e..68f0332a777 100644 --- a/packages/vuetify/src/composables/layout.ts +++ b/packages/vuetify/src/composables/layout.ts @@ -19,7 +19,7 @@ import { convertToUnit, eagerComputed, findChildrenWithProvide, getCurrentInstan // 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'