124 lines
3.2 KiB
Vue
124 lines
3.2 KiB
Vue
<script setup>
|
|
import { computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
options: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
name: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
// Array of option values that should be individually disabled.
|
|
// An already-selected option is never disabled via this prop.
|
|
disabledOptions: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
// Tooltip text shown when hovering over a disabled option.
|
|
disabledTooltip: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
/**
|
|
* Returns true when an option should be treated as disabled.
|
|
* An option that is already selected (checked) is never individually disabled
|
|
* via disabledOptions — it remains selectable so the user can keep it.
|
|
*/
|
|
const isOptionDisabled = (value) => {
|
|
if (props.disabled) return true
|
|
if (props.modelValue === value) return false
|
|
return props.disabledOptions.includes(value)
|
|
}
|
|
|
|
const handleChange = (value) => {
|
|
// Safety guard: do not emit for disabled options
|
|
if (isOptionDisabled(value)) return
|
|
emit('update:modelValue', value)
|
|
}
|
|
|
|
const getSegmentClasses = (option, index) => {
|
|
const optionDisabled = isOptionDisabled(option.value)
|
|
|
|
const classes = [
|
|
'inline-flex',
|
|
'items-center',
|
|
'justify-center',
|
|
'px-8',
|
|
'py-3',
|
|
'text-base',
|
|
'font-medium',
|
|
'select-none',
|
|
'transition-all',
|
|
'duration-200',
|
|
'bg-white/5',
|
|
'text-gray-400',
|
|
'peer-checked:bg-primary',
|
|
'peer-checked:text-gray-900',
|
|
'peer-checked:font-semibold',
|
|
'peer-focus-visible:ring-2',
|
|
'peer-focus-visible:ring-primary',
|
|
'peer-focus-visible:ring-offset-2',
|
|
'peer-focus-visible:ring-offset-surface',
|
|
]
|
|
|
|
if (optionDisabled) {
|
|
// Disabled: visually subtle, no pointer, no hover effects
|
|
classes.push('opacity-35', 'cursor-not-allowed')
|
|
} else {
|
|
// Enabled: pointer cursor and hover effects
|
|
classes.push(
|
|
'cursor-pointer',
|
|
'hover:bg-white/10',
|
|
'hover:text-gray-200',
|
|
'peer-checked:hover:bg-primary-dark',
|
|
'peer-checked:hover:text-gray-900',
|
|
)
|
|
}
|
|
|
|
// All except last: add divider
|
|
if (index < props.options.length - 1) {
|
|
classes.push('border-r', 'border-white/10')
|
|
}
|
|
|
|
return classes.join(' ')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div role="radiogroup" :aria-label="name" class="inline-flex rounded-lg overflow-hidden border border-white/10 min-h-[44px]">
|
|
<label
|
|
v-for="(option, index) in options"
|
|
:key="option.value"
|
|
class="relative"
|
|
>
|
|
<input
|
|
type="radio"
|
|
:name="name"
|
|
:value="option.value"
|
|
:checked="modelValue === option.value"
|
|
:disabled="isOptionDisabled(option.value)"
|
|
:data-cy="option.value === 'not_applicable' ? 'na' : option.value"
|
|
@change="handleChange(option.value)"
|
|
class="sr-only peer"
|
|
/>
|
|
<span :class="getSegmentClasses(option, index)" :title="isOptionDisabled(option.value) ? disabledTooltip : ''">
|
|
{{ option.label }}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</template>
|