@hina-ui/vue 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +485 -262
- package/dist/index.js.map +1 -1
- package/dist/types/components/accordion/AccordionContent.vue.d.ts.map +1 -1
- package/dist/types/components/avatar/AvatarGroup.vue.d.ts.map +1 -1
- package/dist/types/components/avatar/utils/children.d.ts +3 -0
- package/dist/types/components/avatar/utils/children.d.ts.map +1 -0
- package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts +32 -29
- package/dist/types/components/checkbox-group/CheckboxGroup.vue.d.ts.map +1 -1
- package/dist/types/components/collapsible/CollapsibleContent.vue.d.ts.map +1 -1
- package/dist/types/components/combobox/Combobox.vue.d.ts +38 -38
- package/dist/types/components/combobox/Combobox.vue.d.ts.map +1 -1
- package/dist/types/components/combobox/ComboboxList.vue.d.ts +22 -15
- package/dist/types/components/combobox/ComboboxList.vue.d.ts.map +1 -1
- package/dist/types/components/dialog/Dialog.vue.d.ts +22 -16
- package/dist/types/components/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/types/components/dialog/dialog.variants.d.ts +32 -2
- package/dist/types/components/dialog/dialog.variants.d.ts.map +1 -1
- package/dist/types/components/image/ImageGroup.vue.d.ts.map +1 -1
- package/dist/types/components/image/composables/useImageGroupState.d.ts +7 -0
- package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -0
- package/dist/types/components/input/Input.vue.d.ts +3 -3
- package/dist/types/components/input/Input.vue.d.ts.map +1 -1
- package/dist/types/components/input/InputBase.vue.d.ts +3 -3
- package/dist/types/components/input/InputBase.vue.d.ts.map +1 -1
- package/dist/types/components/input/input.variants.d.ts +52 -6
- package/dist/types/components/input/input.variants.d.ts.map +1 -1
- package/dist/types/components/input-group/InputGroup.vue.d.ts +3 -3
- package/dist/types/components/input-group/InputGroup.vue.d.ts.map +1 -1
- package/dist/types/components/input-group/context.d.ts +2 -1
- package/dist/types/components/input-group/context.d.ts.map +1 -1
- package/dist/types/components/input-group/input-group.variants.d.ts +13 -1
- package/dist/types/components/input-group/input-group.variants.d.ts.map +1 -1
- package/dist/types/components/listbox/Listbox.vue.d.ts +36 -29
- package/dist/types/components/listbox/Listbox.vue.d.ts.map +1 -1
- package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts +31 -0
- package/dist/types/components/listbox/ListboxOptionContent.vue.d.ts.map +1 -0
- package/dist/types/components/listbox/listbox.variants.d.ts +19 -1
- package/dist/types/components/listbox/listbox.variants.d.ts.map +1 -1
- package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts +41 -43
- package/dist/types/components/multi-combobox/MultiCombobox.vue.d.ts.map +1 -1
- package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts +27 -0
- package/dist/types/components/multi-combobox/composables/useMultiCombobox.d.ts.map +1 -0
- package/dist/types/components/multi-select/MultiSelect.vue.d.ts +39 -41
- package/dist/types/components/multi-select/MultiSelect.vue.d.ts.map +1 -1
- package/dist/types/components/number-input/NumberInput.vue.d.ts +3 -3
- package/dist/types/components/number-input/NumberInput.vue.d.ts.map +1 -1
- package/dist/types/components/number-input/number-input.variants.d.ts +10 -1
- package/dist/types/components/number-input/number-input.variants.d.ts.map +1 -1
- package/dist/types/components/password-input/PasswordInput.vue.d.ts +3 -3
- package/dist/types/components/password-input/PasswordInput.vue.d.ts.map +1 -1
- package/dist/types/components/radio-group/RadioGroup.vue.d.ts +32 -29
- package/dist/types/components/radio-group/RadioGroup.vue.d.ts.map +1 -1
- package/dist/types/components/scroll-area/scroll-restore.d.ts +0 -1
- package/dist/types/components/scroll-area/scroll-restore.d.ts.map +1 -1
- package/dist/types/components/search-input/SearchInput.vue.d.ts +3 -3
- package/dist/types/components/search-input/SearchInput.vue.d.ts.map +1 -1
- package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts +30 -30
- package/dist/types/components/segmented-control/SegmentedControl.vue.d.ts.map +1 -1
- package/dist/types/components/select/Select.vue.d.ts +41 -37
- package/dist/types/components/select/Select.vue.d.ts.map +1 -1
- package/dist/types/components/select/SelectList.vue.d.ts +22 -15
- package/dist/types/components/select/SelectList.vue.d.ts.map +1 -1
- package/dist/types/components/select/composables/useClearTransition.d.ts +10 -0
- package/dist/types/components/select/composables/useClearTransition.d.ts.map +1 -0
- package/dist/types/components/select/select.variants.d.ts +16 -0
- package/dist/types/components/select/select.variants.d.ts.map +1 -1
- package/dist/types/components/select/types.d.ts +7 -7
- package/dist/types/components/select/types.d.ts.map +1 -1
- package/dist/types/components/textarea/Textarea.vue.d.ts.map +1 -1
- package/dist/types/components/textarea/composables/useTextareaSizing.d.ts +27 -0
- package/dist/types/components/textarea/composables/useTextareaSizing.d.ts.map +1 -0
- package/dist/types/components/textarea/textarea.variants.d.ts +9 -6
- package/dist/types/components/textarea/textarea.variants.d.ts.map +1 -1
- package/dist/types/lib/caret.d.ts +0 -4
- package/dist/types/lib/caret.d.ts.map +1 -1
- package/dist/types/lib/collapse.d.ts +2 -0
- package/dist/types/lib/collapse.d.ts.map +1 -1
- package/dist/types/lib/field-focus.d.ts +0 -4
- package/dist/types/lib/field-focus.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/AccordionContent.vue +2 -11
- package/src/components/avatar/AvatarGroup.vue +3 -8
- package/src/components/avatar/utils/children.ts +7 -0
- package/src/components/checkbox-group/CheckboxGroup.vue +3 -3
- package/src/components/collapsible/CollapsibleContent.vue +2 -11
- package/src/components/combobox/Combobox.vue +3 -3
- package/src/components/combobox/ComboboxList.vue +3 -3
- package/src/components/dialog/Dialog.vue +70 -39
- package/src/components/dialog/dialog.variants.ts +9 -3
- package/src/components/image/ImageGroup.vue +2 -30
- package/src/components/image/composables/useImageGroupState.ts +35 -0
- package/src/components/input/Input.vue +3 -3
- package/src/components/input/InputBase.vue +5 -5
- package/src/components/input/input.variants.ts +13 -3
- package/src/components/input-group/InputGroup.vue +11 -4
- package/src/components/input-group/context.ts +2 -1
- package/src/components/input-group/input-group.variants.ts +8 -1
- package/src/components/listbox/Listbox.vue +26 -29
- package/src/components/listbox/ListboxOptionContent.vue +28 -0
- package/src/components/listbox/listbox.variants.ts +6 -1
- package/src/components/multi-combobox/MultiCombobox.vue +14 -52
- package/src/components/multi-combobox/composables/useMultiCombobox.ts +63 -0
- package/src/components/multi-select/MultiSelect.vue +3 -4
- package/src/components/number-input/NumberInput.vue +8 -7
- package/src/components/number-input/number-input.variants.ts +5 -1
- package/src/components/password-input/PasswordInput.vue +3 -3
- package/src/components/radio-group/RadioGroup.vue +3 -3
- package/src/components/scroll-area/scroll-restore.ts +0 -5
- package/src/components/search-input/SearchInput.vue +3 -3
- package/src/components/segmented-control/SegmentedControl.vue +3 -3
- package/src/components/select/Select.vue +68 -25
- package/src/components/select/SelectList.vue +3 -3
- package/src/components/select/composables/useClearTransition.ts +25 -0
- package/src/components/select/select.variants.ts +24 -0
- package/src/components/select/types.ts +10 -8
- package/src/components/textarea/Textarea.vue +2 -41
- package/src/components/textarea/composables/useTextareaSizing.ts +54 -0
- package/src/components/textarea/textarea.variants.ts +5 -4
- package/src/lib/caret.ts +0 -4
- package/src/lib/collapse.ts +21 -1
- package/src/lib/field-focus.ts +0 -4
- package/src/styles/interaction.css +4 -2
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed
|
|
1
|
+
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
3
|
import { ComboboxAnchor, ComboboxInput, ComboboxRoot, ComboboxTrigger } from 'reka-ui'
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
5
5
|
import { useFieldControl } from '../form-field/context'
|
|
6
|
-
import { focusFieldFrom } from '../../lib/field-focus'
|
|
7
6
|
import { useUiLocale } from '../../locale'
|
|
8
7
|
import { buttonIconBox } from '../button/button.variants'
|
|
9
8
|
import IconSlot from '../button/IconSlot.vue'
|
|
@@ -19,7 +18,8 @@
|
|
|
19
18
|
inputIndicator,
|
|
20
19
|
type InputVariants,
|
|
21
20
|
} from '../input/input.variants'
|
|
22
|
-
import {
|
|
21
|
+
import type { SelectItems, SelectOption } from '../select/types'
|
|
22
|
+
import { useMultiCombobox } from './composables/useMultiCombobox'
|
|
23
23
|
import {
|
|
24
24
|
tagsInputChip,
|
|
25
25
|
tagsInputControl,
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
const props = withDefaults(
|
|
34
34
|
defineProps<{
|
|
35
|
-
options: SelectItems
|
|
35
|
+
options: SelectItems<T>
|
|
36
36
|
placeholder?: string
|
|
37
37
|
ignoreFilter?: boolean
|
|
38
38
|
loading?: boolean
|
|
@@ -52,11 +52,9 @@
|
|
|
52
52
|
const search = defineModel<string>('search', { default: '' })
|
|
53
53
|
const open = defineModel<boolean>('open', { default: false })
|
|
54
54
|
|
|
55
|
-
defineSlots<{ option(props: { option:
|
|
55
|
+
defineSlots<{ option?(props: { option: T }): unknown }>()
|
|
56
56
|
|
|
57
57
|
const t = useUiLocale()
|
|
58
|
-
const keyboard = ref(false)
|
|
59
|
-
const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
|
|
60
58
|
|
|
61
59
|
const {
|
|
62
60
|
id: fieldId,
|
|
@@ -68,52 +66,16 @@
|
|
|
68
66
|
disabled: () => props.disabled,
|
|
69
67
|
})
|
|
70
68
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const selected = computed(() =>
|
|
81
|
-
model.value.map(value => ({ value, label: labels.get(value) ?? String(value) })),
|
|
82
|
-
)
|
|
69
|
+
const { selected, keyboard, input, remove, clear, onHostClick, onInputKeydown } =
|
|
70
|
+
useMultiCombobox({
|
|
71
|
+
options: () => props.options,
|
|
72
|
+
model,
|
|
73
|
+
open,
|
|
74
|
+
disabled: () => disabled.value,
|
|
75
|
+
onClear: () => emit('clear'),
|
|
76
|
+
})
|
|
83
77
|
const chipSize = computed(() => (props.size === 'sm' ? 'sm' : 'md'))
|
|
84
78
|
const clearing = computed(() => props.clearable && selected.value.length > 0 && !disabled.value)
|
|
85
|
-
|
|
86
|
-
watch(open, value => {
|
|
87
|
-
if (!value) keyboard.value = false
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
function remove(value: string | number) {
|
|
91
|
-
if (disabled.value) return
|
|
92
|
-
model.value = model.value.filter(item => item !== value)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function clear() {
|
|
96
|
-
if (!model.value.length) return
|
|
97
|
-
model.value = []
|
|
98
|
-
emit('clear')
|
|
99
|
-
input.value?.$el.focus()
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function onHostClick(event: MouseEvent) {
|
|
103
|
-
const focused = focusFieldFrom(event.currentTarget as HTMLElement, event.target as HTMLElement)
|
|
104
|
-
if (focused && !open.value) open.value = true
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function onInputKeydown(event: KeyboardEvent) {
|
|
108
|
-
keyboard.value = true
|
|
109
|
-
if (
|
|
110
|
-
event.key !== 'Backspace' ||
|
|
111
|
-
(event.target as HTMLInputElement).value !== '' ||
|
|
112
|
-
!model.value.length
|
|
113
|
-
)
|
|
114
|
-
return
|
|
115
|
-
model.value = model.value.slice(0, -1)
|
|
116
|
-
}
|
|
117
79
|
</script>
|
|
118
80
|
|
|
119
81
|
<template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { computed, reactive, ref, shallowRef, watch, type Ref } from 'vue'
|
|
2
|
+
import { focusFieldFrom } from '../../../lib/field-focus'
|
|
3
|
+
import { flattenOptions, type SelectItems, type SelectOption } from '../../select/types'
|
|
4
|
+
|
|
5
|
+
interface MultiComboboxOptions<T extends SelectOption> {
|
|
6
|
+
options: () => SelectItems<T>
|
|
7
|
+
model: Ref<Array<string | number>>
|
|
8
|
+
open: Ref<boolean>
|
|
9
|
+
disabled: () => boolean
|
|
10
|
+
onClear: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useMultiCombobox<T extends SelectOption>(options: MultiComboboxOptions<T>) {
|
|
14
|
+
const { model, open } = options
|
|
15
|
+
const keyboard = ref(false)
|
|
16
|
+
const input = shallowRef<{ $el: HTMLInputElement } | null>(null)
|
|
17
|
+
const labels = reactive(new Map<string | number, string>())
|
|
18
|
+
watch(
|
|
19
|
+
options.options,
|
|
20
|
+
options => {
|
|
21
|
+
for (const option of flattenOptions(options)) labels.set(option.value, option.label)
|
|
22
|
+
},
|
|
23
|
+
{ immediate: true, deep: true },
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const selected = computed(() =>
|
|
27
|
+
model.value.map(value => ({ value, label: labels.get(value) ?? String(value) })),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
watch(open, value => {
|
|
31
|
+
if (!value) keyboard.value = false
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
function remove(value: string | number) {
|
|
35
|
+
if (options.disabled()) return
|
|
36
|
+
model.value = model.value.filter(item => item !== value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clear() {
|
|
40
|
+
if (!model.value.length) return
|
|
41
|
+
model.value = []
|
|
42
|
+
options.onClear()
|
|
43
|
+
input.value?.$el.focus()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onHostClick(event: MouseEvent) {
|
|
47
|
+
const focused = focusFieldFrom(event.currentTarget as HTMLElement, event.target as HTMLElement)
|
|
48
|
+
if (focused && !open.value) open.value = true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onInputKeydown(event: KeyboardEvent) {
|
|
52
|
+
keyboard.value = true
|
|
53
|
+
if (
|
|
54
|
+
event.key !== 'Backspace' ||
|
|
55
|
+
(event.target as HTMLInputElement).value !== '' ||
|
|
56
|
+
!model.value.length
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
model.value = model.value.slice(0, -1)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { selected, keyboard, input, remove, clear, onHostClick, onInputKeydown }
|
|
63
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import { SelectRoot, SelectTrigger } from 'reka-ui'
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
const props = withDefaults(
|
|
27
27
|
defineProps<{
|
|
28
|
-
options: SelectItems
|
|
28
|
+
options: SelectItems<T>
|
|
29
29
|
placeholder?: string
|
|
30
30
|
name?: string
|
|
31
31
|
required?: boolean
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
const model = defineModel<Array<string | number>>({ default: () => [] })
|
|
46
46
|
const open = defineModel<boolean>('open', { default: false })
|
|
47
47
|
|
|
48
|
-
defineSlots<{ option(props: { option:
|
|
48
|
+
defineSlots<{ option?(props: { option: T }): unknown }>()
|
|
49
49
|
|
|
50
50
|
const t = useUiLocale()
|
|
51
51
|
const group = injectInputGroup()
|
|
@@ -157,7 +157,6 @@
|
|
|
157
157
|
<DisclosureIcon />
|
|
158
158
|
</span>
|
|
159
159
|
</SelectTrigger>
|
|
160
|
-
<!-- Reka 2.10 BubbleSelect assigns arrays to select.value, which loses multiple values. -->
|
|
161
160
|
<VisuallyHidden v-if="props.name" as-child>
|
|
162
161
|
<select
|
|
163
162
|
v-model="model"
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
import {
|
|
17
17
|
inputControl,
|
|
18
18
|
inputEmbedded,
|
|
19
|
-
|
|
20
|
-
type
|
|
19
|
+
textInputHost,
|
|
20
|
+
type TextInputVariants,
|
|
21
21
|
} from '../input/input.variants'
|
|
22
22
|
import { numberInputStep, numberInputStepper } from './number-input.variants'
|
|
23
23
|
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
formatOptions?: Intl.NumberFormatOptions
|
|
35
35
|
locale?: string
|
|
36
36
|
controls?: boolean
|
|
37
|
-
variant?:
|
|
38
|
-
size?:
|
|
37
|
+
variant?: TextInputVariants['variant']
|
|
38
|
+
size?: TextInputVariants['size']
|
|
39
39
|
disabled?: boolean
|
|
40
40
|
readonly?: boolean
|
|
41
41
|
invalid?: boolean
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
const t = useUiLocale()
|
|
49
49
|
const group = injectInputGroup()
|
|
50
|
+
const divided = computed(() => (group ? group.variant?.value : props.variant) !== 'bare')
|
|
50
51
|
const size = computed(() => (group ? group.size.value : props.size))
|
|
51
52
|
const {
|
|
52
53
|
id: fieldId,
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
:data-disabled="disabled ? '' : undefined"
|
|
84
85
|
:class="
|
|
85
86
|
cn(
|
|
86
|
-
group ? inputEmbedded() :
|
|
87
|
+
group ? inputEmbedded() : textInputHost({ variant: props.variant, size: props.size }),
|
|
87
88
|
props.class,
|
|
88
89
|
)
|
|
89
90
|
"
|
|
@@ -95,13 +96,13 @@
|
|
|
95
96
|
:aria-describedby="describedBy"
|
|
96
97
|
:class="cn(inputControl(), 'tabular-nums')"
|
|
97
98
|
/>
|
|
98
|
-
<div v-if="props.controls" :class="numberInputStepper({ size })">
|
|
99
|
+
<div v-if="props.controls" :class="numberInputStepper({ size, divided })">
|
|
99
100
|
<NumberFieldIncrement :aria-label="t.numberInput.increase" :class="numberInputStep()">
|
|
100
101
|
<ChevronUp />
|
|
101
102
|
</NumberFieldIncrement>
|
|
102
103
|
<NumberFieldDecrement
|
|
103
104
|
:aria-label="t.numberInput.decrease"
|
|
104
|
-
:class="numberInputStep({ divided
|
|
105
|
+
:class="numberInputStep({ divided })"
|
|
105
106
|
>
|
|
106
107
|
<ChevronDown />
|
|
107
108
|
</NumberFieldDecrement>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { tv } from '../../lib/tv'
|
|
2
2
|
|
|
3
3
|
export const numberInputStepper = tv({
|
|
4
|
-
base: '
|
|
4
|
+
base: 'flex shrink-0 flex-col',
|
|
5
5
|
variants: {
|
|
6
|
+
divided: {
|
|
7
|
+
true: 'border-line border-s',
|
|
8
|
+
},
|
|
6
9
|
size: {
|
|
7
10
|
sm: '[&_svg]:size-3',
|
|
8
11
|
md: '[&_svg]:size-3',
|
|
@@ -11,6 +14,7 @@ export const numberInputStepper = tv({
|
|
|
11
14
|
},
|
|
12
15
|
defaultVariants: {
|
|
13
16
|
size: 'md',
|
|
17
|
+
divided: true,
|
|
14
18
|
},
|
|
15
19
|
})
|
|
16
20
|
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
import { useUiLocale } from '../../locale'
|
|
5
5
|
import InputBase from '../input/InputBase.vue'
|
|
6
6
|
import InputAction from '../input/InputAction.vue'
|
|
7
|
-
import type {
|
|
7
|
+
import type { TextInputVariants } from '../input/input.variants'
|
|
8
8
|
|
|
9
9
|
defineOptions({ name: 'HnPasswordInput', inheritAttrs: false })
|
|
10
10
|
|
|
11
11
|
const props = defineProps<{
|
|
12
|
-
variant?:
|
|
13
|
-
size?:
|
|
12
|
+
variant?: TextInputVariants['variant']
|
|
13
|
+
size?: TextInputVariants['size']
|
|
14
14
|
disabled?: boolean
|
|
15
15
|
invalid?: boolean
|
|
16
16
|
class?: string
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
2
|
import { RadioGroupItem, RadioGroupRoot } from 'reka-ui'
|
|
3
3
|
import { cn } from '../../lib/cn'
|
|
4
4
|
import { useFieldControl } from '../form-field/context'
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
defineOptions({ name: 'HnRadioGroup' })
|
|
23
23
|
|
|
24
24
|
const props = defineProps<{
|
|
25
|
-
options:
|
|
25
|
+
options: T[]
|
|
26
26
|
orientation?: CheckboxGroupVariants['orientation']
|
|
27
27
|
size?: CheckboxVariants['size']
|
|
28
28
|
controlPlacement?: CheckboxVariants['controlPlacement']
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
disabled: () => props.disabled,
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
defineSlots<{ option(props: { option:
|
|
42
|
+
defineSlots<{ option?(props: { option: T }): unknown }>()
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
45
|
<template>
|
|
@@ -37,7 +37,6 @@ export type ScrollRecord = Record<string, number>
|
|
|
37
37
|
|
|
38
38
|
export interface ScrollRestorerPorts {
|
|
39
39
|
getViewport: () => HTMLElement | null
|
|
40
|
-
/** Where the first-paint script left the scroller, read once and cleared. */
|
|
41
40
|
takeFirstPaintTop: () => number | undefined
|
|
42
41
|
readState: () => unknown
|
|
43
42
|
writeState: (state: Record<string, unknown>) => void
|
|
@@ -110,10 +109,6 @@ export function createScrollRestorer(key: string, ports: ScrollRestorerPorts): S
|
|
|
110
109
|
const firstPaintTop = ports.takeFirstPaintTop()
|
|
111
110
|
|
|
112
111
|
if (initial) {
|
|
113
|
-
// The first-paint script already put the scroller where this entry
|
|
114
|
-
// wants it, so anything else means the reader moved it while the page
|
|
115
|
-
// was still hydrating — the wheel listener that would have cancelled a
|
|
116
|
-
// restore is only attached from here on.
|
|
117
112
|
if (saved === undefined) return
|
|
118
113
|
const el = ports.getViewport()
|
|
119
114
|
if (firstPaintTop !== undefined && el && el.scrollTop !== firstPaintTop) return
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
import { Search } from '@lucide/vue'
|
|
3
3
|
import { cn } from '../../lib/cn'
|
|
4
4
|
import InputBase from '../input/InputBase.vue'
|
|
5
|
-
import type {
|
|
5
|
+
import type { TextInputVariants } from '../input/input.variants'
|
|
6
6
|
|
|
7
7
|
defineOptions({ name: 'HnSearchInput', inheritAttrs: false })
|
|
8
8
|
|
|
9
9
|
const props = withDefaults(
|
|
10
10
|
defineProps<{
|
|
11
|
-
variant?:
|
|
12
|
-
size?:
|
|
11
|
+
variant?: TextInputVariants['variant']
|
|
12
|
+
size?: TextInputVariants['size']
|
|
13
13
|
clearable?: boolean
|
|
14
14
|
loading?: boolean
|
|
15
15
|
disabled?: boolean
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
2
|
import { ToggleGroupItem, ToggleGroupRoot, type AcceptableValue } from 'reka-ui'
|
|
3
3
|
import { computed, useId, useSlots } from 'vue'
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const props = withDefaults(
|
|
20
20
|
defineProps<{
|
|
21
|
-
options:
|
|
21
|
+
options: T[]
|
|
22
22
|
size?: SegmentedItemVariants['size']
|
|
23
23
|
orientation?: SegmentedControlVariants['orientation']
|
|
24
24
|
block?: boolean
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
const model = defineModel<string | number>()
|
|
32
32
|
|
|
33
|
-
defineSlots<{ option(props: { option:
|
|
33
|
+
defineSlots<{ option?(props: { option: T }): unknown }>()
|
|
34
34
|
|
|
35
35
|
const slots = useSlots()
|
|
36
36
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
3
|
-
import { SelectRoot, SelectTrigger } from 'reka-ui'
|
|
1
|
+
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
|
+
import { computed, ref, shallowRef } from 'vue'
|
|
3
|
+
import { SelectRoot, SelectTrigger, useDirection } from 'reka-ui'
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
5
5
|
import { useUiLocale } from '../../locale'
|
|
6
|
+
import { X } from '@lucide/vue'
|
|
7
|
+
import InputAction from '../input/InputAction.vue'
|
|
6
8
|
import DisclosureIcon from '../disclosure-icon/DisclosureIcon.vue'
|
|
7
9
|
import { useFieldControl } from '../form-field/context'
|
|
8
10
|
import { injectInputGroup } from '../input-group/context'
|
|
@@ -13,14 +15,16 @@
|
|
|
13
15
|
type InputVariants,
|
|
14
16
|
} from '../input/input.variants'
|
|
15
17
|
import SelectList from './SelectList.vue'
|
|
16
|
-
import {
|
|
18
|
+
import { selectButton, selectClearSlot, selectHost, selectValue } from './select.variants'
|
|
19
|
+
import { useClearTransition } from './composables/useClearTransition'
|
|
17
20
|
import { flattenOptions, type SelectItems, type SelectOption } from './types'
|
|
18
21
|
|
|
19
22
|
defineOptions({ name: 'HnSelect', inheritAttrs: false })
|
|
20
23
|
|
|
21
24
|
const props = defineProps<{
|
|
22
|
-
options: SelectItems
|
|
25
|
+
options: SelectItems<T>
|
|
23
26
|
placeholder?: string
|
|
27
|
+
clearable?: boolean
|
|
24
28
|
name?: string
|
|
25
29
|
required?: boolean
|
|
26
30
|
autocomplete?: string
|
|
@@ -31,17 +35,22 @@
|
|
|
31
35
|
class?: string
|
|
32
36
|
}>()
|
|
33
37
|
|
|
38
|
+
const emit = defineEmits<{ clear: [] }>()
|
|
39
|
+
|
|
34
40
|
const model = defineModel<string | number | null>()
|
|
35
41
|
const open = defineModel<boolean>('open', { default: false })
|
|
36
42
|
|
|
37
43
|
defineSlots<{
|
|
38
|
-
value(props: { option:
|
|
39
|
-
option(props: { option:
|
|
44
|
+
value?(props: { option: T }): unknown
|
|
45
|
+
option?(props: { option: T }): unknown
|
|
40
46
|
}>()
|
|
41
47
|
|
|
42
48
|
const t = useUiLocale()
|
|
43
49
|
const group = injectInputGroup()
|
|
50
|
+
const direction = useDirection()
|
|
44
51
|
const keyboard = ref(false)
|
|
52
|
+
const host = shallowRef<HTMLElement | null>(null)
|
|
53
|
+
const trigger = shallowRef<{ $el: HTMLElement } | null>(null)
|
|
45
54
|
|
|
46
55
|
const {
|
|
47
56
|
id: fieldId,
|
|
@@ -55,6 +64,18 @@
|
|
|
55
64
|
const selected = computed(() =>
|
|
56
65
|
flattenOptions(props.options).find(option => option.value === model.value),
|
|
57
66
|
)
|
|
67
|
+
const clearing = computed(
|
|
68
|
+
() => !!props.clearable && model.value != null && model.value !== '' && !disabled.value,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const { reserved: clearSpace, hooks: clearTransition } = useClearTransition(() => clearing.value)
|
|
72
|
+
|
|
73
|
+
function clear() {
|
|
74
|
+
if (disabled.value || model.value == null || model.value === '') return
|
|
75
|
+
model.value = null
|
|
76
|
+
emit('clear')
|
|
77
|
+
trigger.value?.$el.focus()
|
|
78
|
+
}
|
|
58
79
|
</script>
|
|
59
80
|
|
|
60
81
|
<template>
|
|
@@ -66,34 +87,56 @@
|
|
|
66
87
|
:required="props.required"
|
|
67
88
|
:autocomplete="props.autocomplete"
|
|
68
89
|
>
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
:
|
|
72
|
-
:aria-describedby="describedBy"
|
|
90
|
+
<div
|
|
91
|
+
ref="host"
|
|
92
|
+
:dir="direction"
|
|
73
93
|
data-hn-select
|
|
74
|
-
@keydown="keyboard = true"
|
|
75
|
-
@pointerdown="keyboard = false"
|
|
76
94
|
:data-invalid="invalid ? '' : undefined"
|
|
77
95
|
:data-disabled="disabled ? '' : undefined"
|
|
78
|
-
:aria-invalid="invalid || undefined"
|
|
79
96
|
:class="
|
|
80
97
|
cn(
|
|
81
98
|
group ? inputEmbedded() : inputHost({ variant: props.variant, size: props.size }),
|
|
82
|
-
|
|
99
|
+
selectHost(),
|
|
83
100
|
props.class,
|
|
84
101
|
)
|
|
85
102
|
"
|
|
86
103
|
>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
<SelectTrigger
|
|
105
|
+
ref="trigger"
|
|
106
|
+
v-bind="$attrs"
|
|
107
|
+
:reference="host ?? undefined"
|
|
108
|
+
:id="fieldId"
|
|
109
|
+
:aria-describedby="describedBy"
|
|
110
|
+
:aria-invalid="invalid || undefined"
|
|
111
|
+
data-hn-select-trigger
|
|
112
|
+
@keydown="keyboard = true"
|
|
113
|
+
@pointerdown="keyboard = false"
|
|
114
|
+
:class="selectButton()"
|
|
115
|
+
>
|
|
116
|
+
<span :class="selectValue({ clearing: clearSpace })">
|
|
117
|
+
<template v-if="selected">
|
|
118
|
+
<slot name="value" :option="selected">{{ selected.label }}</slot>
|
|
119
|
+
</template>
|
|
120
|
+
<template v-else>{{ props.placeholder ?? t.select.placeholder }}</template>
|
|
121
|
+
</span>
|
|
122
|
+
<span :class="inputAdornment()">
|
|
123
|
+
<DisclosureIcon />
|
|
124
|
+
</span>
|
|
125
|
+
</SelectTrigger>
|
|
126
|
+
<Transition
|
|
127
|
+
enter-active-class="hn-transition-base"
|
|
128
|
+
enter-from-class="scale-90 opacity-0"
|
|
129
|
+
leave-active-class="hn-transition"
|
|
130
|
+
leave-to-class="scale-90 opacity-0"
|
|
131
|
+
v-on="clearTransition"
|
|
132
|
+
>
|
|
133
|
+
<span v-if="clearing" :class="selectClearSlot()">
|
|
134
|
+
<InputAction data-hn-select-clear :label="t.common.clear" @click="clear">
|
|
135
|
+
<X />
|
|
136
|
+
</InputAction>
|
|
137
|
+
</span>
|
|
138
|
+
</Transition>
|
|
139
|
+
</div>
|
|
97
140
|
<SelectList :options="props.options" :keyboard="keyboard">
|
|
98
141
|
<template #option="slotProps">
|
|
99
142
|
<slot name="option" v-bind="slotProps" />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="T extends SelectOption = SelectOption">
|
|
2
2
|
import { Check } from '@lucide/vue'
|
|
3
3
|
import {
|
|
4
4
|
SelectContent,
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
defineOptions({ name: 'HnSelectList' })
|
|
27
27
|
|
|
28
|
-
const props = defineProps<{ options: SelectItems
|
|
28
|
+
const props = defineProps<{ options: SelectItems<T>; keyboard?: boolean }>()
|
|
29
29
|
|
|
30
30
|
const fresh = ref(false)
|
|
31
31
|
|
|
32
|
-
defineSlots<{ option(props: { option:
|
|
32
|
+
defineSlots<{ option?(props: { option: T }): unknown }>()
|
|
33
33
|
|
|
34
34
|
const t = useUiLocale()
|
|
35
35
|
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useClearTransition(visible: () => boolean) {
|
|
4
|
+
const leaving = ref(false)
|
|
5
|
+
const reserved = computed(() => visible() || leaving.value)
|
|
6
|
+
|
|
7
|
+
function beforeEnter(element: Element) {
|
|
8
|
+
leaving.value = false
|
|
9
|
+
element.removeAttribute('inert')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function beforeLeave(element: Element) {
|
|
13
|
+
leaving.value = true
|
|
14
|
+
element.setAttribute('inert', '')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function finishLeave() {
|
|
18
|
+
leaving.value = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
reserved,
|
|
23
|
+
hooks: { beforeEnter, beforeLeave, afterLeave: finishLeave, leaveCancelled: finishLeave },
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { tv } from '../../lib/tv'
|
|
2
|
+
import { inputActionSlot } from '../input/input.variants'
|
|
2
3
|
|
|
3
4
|
export const selectTrigger = tv({
|
|
4
5
|
base: [
|
|
@@ -9,6 +10,14 @@ export const selectTrigger = tv({
|
|
|
9
10
|
],
|
|
10
11
|
})
|
|
11
12
|
|
|
13
|
+
export const selectButton = tv({
|
|
14
|
+
base: [
|
|
15
|
+
'group/hn-disclosure flex h-full min-w-0 flex-1 cursor-pointer items-center bg-transparent ps-[var(--hn-input-px)] pe-0 text-start text-inherit outline-none [font:inherit]',
|
|
16
|
+
'data-placeholder:text-faint data-placeholder:font-normal disabled:cursor-not-allowed',
|
|
17
|
+
'[&_svg]:size-[var(--hn-input-icon)] [&_svg]:shrink-0',
|
|
18
|
+
],
|
|
19
|
+
})
|
|
20
|
+
|
|
12
21
|
export const selectContent = tv({
|
|
13
22
|
base: [
|
|
14
23
|
'hn-anim-pop z-(--hn-z-overlay) flex w-[var(--reka-select-trigger-width)] min-w-40 flex-col overflow-hidden shadow-md outline-none',
|
|
@@ -40,3 +49,18 @@ export const selectEmpty = tv({
|
|
|
40
49
|
export const selectListBody = tv({
|
|
41
50
|
base: 'flex flex-col p-1',
|
|
42
51
|
})
|
|
52
|
+
|
|
53
|
+
export const selectHost = tv({
|
|
54
|
+
base: 'relative cursor-pointer',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const selectValue = tv({
|
|
58
|
+
base: 'min-w-0 flex-1 truncate',
|
|
59
|
+
variants: {
|
|
60
|
+
clearing: { true: 'pe-[var(--hn-input-h)]' },
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const selectClearSlot = tv({
|
|
65
|
+
base: [inputActionSlot(), 'absolute inset-y-0 end-[var(--hn-input-h)]'],
|
|
66
|
+
})
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type SelectOption<T extends object = object> = {
|
|
2
2
|
value: string | number
|
|
3
3
|
label: string
|
|
4
4
|
description?: string
|
|
5
5
|
disabled?: boolean
|
|
6
|
-
}
|
|
6
|
+
} & T
|
|
7
7
|
|
|
8
|
-
export interface SelectOptionGroup {
|
|
8
|
+
export interface SelectOptionGroup<T extends SelectOption = SelectOption> {
|
|
9
9
|
label: string
|
|
10
|
-
options:
|
|
10
|
+
options: T[]
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export type SelectItems = Array<
|
|
13
|
+
export type SelectItems<T extends SelectOption = SelectOption> = Array<T | SelectOptionGroup<T>>
|
|
14
14
|
|
|
15
|
-
export function isOptionGroup
|
|
16
|
-
|
|
15
|
+
export function isOptionGroup<T extends SelectOption>(
|
|
16
|
+
item: T | SelectOptionGroup<T>,
|
|
17
|
+
): item is SelectOptionGroup<T> {
|
|
18
|
+
return !('value' in item) && 'options' in item
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export function flattenOptions(items: SelectItems):
|
|
21
|
+
export function flattenOptions<T extends SelectOption>(items: SelectItems<T>): T[] {
|
|
20
22
|
return items.flatMap(item => (isOptionGroup(item) ? item.options : [item]))
|
|
21
23
|
}
|