mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-20 04:32:22 +09:00
223 lines
4.6 KiB
Vue
223 lines
4.6 KiB
Vue
<script setup lang="ts">
|
|
const modelValue = defineModel<number | undefined>({
|
|
default: undefined,
|
|
})
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
id?: string
|
|
max?: number
|
|
label?: string
|
|
size?: 'small' | 'base' | 'medium' | 'large' | 'xlarge'
|
|
readonly?: boolean
|
|
disabled?: boolean
|
|
}>(),
|
|
{
|
|
id: undefined,
|
|
max: 5,
|
|
size: 'base',
|
|
label: undefined,
|
|
readonly: undefined,
|
|
},
|
|
)
|
|
|
|
const { field, id } = useVFieldContext({
|
|
help: 'VRangeRating',
|
|
id: props.id,
|
|
})
|
|
|
|
const hasValue = computed(
|
|
() => field?.value !== undefined || modelValue.value !== undefined,
|
|
)
|
|
const active = computed(() => !props.readonly && hasValue.value)
|
|
|
|
const internal = computed({
|
|
get() {
|
|
if (field?.value) {
|
|
return field.value.value ?? 0
|
|
}
|
|
else {
|
|
return modelValue.value ?? 0
|
|
}
|
|
},
|
|
set(value: any) {
|
|
if (field?.value) {
|
|
field.value.setValue(value)
|
|
}
|
|
modelValue.value = value
|
|
},
|
|
})
|
|
|
|
const sizeStyle = computed(() => {
|
|
switch (props.size) {
|
|
case 'small':
|
|
return 'is-size-6'
|
|
case 'base':
|
|
return 'is-size-5'
|
|
case 'medium':
|
|
return 'is-size-4'
|
|
case 'large':
|
|
return 'is-size-3'
|
|
case 'xlarge':
|
|
return 'is-size-2'
|
|
}
|
|
})
|
|
|
|
const radiogroup = ref()
|
|
function focus() {
|
|
if (props.readonly) return
|
|
if (props.disabled) return
|
|
if (radiogroup.value) {
|
|
radiogroup.value.focus()
|
|
}
|
|
}
|
|
|
|
const wrapper = ref()
|
|
const { focused } = useFocusWithin(wrapper)
|
|
onKeyStroke('ArrowLeft', (e) => {
|
|
if (!focused.value) return
|
|
if (props.disabled) return
|
|
|
|
e.preventDefault()
|
|
|
|
if (internal.value > 0) {
|
|
internal.value = internal.value - 1
|
|
}
|
|
})
|
|
onKeyStroke('ArrowRight', (e) => {
|
|
if (!focused.value) return
|
|
if (props.disabled) return
|
|
|
|
e.preventDefault()
|
|
|
|
if (internal.value < props.max) {
|
|
internal.value = internal.value + 1
|
|
}
|
|
})
|
|
|
|
const highlighted = ref<number>()
|
|
function highlightIndex(index: number) {
|
|
if (props.readonly) return
|
|
highlighted.value = index + 1
|
|
}
|
|
function unhighlight() {
|
|
highlighted.value = undefined
|
|
}
|
|
|
|
function selectIndex(index: number) {
|
|
if (props.readonly) return
|
|
if (props.disabled) return
|
|
internal.value = index + 1
|
|
}
|
|
|
|
function isStarSelected(index: number) {
|
|
if (!hasValue.value) return 0
|
|
|
|
if (highlighted.value !== undefined) {
|
|
return highlighted.value - index > 0
|
|
}
|
|
|
|
return internal.value - index > 0
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="wrapper"
|
|
class="rating-wrap"
|
|
>
|
|
<div
|
|
v-if="props.label || 'label' in $slots"
|
|
class="rating-label"
|
|
>
|
|
<slot name="label">
|
|
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
|
|
<label
|
|
:id="`${id}-label`"
|
|
:for="id"
|
|
@click="focus"
|
|
@keydown.enter="focus"
|
|
>
|
|
{{ props.label }}
|
|
</label>
|
|
</slot>
|
|
</div>
|
|
<div
|
|
:id="id"
|
|
ref="radiogroup"
|
|
class="rating"
|
|
:class="{
|
|
'is-active': active,
|
|
'is-highlighted': highlighted,
|
|
[sizeStyle]: true,
|
|
}"
|
|
:aria-labelledby="
|
|
props.label || ('label' in $slots && active) ? `${id}-label` : undefined
|
|
"
|
|
:tabindex="active ? 0 : undefined"
|
|
:role="active ? 'radiogroup' : undefined"
|
|
>
|
|
<span
|
|
v-for="(star, index) in props.max"
|
|
:key="index"
|
|
:role="active ? 'radio' : undefined"
|
|
:aria-label="String(index + 1)"
|
|
:aria-checked="active ? internal - index > 0 : undefined"
|
|
aria-hidden="true"
|
|
class="rating-star"
|
|
:class="{
|
|
'is-rating-star-selected': isStarSelected(index),
|
|
}"
|
|
@pointerenter.passive="() => highlightIndex(index)"
|
|
@pointerleave.passive="() => unhighlight()"
|
|
@click.passive="() => selectIndex(index)"
|
|
>
|
|
<slot
|
|
v-bind="{
|
|
value: index + 1,
|
|
isSelected: isStarSelected(index),
|
|
isHighlighted: index + 1 === highlighted,
|
|
}"
|
|
>
|
|
<VIcon icon="ph:star-fill" />
|
|
</slot>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.rating-wrap {
|
|
text-align: inset-inline-end;
|
|
|
|
.rating-label {
|
|
color: var(--light-text);
|
|
}
|
|
|
|
.rating {
|
|
display: inline-flex;
|
|
|
|
.rating-star {
|
|
color: color-mix(in oklab, var(--widget-grey), black 8%);
|
|
|
|
&.is-rating-star-selected {
|
|
color: var(--yellow) !important;
|
|
}
|
|
}
|
|
|
|
&.is-active {
|
|
[role='radio'] {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.is-dark .rating-wrap {
|
|
.rating {
|
|
.rating-star {
|
|
color: color-mix(in oklab, var(--dark-sidebar), white 22%);
|
|
}
|
|
}
|
|
}
|
|
</style>
|