1- <script lang="ts">
2- import { defineComponent , PropType , ref , computed } from ' vue'
1+ <script
2+ setup
3+ lang="ts"
4+ generic ="
5+ T extends BaseOption = ExtendedOption ,
6+ Options extends T [] | OptionGroup <string , T >[] = T [] | OptionGroup <string , T >[],
7+ ModelValue extends string = string
8+ " >
9+ import { ref , computed , useAttrs } from ' vue'
310import { Listbox , ListboxButton } from ' @headlessui/vue'
4-
5- import type { Option } from ' ./Option.vue'
6- import { OptionGroup , areOptionsGrouped } from ' ./OptionGroup.vue'
711import AColorCircle from ' ./ColorCircle.vue'
8- import OptionsPanel , { Direction } from ' ./internal/OptionsPanel.vue'
12+ import OptionsPanel , { type Direction } from ' ./internal/OptionsPanel.vue'
913import FloatingArrow from ' ./internal/FloatingArrow.vue'
14+ import {
15+ type BaseOption ,
16+ type ExtendedOption ,
17+ type OptionGroup ,
18+ areOptionsGrouped
19+ } from ' ../types/selection'
20+ import { type Color } from ' ../colors'
1021
11- export default defineComponent ({
12- name: ' AListbox' ,
13- components: {
14- Listbox ,
15- ListboxButton ,
16- OptionsPanel ,
17- FloatingArrow ,
18- AColorCircle
19- },
20- props: {
22+ const props = withDefaults (
23+ defineProps <{
2124 /**
2225 * An option is at the minimum a `{ value: string, label: string }` object.
2326 * This prop is used to display the list of listbox options,
2427 * as well as the correct label of the currently selected value.
2528 *
2629 * You can also provide a grouped structure of options: `[{ title: string, options: Option[] }]`
2730 */
28- options: {
29- type: Array as PropType <Option [] | OptionGroup []>,
30- required: true
31- },
31+ options: Options
3232 /**
3333 * an optional placeholder can be displayed when no value is currently selected.
3434 */
35- placeholder: {
36- type: String ,
37- default: ' Select value'
38- },
35+ placeholder? : string
3936 /**
4037 * the size of the listbox component
4138 */
42- size: {
43- type: String as PropType <' sm' | ' md' >,
44- default: ' sm'
45- },
39+ size? : ' sm' | ' md'
4640 /**
4741 * how the listbox button is displayed when not focused
4842 */
49- variant: {
50- type: String as PropType <' subtle' | ' default' >,
51- default: ' default'
52- },
43+ variant? : ' subtle' | ' default'
5344 /**
5445 * the prop modelValue is required to use [v-model](https://vuejs.org/guide/components/events.html#usage-with-v-model) with a component.
5546 */
56- modelValue: {
57- type: String ,
58- required: true
59- },
47+ modelValue: ModelValue
6048 /**
6149 * direction in which the dropdown is opening.
6250 * possible values: `up`, `down`. Default is `down`
6351 */
64- direction: {
65- type: String as PropType <Direction >,
66- default: ' down'
67- },
52+ direction? : Direction
6853 /**
6954 * extra classes to style the listbox options panel
7055 * useful for setting the panel height
7156 */
72- panelClasses: {
73- type: String ,
74- default: ' '
75- },
57+ panelClasses? : string
7658 /**
7759 * the options panel can be rendered inline instead of the absolutely positioned dropdown
7860 */
79- inline: {
80- type: Boolean ,
81- default: false
82- },
61+ inline? : boolean
8362 /**
8463 * allow the options panel to be rendered outside the flow of a container that has content that needs scrolling
8564 */
86- escapeOverflow: {
87- type: Boolean ,
88- default: false
89- }
90- },
91- emits: [' update:modelValue' ],
92- setup() {
93- const listboxButton = ref ()
94- const optionsPanelWidth = computed (() => listboxButton .value ?.el .offsetWidth )
65+ escapeOverflow? : boolean
66+ }>(),
67+ {
68+ placeholder: ' Select value' ,
69+ size: ' sm' ,
70+ variant: ' default' ,
71+ direction: ' down' ,
72+ panelClasses: ' ' ,
73+ inline: false ,
74+ escapeOverflow: false
75+ }
76+ )
77+
78+ const emit = defineEmits <{
79+ ' update:modelValue' : [value : ModelValue ]
80+ }>()
9581
96- // hack together a tab-out behavior for the listbox
97- const handleTab = (event : KeyboardEvent ) => {
98- if (event .key === ' Tab' ) {
99- const newEvent = new KeyboardEvent (' keydown' , { key: ' Escape' })
100- event .target ?.dispatchEvent (newEvent )
101- }
102- }
82+ const attrs = useAttrs ()
10383
104- return { handleTab , listboxButton , optionsPanelWidth }
105- },
106- computed: {
107- flatOptions() {
108- return areOptionsGrouped (this .options )
109- ? this .options .map (({ options }) => options ).flat ()
110- : this .options
111- },
112- valueOption() {
113- return this .flatOptions .find (option => option .value === this .model )
114- },
115- valueLabel() {
116- return this .valueOption ?.label || this .model
117- },
118- model: {
119- get() {
120- return this .modelValue
121- },
122- set(value : string ) {
123- this .$emit (' update:modelValue' , value )
124- }
125- }
84+ const listboxButton = ref ()
85+ const optionsPanelWidth = computed (() => listboxButton .value ?.el .offsetWidth )
86+
87+ // hack together a tab-out behavior for the listbox
88+ const handleTab = (event : KeyboardEvent ) => {
89+ if (event .key === ' Tab' ) {
90+ const newEvent = new KeyboardEvent (' keydown' , { key: ' Escape' })
91+ event .target ?.dispatchEvent (newEvent )
12692 }
93+ }
94+
95+ const flatOptions = computed ((): T [] =>
96+ areOptionsGrouped (props .options )
97+ ? props .options .map (({ options }) => options ).flat ()
98+ : (props .options as T [])
99+ )
100+
101+ const model = computed ({
102+ get : (): ModelValue => props .modelValue ,
103+ set : (value : ModelValue ) => emit (' update:modelValue' , value )
104+ })
105+
106+ const valueOption = computed (() => flatOptions .value .find (option => option .value === model .value ))
107+
108+ const valueLabel = computed (() => valueOption .value ?.label || model .value )
109+
110+ const valueOptionColor = computed (() => {
111+ const opt = valueOption .value as (BaseOption & { color? : Color }) | undefined
112+ return opt ?.color
127113})
128114 </script >
129115<template >
@@ -145,16 +131,16 @@ export default defineComponent({
145131 -->
146132 <slot name =" listbox-value" >
147133 <div v-if =" model" class =" truncate" >
148- <AColorCircle v-if =" valueOption?.color " :color =" valueOption?.color " class =" mr-2" />
134+ <AColorCircle v-if =" valueOptionColor " :color =" valueOptionColor " class =" mr-2" />
149135 <span >{{ valueLabel }}</span >
150136 </div >
151137 <span v-else class =" a-text-input-placeholder" >{{ placeholder }}</span >
152138 </slot >
153139 </div >
154140 <FloatingArrow
155141 v-if =" !inline"
156- :float =" variant === 'subtle' && !$ attrs.disabled"
157- :class =" { 'text-warsaw': $ attrs.disabled }" />
142+ :float =" variant === 'subtle' && !attrs.disabled"
143+ :class =" { 'text-warsaw': attrs.disabled }" />
158144 </slot >
159145 </ListboxButton >
160146 <OptionsPanel
0 commit comments