Make sure that the user doesn't select more than eight maybe buttons in the pre-select
This commit is contained in:
@@ -18,15 +18,41 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
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'])
|
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) => {
|
const handleChange = (value) => {
|
||||||
|
// Safety guard: do not emit for disabled options
|
||||||
|
if (isOptionDisabled(value)) return
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSegmentClasses = (index) => {
|
const getSegmentClasses = (option, index) => {
|
||||||
|
const optionDisabled = isOptionDisabled(option.value)
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
'inline-flex',
|
'inline-flex',
|
||||||
'items-center',
|
'items-center',
|
||||||
@@ -40,11 +66,6 @@ const getSegmentClasses = (index) => {
|
|||||||
'duration-200',
|
'duration-200',
|
||||||
'bg-white/5',
|
'bg-white/5',
|
||||||
'text-gray-400',
|
'text-gray-400',
|
||||||
'cursor-pointer',
|
|
||||||
'hover:bg-white/10',
|
|
||||||
'hover:text-gray-200',
|
|
||||||
'peer-checked:hover:bg-primary-dark',
|
|
||||||
'peer-checked:hover:text-gray-900',
|
|
||||||
'peer-checked:bg-primary',
|
'peer-checked:bg-primary',
|
||||||
'peer-checked:text-gray-900',
|
'peer-checked:text-gray-900',
|
||||||
'peer-checked:font-semibold',
|
'peer-checked:font-semibold',
|
||||||
@@ -54,16 +75,25 @@ const getSegmentClasses = (index) => {
|
|||||||
'peer-focus-visible:ring-offset-surface',
|
'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
|
// All except last: add divider
|
||||||
if (index < props.options.length - 1) {
|
if (index < props.options.length - 1) {
|
||||||
classes.push('border-r', 'border-white/10')
|
classes.push('border-r', 'border-white/10')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disabled state
|
|
||||||
if (props.disabled) {
|
|
||||||
classes.push('disabled:opacity-50', 'disabled:cursor-not-allowed')
|
|
||||||
}
|
|
||||||
|
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -80,12 +110,12 @@ const getSegmentClasses = (index) => {
|
|||||||
:name="name"
|
:name="name"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
:checked="modelValue === option.value"
|
:checked="modelValue === option.value"
|
||||||
:disabled="disabled"
|
:disabled="isOptionDisabled(option.value)"
|
||||||
:data-cy="option.value === 'not_applicable' ? 'na' : option.value"
|
:data-cy="option.value === 'not_applicable' ? 'na' : option.value"
|
||||||
@change="handleChange(option.value)"
|
@change="handleChange(option.value)"
|
||||||
class="sr-only peer"
|
class="sr-only peer"
|
||||||
/>
|
/>
|
||||||
<span :class="getSegmentClasses(index)">
|
<span :class="getSegmentClasses(option, index)" :title="isOptionDisabled(option.value) ? disabledTooltip : ''">
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { Head, useForm } from '@inertiajs/vue3'
|
import { Head, useForm } from '@inertiajs/vue3'
|
||||||
import { ArrowLeftIcon } from '@heroicons/vue/24/outline'
|
import { ArrowLeftIcon } from '@heroicons/vue/24/outline'
|
||||||
|
import { ExclamationTriangleIcon } from '@heroicons/vue/20/solid'
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
import AppButton from '@/Components/AppButton.vue'
|
import AppButton from '@/Components/AppButton.vue'
|
||||||
import RadioButtonGroup from '@/Components/RadioButtonGroup.vue'
|
import RadioButtonGroup from '@/Components/RadioButtonGroup.vue'
|
||||||
|
|
||||||
defineOptions({ layout: AppLayout })
|
defineOptions({ layout: AppLayout })
|
||||||
|
|
||||||
|
const MAX_MAYBE_ANSWERS = 8
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
screening: {
|
screening: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -49,6 +52,14 @@ const allAnswered = computed(() => {
|
|||||||
return Object.values(form.answers).every(v => v !== null)
|
return Object.values(form.answers).every(v => v !== null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const unknownCount = computed(() => {
|
||||||
|
return Object.values(form.answers).filter(v => v === 'unknown').length
|
||||||
|
})
|
||||||
|
|
||||||
|
const maybeLimitReached = computed(() => {
|
||||||
|
return unknownCount.value >= MAX_MAYBE_ANSWERS
|
||||||
|
})
|
||||||
|
|
||||||
const bottomBackButtonRef = ref(null)
|
const bottomBackButtonRef = ref(null)
|
||||||
const showStickyBack = ref(false)
|
const showStickyBack = ref(false)
|
||||||
|
|
||||||
@@ -92,6 +103,11 @@ onUnmounted(() => {
|
|||||||
<h1 class="text-3xl font-bold text-white mb-2">Pre-Screening Questions</h1>
|
<h1 class="text-3xl font-bold text-white mb-2">Pre-Screening Questions</h1>
|
||||||
<p class="text-gray-400 mb-8">Please answer all questions to proceed.</p>
|
<p class="text-gray-400 mb-8">Please answer all questions to proceed.</p>
|
||||||
|
|
||||||
|
<p v-if="maybeLimitReached" class="text-amber-400/80 text-sm mb-6 flex items-center gap-2">
|
||||||
|
<ExclamationTriangleIcon class="h-4 w-4 shrink-0" />
|
||||||
|
You've used all {{ MAX_MAYBE_ANSWERS }} "I don't know" answers. Please choose Yes or No for the remaining questions.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="space-y-4 mb-8">
|
<div class="space-y-4 mb-8">
|
||||||
<div
|
<div
|
||||||
v-for="(question, index) in questions"
|
v-for="(question, index) in questions"
|
||||||
@@ -111,6 +127,8 @@ onUnmounted(() => {
|
|||||||
{ value: 'unknown', label: 'I don\'t know' },
|
{ value: 'unknown', label: 'I don\'t know' },
|
||||||
{ value: 'no', label: 'No' },
|
{ value: 'no', label: 'No' },
|
||||||
]"
|
]"
|
||||||
|
:disabled-options="maybeLimitReached && form.answers[index + 1] !== 'unknown' ? ['unknown'] : []"
|
||||||
|
disabled-tooltip="You can only select "I don't know" for a maximum of 8 questions"
|
||||||
/>
|
/>
|
||||||
<p v-if="form.errors[`answers.${index + 1}`]" class="text-red-500 text-sm mt-1">
|
<p v-if="form.errors[`answers.${index + 1}`]" class="text-red-500 text-sm mt-1">
|
||||||
{{ form.errors[`answers.${index + 1}`] }}
|
{{ form.errors[`answers.${index + 1}`] }}
|
||||||
|
|||||||
Reference in New Issue
Block a user