diff --git a/packages/docs/components.d.ts b/packages/docs/components.d.ts index c53f1629ae3..eb5a98f672b 100644 --- a/packages/docs/components.d.ts +++ b/packages/docs/components.d.ts @@ -23,6 +23,8 @@ declare module 'vue' { ApiSection: typeof import('./src/components/api/Section.vue')['default'] ApiSlotsTable: typeof import('./src/components/api/SlotsTable.vue')['default'] AppBackToTop: typeof import('./src/components/app/BackToTop.vue')['default'] + AppBanner: typeof import('./src/components/app/Banner.vue')['default'] + AppBarAuthDialog: typeof import('./src/components/app/bar/AuthDialog.vue')['default'] AppBarBar: typeof import('./src/components/app/bar/Bar.vue')['default'] AppBarEcosystemMenu: typeof import('./src/components/app/bar/EcosystemMenu.vue')['default'] AppBarEnterpriseLink: typeof import('./src/components/app/bar/EnterpriseLink.vue')['default'] @@ -76,6 +78,7 @@ declare module 'vue' { AppSettingsOptionsQuickbarOption: typeof import('./src/components/app/settings/options/QuickbarOption.vue')['default'] AppSettingsOptionsRailDrawerOption: typeof import('./src/components/app/settings/options/RailDrawerOption.vue')['default'] AppSettingsOptionsSlashSearchOption: typeof import('./src/components/app/settings/options/SlashSearchOption.vue')['default'] + AppSettingsOptionsSyncOption: typeof import('./src/components/app/settings/options/SyncOption.vue')['default'] AppSettingsOptionsThemeOption: typeof import('./src/components/app/settings/options/ThemeOption.vue')['default'] AppSettingsPerksOptions: typeof import('./src/components/app/settings/PerksOptions.vue')['default'] AppSettingsSettingsHeader: typeof import('./src/components/app/settings/SettingsHeader.vue')['default'] @@ -149,10 +152,16 @@ declare module 'vue' { SponsorCard: typeof import('./src/components/sponsor/Card.vue')['default'] SponsorLink: typeof import('./src/components/sponsor/Link.vue')['default'] SponsorSponsors: typeof import('./src/components/sponsor/Sponsors.vue')['default'] + UserAccountConnectedAccounts: typeof import('./src/components/user/account/ConnectedAccounts.vue')['default'] + UserAccountOneSubscription: typeof import('./src/components/user/account/OneSubscription.vue')['default'] UserBadgesUserAdminBadge: typeof import('./src/components/user/badges/UserAdminBadge.vue')['default'] UserBadgesUserOneBadge: typeof import('./src/components/user/badges/UserOneBadge.vue')['default'] UserBadgesUserSponsorBadge: typeof import('./src/components/user/badges/UserSponsorBadge.vue')['default'] + UserDiscordLogin: typeof import('./src/components/user/DiscordLogin.vue')['default'] + UserGithubLogin: typeof import('./src/components/user/GithubLogin.vue')['default'] UserOneSubCard: typeof import('./src/components/user/OneSubCard.vue')['default'] + UserUserBadges: typeof import('./src/components/user/UserBadges.vue')['default'] + UserUserProfile: typeof import('./src/components/user/UserProfile.vue')['default'] UserUserTabs: typeof import('./src/components/user/UserTabs.vue')['default'] } } diff --git a/packages/docs/src/examples/v-tabs/misc-content.vue b/packages/docs/src/examples/v-tabs/misc-content.vue index 8b826cbf07c..98cab1775c3 100644 --- a/packages/docs/src/examples/v-tabs/misc-content.vue +++ b/packages/docs/src/examples/v-tabs/misc-content.vue @@ -7,13 +7,9 @@ - - mdi-magnify - + - - mdi-dots-vertical - + - - + {{ text }} - - + + diff --git a/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue b/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue index a4dc590e6be..a2e0619037e 100644 --- a/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue +++ b/packages/docs/src/examples/v-tabs/misc-dynamic-height.vue @@ -2,7 +2,6 @@ @@ -24,9 +23,8 @@ - Item {{ n }} - + :text="`Item ${n}`" + > diff --git a/packages/docs/src/examples/v-tabs/misc-dynamic.vue b/packages/docs/src/examples/v-tabs/misc-dynamic.vue index cca6835ff4c..fe290dc018d 100644 --- a/packages/docs/src/examples/v-tabs/misc-dynamic.vue +++ b/packages/docs/src/examples/v-tabs/misc-dynamic.vue @@ -7,29 +7,29 @@ - Item {{ n }} - + > + - Remove Tab - + > + + - Add Tab - + > diff --git a/packages/docs/src/examples/v-tabs/misc-mobile.vue b/packages/docs/src/examples/v-tabs/misc-mobile.vue index 9d36535411f..3d9835c10c7 100644 --- a/packages/docs/src/examples/v-tabs/misc-mobile.vue +++ b/packages/docs/src/examples/v-tabs/misc-mobile.vue @@ -1,19 +1,15 @@ diff --git a/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue b/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue index c5d95d4a8de..93b537be551 100644 --- a/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue +++ b/packages/docs/src/examples/v-tabs/misc-overflow-to-menu.vue @@ -9,13 +9,9 @@ - - mdi-magnify - + - - mdi-dots-vertical - + - - + - - + + diff --git a/packages/docs/src/examples/v-tabs/misc-pagination.vue b/packages/docs/src/examples/v-tabs/misc-pagination.vue index c6a0f07403c..54311a3d4c0 100644 --- a/packages/docs/src/examples/v-tabs/misc-pagination.vue +++ b/packages/docs/src/examples/v-tabs/misc-pagination.vue @@ -8,10 +8,9 @@ - Item {{ i }} - + > diff --git a/packages/docs/src/examples/v-tabs/misc-tab-items.vue b/packages/docs/src/examples/v-tabs/misc-tab-items.vue index b9afa6164dc..3208dbfeefb 100644 --- a/packages/docs/src/examples/v-tabs/misc-tab-items.vue +++ b/packages/docs/src/examples/v-tabs/misc-tab-items.vue @@ -7,9 +7,8 @@ - {{ item.tab }} - + :title="item.tab" + > diff --git a/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue b/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue index 7d503eca5f0..f6c7b87386e 100644 --- a/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue +++ b/packages/docs/src/examples/v-tabs/prop-align-tabs-center.vue @@ -9,8 +9,9 @@ City Abstract - - + - - + + diff --git a/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue b/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue index 9d8e075ab1c..cdb081c8430 100644 --- a/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue +++ b/packages/docs/src/examples/v-tabs/prop-align-tabs-end.vue @@ -9,8 +9,9 @@ City Abstract - - + - - + + diff --git a/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue b/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue index 6cd501b2006..80a979eb1e8 100644 --- a/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue +++ b/packages/docs/src/examples/v-tabs/prop-align-tabs-title.vue @@ -7,13 +7,9 @@ - - mdi-magnify - + - - mdi-dots-vertical - + - - + - - + + diff --git a/packages/docs/src/examples/v-tabs/prop-direction.vue b/packages/docs/src/examples/v-tabs/prop-direction.vue index 29112515403..4b4c8dc2ec7 100644 --- a/packages/docs/src/examples/v-tabs/prop-direction.vue +++ b/packages/docs/src/examples/v-tabs/prop-direction.vue @@ -1,37 +1,21 @@ diff --git a/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue b/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue index 5aeece5c879..40c53637210 100644 --- a/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue +++ b/packages/docs/src/examples/v-tabs/prop-fixed-tabs.vue @@ -3,11 +3,8 @@ bg-color="indigo-darken-2" fixed-tabs > - - Option - - - Another Option - + + + diff --git a/packages/docs/src/examples/v-tabs/prop-grow.vue b/packages/docs/src/examples/v-tabs/prop-grow.vue index 6e015e5f11b..eec8625e747 100644 --- a/packages/docs/src/examples/v-tabs/prop-grow.vue +++ b/packages/docs/src/examples/v-tabs/prop-grow.vue @@ -15,14 +15,13 @@ - {{ item }} - + > - - + {{ text }} - - + + diff --git a/packages/docs/src/examples/v-tabs/prop-icons.vue b/packages/docs/src/examples/v-tabs/prop-icons.vue index 6219e73017a..3c5d10caaf2 100644 --- a/packages/docs/src/examples/v-tabs/prop-icons.vue +++ b/packages/docs/src/examples/v-tabs/prop-icons.vue @@ -9,9 +9,8 @@ - Item {{ i }} - + :text="`Item ${i}`" + > diff --git a/packages/docs/src/examples/v-tabs/prop-stacked.vue b/packages/docs/src/examples/v-tabs/prop-stacked.vue index 993f5edb995..db0ba0eafe0 100644 --- a/packages/docs/src/examples/v-tabs/prop-stacked.vue +++ b/packages/docs/src/examples/v-tabs/prop-stacked.vue @@ -7,23 +7,26 @@ stacked > - mdi-phone + + Recents - mdi-heart + + Favorites - mdi-account-box + + Nearby - - + {{ text }} - - + + diff --git a/packages/docs/src/examples/v-tabs/slot-tabs.vue b/packages/docs/src/examples/v-tabs/slot-tabs.vue new file mode 100644 index 00000000000..5888db54568 --- /dev/null +++ b/packages/docs/src/examples/v-tabs/slot-tabs.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/packages/docs/src/examples/v-tabs/usage.vue b/packages/docs/src/examples/v-tabs/usage.vue index d3e4df2f719..d3e8bf85626 100644 --- a/packages/docs/src/examples/v-tabs/usage.vue +++ b/packages/docs/src/examples/v-tabs/usage.vue @@ -10,19 +10,19 @@ - - + + One - + - + Two - + - + Three - - + + diff --git a/packages/docs/src/pages/en/components/tabs.md b/packages/docs/src/pages/en/components/tabs.md index f24ae4fcd10..75383cdfba1 100644 --- a/packages/docs/src/pages/en/components/tabs.md +++ b/packages/docs/src/pages/en/components/tabs.md @@ -124,3 +124,17 @@ Tabs can be dynamically added and removed. In this example when we add a new tab You can use a menu to hold additional tabs, swapping them out on the fly. + +### Slots + +#### Tab and window items + +Use the **tab** and **item** slots with the **items** prop to reduce the markup required to build tabs. + +::: success + +This feature was introduced in [v3.6.0 (Nebula)](/getting-started/release-notes/?version=v3.6.0) + +::: + + diff --git a/packages/docs/src/plugins/icons.ts b/packages/docs/src/plugins/icons.ts index c0cf8a9cd90..18f53826e17 100644 --- a/packages/docs/src/plugins/icons.ts +++ b/packages/docs/src/plugins/icons.ts @@ -48,6 +48,7 @@ export { mdiBookmark, mdiBookmarkMinus, mdiBookmarkOutline, + mdiBookOpenPageVariant, mdiBookVariant, mdiBottleTonicPlus, mdiBriefcase, @@ -194,6 +195,7 @@ export { mdiGithub, mdiGlassWine, mdiGoogleNearby, + mdiHandshakeOutline, mdiHeadQuestionOutline, mdiHeart, mdiHeartOutline, @@ -222,6 +224,7 @@ export { mdiLayersOutline, mdiLayersTriple, mdiLeaf, + mdiLicense, mdiLifebuoy, mdiLightbulbOnOutline, mdiLink, diff --git a/packages/vuetify/src/components/VTabs/VTabs.tsx b/packages/vuetify/src/components/VTabs/VTabs.tsx index bd6b47665ba..df42d23bb31 100644 --- a/packages/vuetify/src/components/VTabs/VTabs.tsx +++ b/packages/vuetify/src/components/VTabs/VTabs.tsx @@ -3,6 +3,8 @@ import './VTabs.sass' // Components import { VTab } from './VTab' +import { VTabsWindow } from './VTabsWindow' +import { VTabsWindowItem } from './VTabsWindowItem' import { makeVSlideGroupProps, VSlideGroup } from '@/components/VSlideGroup/VSlideGroup' // Composables @@ -22,6 +24,20 @@ import { VTabsSymbol } from './shared' export type TabItem = string | number | Record +export type VTabsSlot = { + item: TabItem +} + +export type VTabsSlots = { + default: never + tab: VTabsSlot + item: VTabsSlot + window: never +} & { + [key: `tab.${string}`]: VTabsSlot + [key: `item.${string}`]: VTabsSlot +} + function parseItems (items: readonly TabItem[] | undefined) { if (!items) return [] @@ -53,12 +69,15 @@ export const makeVTabsProps = propsFactory({ hideSlider: Boolean, sliderColor: String, - ...makeVSlideGroupProps({ mandatory: 'force' as const }), + ...makeVSlideGroupProps({ + mandatory: 'force' as const, + selectedClass: 'v-tab-item--selected', + }), ...makeDensityProps(), ...makeTagProps(), }, 'VTabs') -export const VTabs = genericComponent()({ +export const VTabs = genericComponent()({ name: 'VTabs', props: makeVTabsProps(), @@ -69,7 +88,7 @@ export const VTabs = genericComponent()({ setup (props, { slots }) { const model = useProxiedModel(props, 'modelValue') - const parsedItems = computed(() => parseItems(props.items)) + const items = computed(() => parseItems(props.items)) const { densityClasses } = useDensity(props) const { backgroundColorClasses, backgroundColorStyles } = useBackgroundColor(toRef(props, 'bgColor')) @@ -86,36 +105,66 @@ export const VTabs = genericComponent()({ useRender(() => { const slideGroupProps = VSlideGroup.filterProps(props) + const hasWindow = !!(slots.window || props.items.length > 0) return ( - - { slots.default ? slots.default() : parsedItems.value.map(item => ( - - ))} - + <> + + { slots.default?.() ?? items.value.map(item => ( + slots.tab?.({ item }) ?? ( + slots[`tab.${item.value}`]?.({ item }), + }} + /> + ) + ))} + + + { hasWindow && ( + + { items.value.map(item => slots.item?.({ item }) ?? ( + slots[`item.${item.value}`]?.({ item }), + }} + /> + ))} + + { slots.window?.() } + + )} + ) }) diff --git a/packages/vuetify/src/components/VTabs/VTabsWindow.tsx b/packages/vuetify/src/components/VTabs/VTabsWindow.tsx new file mode 100644 index 00000000000..ea39d0b36a6 --- /dev/null +++ b/packages/vuetify/src/components/VTabs/VTabsWindow.tsx @@ -0,0 +1,66 @@ +// Components +import { makeVWindowProps, VWindow } from '@/components/VWindow/VWindow' + +// Composables +import { useProxiedModel } from '@/composables/proxiedModel' + +// Utilities +import { computed, inject } from 'vue' +import { genericComponent, omit, propsFactory, useRender } from '@/util' + +// Types +import { VTabsSymbol } from './shared' + +export const makeVTabsWindowProps = propsFactory({ + ...omit(makeVWindowProps(), ['continuous', 'nextIcon', 'prevIcon', 'showArrows', 'touch', 'mandatory']), +}, 'VTabsWindow') + +export const VTabsWindow = genericComponent()({ + name: 'VTabsWindow', + + props: makeVTabsWindowProps(), + + emits: { + 'update:modelValue': (v: unknown) => true, + }, + + setup (props, { slots }) { + const group = inject(VTabsSymbol, null) + const _model = useProxiedModel(props, 'modelValue') + + const model = computed({ + get () { + // Always return modelValue if defined + // or if not within a VTabs group + if (_model.value != null || !group) return _model.value + + // If inside of a VTabs, find the currently selected + // item by id. Item value may be assigned by its index + return group.items.value.find(item => group.selected.value.includes(item.id))?.value + }, + set (val) { + _model.value = val + }, + }) + + useRender(() => { + const windowProps = VWindow.filterProps(props) + + return ( + + ) + }) + + return {} + }, +}) + +export type VTabsWindow = InstanceType diff --git a/packages/vuetify/src/components/VTabs/VTabsWindowItem.tsx b/packages/vuetify/src/components/VTabs/VTabsWindowItem.tsx new file mode 100644 index 00000000000..eebd10644a3 --- /dev/null +++ b/packages/vuetify/src/components/VTabs/VTabsWindowItem.tsx @@ -0,0 +1,38 @@ +// Components +import { makeVWindowItemProps, VWindowItem } from '@/components/VWindow/VWindowItem' + +// Utilities +import { genericComponent, propsFactory, useRender } from '@/util' + +export const makeVTabsWindowItemProps = propsFactory({ + ...makeVWindowItemProps(), +}, 'VTabsWindowItem') + +export const VTabsWindowItem = genericComponent()({ + name: 'VTabsWindowItem', + + props: makeVTabsWindowItemProps(), + + setup (props, { slots }) { + useRender(() => { + const windowItemProps = VWindowItem.filterProps(props) + + return ( + + ) + }) + + return {} + }, +}) + +export type VTabsWindowItem = InstanceType diff --git a/packages/vuetify/src/components/VTabs/index.ts b/packages/vuetify/src/components/VTabs/index.ts index e3a24f56450..c137dab2d86 100644 --- a/packages/vuetify/src/components/VTabs/index.ts +++ b/packages/vuetify/src/components/VTabs/index.ts @@ -1,2 +1,4 @@ -export { VTabs } from './VTabs' export { VTab } from './VTab' +export { VTabs } from './VTabs' +export { VTabsWindow } from './VTabsWindow' +export { VTabsWindowItem } from './VTabsWindowItem'