Files
sky_phone-sky-systems/frontend/src/ui/controls/SkySegmented.vue
T
2026-08-15 00:00:14 +02:00

148 lines
4.1 KiB
Vue

<script setup lang="ts">
import {
computed,
nextTick,
onBeforeUnmount,
onMounted,
onUpdated,
ref,
type ComponentPublicInstance,
type CSSProperties,
} from 'vue'
import { useSkyNavbar } from '../navbar-context'
import SkyGlass from './SkyGlass.vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
activeIndex?: number
ariaLabel?: string
compact?: boolean
glassHighlight?: boolean
itemCount?: number
navigation?: boolean
outline?: boolean
raised?: boolean
rounded?: boolean
strong?: boolean
}>(),
{
activeIndex: undefined,
ariaLabel: '',
compact: false,
glassHighlight: true,
itemCount: undefined,
navigation: undefined,
outline: false,
raised: false,
rounded: false,
strong: false,
},
)
const insideNavbar = useSkyNavbar()
const isNavbarSegmented = computed(
() => insideNavbar && typeof props.navigation === 'undefined',
)
const isNavigation = computed(() => props.navigation === true)
const usesGlass = computed(() => isNavigation.value || isNavbarSegmented.value)
const rootComponent = computed(() => (usesGlass.value ? SkyGlass : 'div'))
const root = ref<HTMLElement | ComponentPublicInstance | null>(null)
const measuredItemCount = ref(0)
const measuredActiveIndex = ref(0)
let observer: MutationObserver | undefined
function rootElement(): HTMLElement | null {
const value = root.value
if (!value || typeof HTMLElement === 'undefined') return null
if (value instanceof HTMLElement) return value
return value.$el instanceof HTMLElement ? value.$el : null
}
function measureChildren(): void {
const element = rootElement()
if (!element) return
const buttons = Array.from(
element.querySelectorAll<HTMLElement>(':scope > .sky-segmented-button'),
)
const activeIndex = buttons.findIndex((button) =>
button.classList.contains('sky-segmented-button--active'),
)
measuredItemCount.value = buttons.length
measuredActiveIndex.value = activeIndex < 0 ? 0 : activeIndex
}
onMounted(() => {
measureChildren()
const element = rootElement()
if (!element || typeof MutationObserver === 'undefined') return
observer = new MutationObserver(measureChildren)
observer.observe(element, {
attributes: true,
attributeFilter: ['class'],
childList: true,
subtree: true,
})
})
onUpdated(() => void nextTick(measureChildren))
onBeforeUnmount(() => observer?.disconnect())
const indicatorStyle = computed<CSSProperties | undefined>(() => {
const itemCount = Number.isFinite(props.itemCount)
? Math.max(0, Math.floor(props.itemCount ?? 0))
: measuredItemCount.value
if (!props.strong || itemCount === 0) return undefined
const requestedIndex = Number.isFinite(props.activeIndex)
? Math.floor(props.activeIndex ?? 0)
: measuredActiveIndex.value
const activeIndex = Math.max(0, Math.min(itemCount - 1, requestedIndex))
const widthPercentage = Number((100 / itemCount).toFixed(4))
const edgePadding = usesGlass.value ? 4 : 2
const spacing = Number(
((edgePadding * 2 + (itemCount - 1) * 4) / itemCount).toFixed(4),
)
const percentageOffset = activeIndex * 100
const pixelOffset = activeIndex * 4
return {
'--sky-segmented-indicator-offset': `calc(${percentageOffset}% + ${pixelOffset}px)`,
'--sky-segmented-indicator-offset-rtl': `calc(-${percentageOffset}% - ${pixelOffset}px)`,
width: `calc(${widthPercentage}% - ${spacing}px)`,
}
})
</script>
<template>
<component
:is="rootComponent"
ref="root"
v-bind="$attrs"
:highlight="usesGlass ? glassHighlight : undefined"
class="sky-segmented"
:class="{
'sky-segmented--navigation': isNavigation,
'sky-segmented--navbar': isNavbarSegmented,
'sky-segmented--compact': compact,
'sky-segmented--outline': outline,
'sky-segmented--raised': raised,
'sky-segmented--rounded': rounded,
'sky-segmented--strong': strong,
}"
role="group"
:aria-label="ariaLabel || undefined"
>
<slot />
<span
v-if="indicatorStyle"
class="sky-segmented__highlight"
:style="indicatorStyle"
aria-hidden="true"
/>
</component>
</template>