quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
292 lines (245 loc) • 7.67 kB
JavaScript
import {
h,
ref,
computed,
inject,
onBeforeUnmount,
onMounted,
withDirectives,
getCurrentInstance
} from 'vue'
import QIcon from '../icon/QIcon.js'
import Ripple from '../../directives/ripple/Ripple.js'
import { hMergeSlot } from '../../utils/private.render/render.js'
import {
isKeyCode,
shouldIgnoreKey
} from '../../utils/private.keyboard/key-composition.js'
import { tabsKey, emptyRenderFn } from '../../utils/private.symbols/symbols.js'
import { stopAndPrevent } from '../../utils/event/event.js'
import uid from '../../utils/uid/uid.js'
import { isDeepEqual } from '../../utils/is/is.js'
let id = 0
export const useTabEmits = ['click', 'keydown']
export const useTabProps = {
icon: String,
label: [Number, String],
alert: [Boolean, String],
alertIcon: String,
name: {
type: [Number, String],
default: () => `t_${id++}`
},
noCaps: Boolean,
tabindex: [String, Number],
disable: Boolean,
contentClass: String,
ripple: {
type: [Boolean, Object],
default: true
}
}
export default function useTab(props, slots, emit, routeData) {
const $tabs = inject(tabsKey, emptyRenderFn)
if ($tabs === emptyRenderFn) {
console.error('QTab/QRouteTab component needs to be child of QTabs')
return emptyRenderFn
}
const { proxy } = getCurrentInstance()
const blurTargetRef = ref(null)
const rootRef = ref(null)
const tabIndicatorRef = ref(null)
const ripple = computed(() =>
props.disable === true || props.ripple === false
? false
: Object.assign(
{ keyCodes: [13, 32], early: true },
props.ripple === true ? {} : props.ripple
)
)
const isActive = computed(() => $tabs.currentModel.value === props.name)
const classes = computed(
() =>
'q-tab relative-position self-stretch flex flex-center text-center' +
(isActive.value === true
? ' q-tab--active' +
($tabs.tabProps.value.activeClass
? ' ' + $tabs.tabProps.value.activeClass
: '') +
($tabs.tabProps.value.activeColor
? ` text-${$tabs.tabProps.value.activeColor}`
: '') +
($tabs.tabProps.value.activeBgColor
? ` bg-${$tabs.tabProps.value.activeBgColor}`
: '')
: ' q-tab--inactive') +
(props.icon && props.label && $tabs.tabProps.value.inlineLabel === false
? ' q-tab--full'
: '') +
(props.noCaps === true || $tabs.tabProps.value.noCaps === true
? ' q-tab--no-caps'
: '') +
(props.disable === true
? ' disabled'
: ' q-focusable q-hoverable cursor-pointer') +
(routeData !== void 0 ? routeData.linkClass.value : '')
)
const innerClass = computed(
() =>
'q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable ' +
($tabs.tabProps.value.inlineLabel === true
? 'row no-wrap q-tab__content--inline'
: 'column') +
(props.contentClass !== void 0 ? ` ${props.contentClass}` : '')
)
const tabIndex = computed(() =>
props.disable === true ||
$tabs.hasFocus.value === true ||
(isActive.value === false && $tabs.hasActiveTab.value === true)
? -1
: props.tabindex || 0
)
function onClick(e, keyboard) {
if (keyboard !== true && e?.qAvoidFocus !== true) {
blurTargetRef.value?.focus()
}
if (props.disable === true) {
// we should hinder native navigation though
if (routeData?.hasRouterLink.value === true) {
stopAndPrevent(e)
}
return
}
// do we have a QTab?
if (routeData === void 0) {
$tabs.updateModel({ name: props.name })
emit('click', e)
return
}
if (routeData.hasRouterLink.value === true) {
const go = (opts = {}) => {
// if requiring to go to another route, then we
// let the QTabs route watcher do its job,
// otherwise directly select this
let hardError
const reqId =
opts.to === void 0 || isDeepEqual(opts.to, props.to) === true
? ($tabs.avoidRouteWatcher = uid())
: null
return routeData
.navigateToRouterLink(e, { ...opts, returnRouterError: true })
.catch(err => {
hardError = err
})
.then(softError => {
if (reqId === $tabs.avoidRouteWatcher) {
$tabs.avoidRouteWatcher = false
// if we don't have any hard errors or any soft errors, except for
// when navigating to the same route (on all other soft errors,
// like when navigation was aborted in a nav guard, we don't activate this tab)
if (
hardError === void 0 &&
(softError === void 0 ||
softError.message?.startsWith(
'Avoided redundant navigation'
) === true)
) {
$tabs.updateModel({ name: props.name })
}
}
if (opts.returnRouterError === true) {
return hardError !== void 0
? Promise.reject(hardError)
: softError
}
})
}
emit('click', e, go)
if (e.defaultPrevented !== true) go()
return
}
emit('click', e)
}
function onKeydown(e) {
if (isKeyCode(e, [13, 32])) {
onClick(e, true)
} else if (
shouldIgnoreKey(e) !== true &&
e.keyCode >= 35 &&
e.keyCode <= 40 &&
e.altKey !== true &&
e.metaKey !== true
) {
if ($tabs.onKbdNavigate(e.keyCode, proxy.$el) === true) stopAndPrevent(e)
}
emit('keydown', e)
}
function getContent() {
const narrow = $tabs.tabProps.value.narrowIndicator,
content = [],
indicator = h('div', {
ref: tabIndicatorRef,
class: ['q-tab__indicator', $tabs.tabProps.value.indicatorClass]
})
if (props.icon !== void 0) {
content.push(
h(QIcon, {
class: 'q-tab__icon',
name: props.icon
})
)
}
if (props.label !== void 0) {
content.push(h('div', { class: 'q-tab__label' }, props.label))
}
if (props.alert !== false) {
content.push(
props.alertIcon !== void 0
? h(QIcon, {
class: 'q-tab__alert-icon',
color: props.alert !== true ? props.alert : void 0,
name: props.alertIcon
})
: h('div', {
class:
'q-tab__alert' +
(props.alert !== true ? ` text-${props.alert}` : '')
})
)
}
if (narrow === true) content.push(indicator)
const node = [
h('div', { class: 'q-focus-helper', tabindex: -1, ref: blurTargetRef }),
h('div', { class: innerClass.value }, hMergeSlot(slots.default, content))
]
if (narrow === false) node.push(indicator)
return node
}
const tabData = {
name: computed(() => props.name),
rootRef,
tabIndicatorRef,
routeData
}
onBeforeUnmount(() => {
$tabs.unregisterTab(tabData)
})
onMounted(() => {
$tabs.registerTab(tabData)
})
function renderTab(tag, customData) {
const data = {
ref: rootRef,
class: classes.value,
tabindex: tabIndex.value,
role: 'tab',
'aria-selected': isActive.value === true ? 'true' : 'false',
'aria-disabled': props.disable === true ? 'true' : void 0,
onClick,
onKeydown,
...customData
}
return withDirectives(h(tag, data, getContent()), [[Ripple, ripple.value]])
}
return { renderTab, $tabs }
}