pgo-uiux2 1.0.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/.env +1 -0
- package/.env.production +1 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +3 -0
- package/BUTTON_GUIDE.md +257 -0
- package/README.md +49 -0
- package/THEME_REFERENCE.md +310 -0
- package/eslint.config.ts +27 -0
- package/index.html +13 -0
- package/package.json +85 -0
- package/public/favicon.ico +0 -0
- package/src/App.vue +368 -0
- package/src/assets/fonts/Faruma.ttf +0 -0
- package/src/components/examples/AppBarExample.vue +101 -0
- package/src/components/examples/AvatarExample.vue +47 -0
- package/src/components/examples/BannerExample.vue +287 -0
- package/src/components/examples/BaseInputExample.vue +25 -0
- package/src/components/examples/BreadcrumbExample.vue +53 -0
- package/src/components/examples/CardExample.vue +77 -0
- package/src/components/examples/ChipExample.vue +225 -0
- package/src/components/examples/DatePickerExample.vue +31 -0
- package/src/components/examples/DropdownExample.vue +84 -0
- package/src/components/examples/EditorExample.vue +200 -0
- package/src/components/examples/ExpansionPanelExample.vue +42 -0
- package/src/components/examples/FileUploadExample.vue +40 -0
- package/src/components/examples/FormExample.vue +121 -0
- package/src/components/examples/HugeTest.vue +8 -0
- package/src/components/examples/LayoutContainerExample.vue +80 -0
- package/src/components/examples/ModalExample.vue +82 -0
- package/src/components/examples/NavDrawerExample.vue +170 -0
- package/src/components/examples/NumberFieldExample.vue +145 -0
- package/src/components/examples/RadioButtonExample.vue +161 -0
- package/src/components/examples/SearchExample.vue +322 -0
- package/src/components/examples/SelectExample.vue +121 -0
- package/src/components/examples/StackedTableViewExample.vue +53 -0
- package/src/components/examples/TabExample.vue +336 -0
- package/src/components/examples/TableExample.vue +228 -0
- package/src/components/examples/TextFieldExample.vue +181 -0
- package/src/components/examples/TextareaExample.vue +173 -0
- package/src/components/examples/ThemeToggle.vue +50 -0
- package/src/components/examples/TimelineExample.vue +66 -0
- package/src/components/examples/TipTapEditorExample.vue +20 -0
- package/src/components/examples/TooltipExample.vue +53 -0
- package/src/components/examples/VueDatePickerShowcase.vue +214 -0
- package/src/components/examples/_DatePickerExample.vue +33 -0
- package/src/components/examples/__FormExample.vue +77 -0
- package/src/components/index.ts +25 -0
- package/src/components/pgo/AppBar.vue +347 -0
- package/src/components/pgo/Avatar.vue +139 -0
- package/src/components/pgo/Banner.vue +300 -0
- package/src/components/pgo/Breadcrumb.vue +101 -0
- package/src/components/pgo/Button.vue +171 -0
- package/src/components/pgo/Card.vue +178 -0
- package/src/components/pgo/ConfirmationModel.vue +32 -0
- package/src/components/pgo/DataTable.vue +845 -0
- package/src/components/pgo/DatePicker/CalendarPanel.vue +43 -0
- package/src/components/pgo/DatePicker/__DatePicker.vue +122 -0
- package/src/components/pgo/DatePicker/types.ts +11 -0
- package/src/components/pgo/DatePicker/useCalendar.ts +39 -0
- package/src/components/pgo/DatePicker/useDatePicker.ts +31 -0
- package/src/components/pgo/Deprecated/ToastContainer.vue +51 -0
- package/src/components/pgo/Deprecated/ToastItem.vue +55 -0
- package/src/components/pgo/Dropdown.vue +296 -0
- package/src/components/pgo/DropdownItem.vue +40 -0
- package/src/components/pgo/Editor.vue +511 -0
- package/src/components/pgo/ExpansionPanel.vue +185 -0
- package/src/components/pgo/Footer.vue +39 -0
- package/src/components/pgo/HeroIcon.vue +124 -0
- package/src/components/pgo/InputSearch.vue +194 -0
- package/src/components/pgo/LayoutContainer.vue +104 -0
- package/src/components/pgo/Main.vue +37 -0
- package/src/components/pgo/Modal.vue +273 -0
- package/src/components/pgo/NavDrawer.vue +127 -0
- package/src/components/pgo/NavDrawerItem.vue +161 -0
- package/src/components/pgo/NavigationDrawer.vue +849 -0
- package/src/components/pgo/OLDNavDrawer.vue +661 -0
- package/src/components/pgo/OldAppBar.vue +223 -0
- package/src/components/pgo/PApp.vue +102 -0
- package/src/components/pgo/Pagination.vue +242 -0
- package/src/components/pgo/Search copy.vue +310 -0
- package/src/components/pgo/Search.vue +411 -0
- package/src/components/pgo/StackedTableView.vue +167 -0
- package/src/components/pgo/Tab.vue +617 -0
- package/src/components/pgo/TestInput.vue +395 -0
- package/src/components/pgo/Timeline.vue +367 -0
- package/src/components/pgo/TimelineItem.vue +80 -0
- package/src/components/pgo/TipTapEditor.vue +315 -0
- package/src/components/pgo/Tooltip.NOTES.md +12 -0
- package/src/components/pgo/Tooltip.PROPS.md +21 -0
- package/src/components/pgo/Tooltip.vue +281 -0
- package/src/components/pgo/base/Base.vue +444 -0
- package/src/components/pgo/buttons/Chip.vue +324 -0
- package/src/components/pgo/buttons/ChipGroup.vue +224 -0
- package/src/components/pgo/buttons/Radio.vue +424 -0
- package/src/components/pgo/filters/FilterSection.vue +188 -0
- package/src/components/pgo/filters/Searchbar.vue +216 -0
- package/src/components/pgo/forms/DynamicForm.vue +45 -0
- package/src/components/pgo/forms/Form.vue +132 -0
- package/src/components/pgo/index.ts +15 -0
- package/src/components/pgo/inputs/Checkbox.vue +320 -0
- package/src/components/pgo/inputs/DatePicker.vue +395 -0
- package/src/components/pgo/inputs/FileUpload.vue +326 -0
- package/src/components/pgo/inputs/NumberField.vue +243 -0
- package/src/components/pgo/inputs/Radio.vue +162 -0
- package/src/components/pgo/inputs/RadioGroup.vue +188 -0
- package/src/components/pgo/inputs/Select.vue +535 -0
- package/src/components/pgo/inputs/TextField.vue +194 -0
- package/src/components/pgo/inputs/Textarea.vue +181 -0
- package/src/main.js +12 -0
- package/src/pgo-components/_index.js +31 -0
- package/src/pgo-components/assets/fonts/Faruma.ttf +0 -0
- package/src/pgo-components/assets/fonts/logo.png +0 -0
- package/src/pgo-components/composables/useTheme.js +10 -0
- package/src/pgo-components/directives/tooltip-directive.ts +393 -0
- package/src/pgo-components/index.js +96 -0
- package/src/pgo-components/lib/componentConfig.js +147 -0
- package/src/pgo-components/lib/core/composables/_useCalendar.ts +127 -0
- package/src/pgo-components/lib/core/composables/useDefaults.ts +15 -0
- package/src/pgo-components/lib/core/composables/useLanguageSelect.js +0 -0
- package/src/pgo-components/lib/core/composables/useRtl.ts +12 -0
- package/src/pgo-components/lib/core/defaults/createDefaults.ts +5 -0
- package/src/pgo-components/lib/core/defaults/defaults.ts +7 -0
- package/src/pgo-components/lib/core/rtl/rtl.ts +3 -0
- package/src/pgo-components/lib/core/rtl/setRtl.ts +19 -0
- package/src/pgo-components/lib/drawerState.ts +3 -0
- package/src/pgo-components/lib/i18n/defaultLables.js +71 -0
- package/src/pgo-components/lib/i18n/i18nPlugin.js +52 -0
- package/src/pgo-components/lib/i18n/useI18n.js +35 -0
- package/src/pgo-components/lib/index.ts +38 -0
- package/src/pgo-components/pages/Component.vue +7 -0
- package/src/pgo-components/pages/ComponentRenderer.vue +85 -0
- package/src/pgo-components/pages/Home.vue +130 -0
- package/src/pgo-components/pages/ListView.vue +370 -0
- package/src/pgo-components/pages/Page1.vue +296 -0
- package/src/pgo-components/pages/_Page1.vue +180 -0
- package/src/pgo-components/plugins/SnackBar.vue +251 -0
- package/src/pgo-components/plugins/SnackBarContainer.vue +53 -0
- package/src/pgo-components/plugins/SnackBarPlugin.ts +136 -0
- package/src/pgo-components/plugins/theme-plugin.js +114 -0
- package/src/pgo-components/plugins/types.ts +46 -0
- package/src/pgo-components/plugins/useSnackBar.js +11 -0
- package/src/pgo-components/plugins/useSnackBar.ts +21 -0
- package/src/pgo-components/plugins/validation-plugin.js +11 -0
- package/src/pgo-components/services/Entry.json +813 -0
- package/src/pgo-components/services/axios.js +54 -0
- package/src/pgo-components/services/data.json +90 -0
- package/src/pgo-components/services/person.json +260 -0
- package/src/pgo-components/services/toast.ts +44 -0
- package/src/pgo-components/styles/global.css +234 -0
- package/src/pgo-components/styles/reset.css +96 -0
- package/src/pgo-components/styles/tokens.css +18 -0
- package/src/pgo-components/styles/utilities/border-radius.css +57 -0
- package/src/pgo-components/styles/utilities/borders.css +85 -0
- package/src/pgo-components/styles/utilities/colors.css +38 -0
- package/src/pgo-components/styles/utilities/cursor.css +19 -0
- package/src/pgo-components/styles/utilities/display.css +78 -0
- package/src/pgo-components/styles/utilities/elevation.css +33 -0
- package/src/pgo-components/styles/utilities/flex.css +403 -0
- package/src/pgo-components/styles/utilities/float.css +41 -0
- package/src/pgo-components/styles/utilities/hover.css +9 -0
- package/src/pgo-components/styles/utilities/index.css +18 -0
- package/src/pgo-components/styles/utilities/opacity.css +27 -0
- package/src/pgo-components/styles/utilities/overflow.css +26 -0
- package/src/pgo-components/styles/utilities/palette.css +515 -0
- package/src/pgo-components/styles/utilities/position.css +14 -0
- package/src/pgo-components/styles/utilities/sizing.css +70 -0
- package/src/pgo-components/styles/utilities/spacing.css +578 -0
- package/src/pgo-components/styles/utilities/transitions.css +58 -0
- package/src/pgo-components/styles/utilities/typography.css +91 -0
- package/src/pgo-components/styles/utilities/z-index.css +11 -0
- package/src/pgo-components/tokens/index.js +337 -0
- package/src/router/index.js +88 -0
- package/src/shims-vue.d.ts +14 -0
- package/src/validations/validationRules.js +50 -0
- package/tailwind.config.js +73 -0
- package/test.php +5 -0
- package/tsconfig.json +25 -0
- package/ui +31 -0
- package/ui.pgo.mv.conf +18 -0
- package/vite.config.js +42 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label
|
|
3
|
+
:class="['inline-flex gap-2 items-center', { 'opacity-50 cursor-not-allowed': disabled, 'cursor-pointer': !disabled }]"
|
|
4
|
+
>
|
|
5
|
+
<input
|
|
6
|
+
ref="inputRef"
|
|
7
|
+
v-bind="attrs"
|
|
8
|
+
class="sr-only"
|
|
9
|
+
type="radio"
|
|
10
|
+
:name="name"
|
|
11
|
+
:value="value"
|
|
12
|
+
:disabled="disabled"
|
|
13
|
+
:checked="isChecked"
|
|
14
|
+
@change="onChange"
|
|
15
|
+
@keydown.enter.prevent="onChange"
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
<!-- Outer circle -->
|
|
19
|
+
<span
|
|
20
|
+
:class="[
|
|
21
|
+
'relative flex items-center justify-center rounded-full border transition-colors duration-150',
|
|
22
|
+
sizeOuter,
|
|
23
|
+
isChecked ? checkedOuter : uncheckedOuter,
|
|
24
|
+
focusClass
|
|
25
|
+
]"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
>
|
|
28
|
+
<!-- inner dot -->
|
|
29
|
+
<span
|
|
30
|
+
v-if="isChecked"
|
|
31
|
+
:class="['rounded-full bg-white transition-transform', sizeInner]"
|
|
32
|
+
/>
|
|
33
|
+
</span>
|
|
34
|
+
|
|
35
|
+
<!-- Label -->
|
|
36
|
+
<span v-if="$slots.default || label" class="select-none ml-2 text-sm" :class="labelClass">
|
|
37
|
+
<slot>{{ selectedlabel }}</slot>
|
|
38
|
+
</span>
|
|
39
|
+
</label>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { computed, ref, inject, useAttrs, withDefaults, defineProps, defineEmits } from 'vue'
|
|
44
|
+
import { useLanguageSelected } from '../../../pgo-components/lib/componentConfig'
|
|
45
|
+
|
|
46
|
+
const { language } = inject<{ language: { value: string } }>('i18n') || { language: { value: 'en' } }
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<{
|
|
49
|
+
modelValue?: string|number|boolean|null,
|
|
50
|
+
value: string|number|boolean,
|
|
51
|
+
name?: string,
|
|
52
|
+
label?: string|object,
|
|
53
|
+
disabled?: boolean,
|
|
54
|
+
size?: 'sm'|'md'|'lg',
|
|
55
|
+
lang?: string,
|
|
56
|
+
dir?: string,
|
|
57
|
+
color?: string
|
|
58
|
+
}>(), {
|
|
59
|
+
size: 'md',
|
|
60
|
+
disabled: false,
|
|
61
|
+
color: 'primary'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const emit = defineEmits(['update:modelValue', 'change'])
|
|
65
|
+
|
|
66
|
+
const attrs = useAttrs()
|
|
67
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
68
|
+
|
|
69
|
+
const isChecked = computed(() => {
|
|
70
|
+
// Loose equality to support number/string matching
|
|
71
|
+
// but keep it predictable; change if strict equality preferred
|
|
72
|
+
// (e.g. Number/ String conversion)
|
|
73
|
+
// eslint-disable-next-line eqeqeq
|
|
74
|
+
return props.modelValue == props.value
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const selectedDirection = computed(() => {
|
|
78
|
+
let selected = ''
|
|
79
|
+
if (props.dir == 'rtl') selected = 'rtl'
|
|
80
|
+
else if (props.lang == 'dv') selected = 'rtl'
|
|
81
|
+
else if (props.dir == 'ltr') selected = 'ltr'
|
|
82
|
+
else if (props.lang == 'en') selected = 'ltr'
|
|
83
|
+
else selected = ''
|
|
84
|
+
return selected
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const selectLanguage = computed(() => {
|
|
88
|
+
let Selected = ''
|
|
89
|
+
if (props.lang != ''){
|
|
90
|
+
Selected = props.lang
|
|
91
|
+
}
|
|
92
|
+
else if (props.dir == 'rtl') {
|
|
93
|
+
Selected = 'dv'
|
|
94
|
+
}
|
|
95
|
+
else if (props.dir == 'ltr') {
|
|
96
|
+
Selected = 'en'
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
Selected = language.value
|
|
100
|
+
}
|
|
101
|
+
console.log('selectLanguage in Card222:', Selected)
|
|
102
|
+
return (Selected === 'dv') ? 'dv' : 'en'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const faruma = computed(() => {
|
|
106
|
+
if (selectLanguage.value == 'dv') return 'faruma'
|
|
107
|
+
return ''
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const selectedlabel = computed(() => {
|
|
111
|
+
return useLanguageSelected(props.label, selectLanguage.value, '')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const onChange = (e?: Event) => {
|
|
115
|
+
if (props.disabled) return
|
|
116
|
+
emit('update:modelValue', props.value)
|
|
117
|
+
emit('change', props.value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Size classes
|
|
121
|
+
const sizeOuter = computed(() => {
|
|
122
|
+
switch (props.size) {
|
|
123
|
+
case 'sm': return 'w-4 h-4'
|
|
124
|
+
case 'lg': return 'w-6 h-6'
|
|
125
|
+
default: return 'w-5 h-5'
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
const sizeInner = computed(() => {
|
|
129
|
+
switch (props.size) {
|
|
130
|
+
case 'sm': return 'w-2 h-2'
|
|
131
|
+
case 'lg': return 'w-3 h-3'
|
|
132
|
+
default: return 'w-2.5 h-2.5'
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Color mapping (common tokens in your project). Accept a Tailwind class string too.
|
|
137
|
+
const colorClass = computed(() => {
|
|
138
|
+
const c = String(props.color || 'primary')
|
|
139
|
+
// If user passed a tailwind color class like "bg-red-500" or "text-red-500", use it.
|
|
140
|
+
if (/^(bg-|text-|border-)/.test(c)) return c
|
|
141
|
+
// Map simple tokens to classes — adapt to your project's tokens
|
|
142
|
+
const map: Record<string, string> = {
|
|
143
|
+
primary: 'bg-primary',
|
|
144
|
+
secondary: 'bg-secondary',
|
|
145
|
+
success: 'bg-green-500',
|
|
146
|
+
danger: 'bg-red-500',
|
|
147
|
+
info: 'bg-sky-500',
|
|
148
|
+
warning: 'bg-amber-500'
|
|
149
|
+
}
|
|
150
|
+
return map[c] || 'bg-primary'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Checked / unchecked classes
|
|
154
|
+
const checkedOuter = computed(() => `${colorClass.value} border-transparent`)
|
|
155
|
+
const uncheckedOuter = computed(() => 'bg-white border-gray-300 dark:border-gray-600')
|
|
156
|
+
|
|
157
|
+
// Focus ring
|
|
158
|
+
const focusClass = 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-primary'
|
|
159
|
+
|
|
160
|
+
// Optional label color
|
|
161
|
+
const labelClass = computed(() => props.disabled ? 'text-gray-400' : 'text-text')
|
|
162
|
+
</script>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:id="id"
|
|
4
|
+
role="radiogroup"
|
|
5
|
+
:aria-labelledby="ariaLabelledby"
|
|
6
|
+
:aria-disabled="disabled ? 'true' : undefined"
|
|
7
|
+
:class="['inline-flex', direction === 'vertical' ? 'flex-col' : 'items-center', gapClass]"
|
|
8
|
+
@keydown="onKeydown"
|
|
9
|
+
>
|
|
10
|
+
<!-- Render from options prop if provided -->
|
|
11
|
+
<template v-if="Array.isArray(options) && options.length > 0">
|
|
12
|
+
<label
|
|
13
|
+
v-for="(opt, idx) in options"
|
|
14
|
+
:key="`opt-${idx}-${String(opt?.value)}`"
|
|
15
|
+
:class="['inline-flex items-center', { 'opacity-50 pointer-events-none': disabled || opt.disabled }]"
|
|
16
|
+
>
|
|
17
|
+
<Radio
|
|
18
|
+
ref="setOptionRef(idx)"
|
|
19
|
+
v-model="internalValue"
|
|
20
|
+
:value="opt.value"
|
|
21
|
+
:name="groupName"
|
|
22
|
+
:disabled="disabled || !!opt.disabled"
|
|
23
|
+
:size="size"
|
|
24
|
+
:color="opt.color || color"
|
|
25
|
+
@change="onChange(opt.value)"
|
|
26
|
+
>
|
|
27
|
+
<template #default>
|
|
28
|
+
<div class="flex flex-col">
|
|
29
|
+
<div class="select-none ml-2 text-sm" :class="labelClass">{{ opt.label }}</div>
|
|
30
|
+
<small v-if="opt.description" class="text-xs text-gray-500 ml-2">{{ opt.description }}</small>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
</Radio>
|
|
34
|
+
</label>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<!-- Fallback: render default slot - expected to contain Radio children -->
|
|
38
|
+
<slot v-else />
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script setup lang="ts">
|
|
43
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
44
|
+
import Radio from './Radio.vue'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
Props
|
|
48
|
+
- modelValue: the v-model value
|
|
49
|
+
- options: optional array of { label, value, disabled?, description?, color? }
|
|
50
|
+
- name: optional group name (if not provided a unique name is generated)
|
|
51
|
+
- disabled, size, color, direction
|
|
52
|
+
- ariaLabelledby: optional id for label element (for accessibility)
|
|
53
|
+
*/
|
|
54
|
+
const props = defineProps<{
|
|
55
|
+
modelValue?: unknown,
|
|
56
|
+
options?: Array<{
|
|
57
|
+
label: string,
|
|
58
|
+
value: any,
|
|
59
|
+
disabled?: boolean,
|
|
60
|
+
description?: string,
|
|
61
|
+
color?: string
|
|
62
|
+
}>,
|
|
63
|
+
name?: string,
|
|
64
|
+
disabled?: boolean,
|
|
65
|
+
size?: 'sm'|'md'|'lg',
|
|
66
|
+
color?: string,
|
|
67
|
+
direction?: 'horizontal'|'vertical',
|
|
68
|
+
gap?: string,
|
|
69
|
+
ariaLabelledby?: string
|
|
70
|
+
}>()
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits<{
|
|
73
|
+
(e: 'update:modelValue', value: any): void
|
|
74
|
+
(e: 'change', value: any): void
|
|
75
|
+
}>()
|
|
76
|
+
|
|
77
|
+
// local id + group name
|
|
78
|
+
const uid = Math.random().toString(36).slice(2, 9)
|
|
79
|
+
const id = `radio-group-${uid}`
|
|
80
|
+
const groupName = computed(() => props.name || `rg-${uid}`)
|
|
81
|
+
const ariaLabelledby = computed(() => props.ariaLabelledby || undefined)
|
|
82
|
+
|
|
83
|
+
// layout helpers
|
|
84
|
+
const direction = computed(() => props.direction || 'horizontal')
|
|
85
|
+
const gapClass = computed(() => {
|
|
86
|
+
if (props.gap) return props.gap
|
|
87
|
+
return direction.value === 'vertical' ? 'space-y-2' : 'space-x-4'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// simple defaults
|
|
91
|
+
const size = computed(() => props.size || 'md')
|
|
92
|
+
const color = computed(() => props.color || 'primary')
|
|
93
|
+
const disabled = computed(() => !!props.disabled)
|
|
94
|
+
const labelClass = computed(() => disabled.value ? 'text-gray-400' : 'text-text')
|
|
95
|
+
|
|
96
|
+
// internal v-model proxy
|
|
97
|
+
const internalValue = ref(props.modelValue)
|
|
98
|
+
watch(() => props.modelValue, v => { internalValue.value = v }, { immediate: true })
|
|
99
|
+
watch(internalValue, (val) => {
|
|
100
|
+
emit('update:modelValue', val)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// refs to child radios for keyboard navigation
|
|
104
|
+
const optionRefs = ref<Array<HTMLElement | null>>([])
|
|
105
|
+
|
|
106
|
+
function setOptionRef(index: number) {
|
|
107
|
+
return (el: HTMLElement | null) => {
|
|
108
|
+
optionRefs.value[index] = el
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// update:modelValue already emitted via watcher above; this emits a change event too
|
|
113
|
+
function onChange(val: any) {
|
|
114
|
+
return () => {
|
|
115
|
+
emit('change', val)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Keyboard navigation: ArrowRight/ArrowDown => next, ArrowLeft/ArrowUp => prev, Home/End
|
|
120
|
+
function focusIndex(index: number) {
|
|
121
|
+
const el = optionRefs.value[index]
|
|
122
|
+
if (el && typeof (el as any).focus === 'function') {
|
|
123
|
+
;(el as any).focus()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findEnabledIndex(start = 0, step = 1) {
|
|
128
|
+
const len = optionRefs.value.length
|
|
129
|
+
if (len === 0) return -1
|
|
130
|
+
let i = (start + len) % len
|
|
131
|
+
for (let iter = 0; iter < len; iter++) {
|
|
132
|
+
const idx = (i + iter * step + len) % len
|
|
133
|
+
const el = optionRefs.value[idx]
|
|
134
|
+
if (!el) continue
|
|
135
|
+
const radioEl = el.querySelector ? el.querySelector('input[type="radio"]') : el
|
|
136
|
+
if (!radioEl) continue
|
|
137
|
+
if ((radioEl as HTMLInputElement).disabled) continue
|
|
138
|
+
return idx
|
|
139
|
+
}
|
|
140
|
+
return -1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function onKeydown(e: KeyboardEvent) {
|
|
144
|
+
if (!Array.isArray(props.options) || props.options.length === 0) return
|
|
145
|
+
const len = props.options.length
|
|
146
|
+
if (len === 0) return
|
|
147
|
+
|
|
148
|
+
const currentIndex = props.options.findIndex(o => o.value == internalValue.value)
|
|
149
|
+
|
|
150
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
151
|
+
e.preventDefault()
|
|
152
|
+
const next = findEnabledIndex((currentIndex >= 0 ? currentIndex : -1) + 1, 1)
|
|
153
|
+
if (next >= 0) {
|
|
154
|
+
internalValue.value = props.options[next].value
|
|
155
|
+
await nextTick()
|
|
156
|
+
focusIndex(next)
|
|
157
|
+
emit('change', internalValue.value)
|
|
158
|
+
}
|
|
159
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
160
|
+
e.preventDefault()
|
|
161
|
+
const prev = findEnabledIndex((currentIndex >= 0 ? currentIndex : len) - 1, -1)
|
|
162
|
+
if (prev >= 0) {
|
|
163
|
+
internalValue.value = props.options[prev].value
|
|
164
|
+
await nextTick()
|
|
165
|
+
focusIndex(prev)
|
|
166
|
+
emit('change', internalValue.value)
|
|
167
|
+
}
|
|
168
|
+
} else if (e.key === 'Home') {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
const first = findEnabledIndex(0, 1)
|
|
171
|
+
if (first >= 0) {
|
|
172
|
+
internalValue.value = props.options[first].value
|
|
173
|
+
await nextTick()
|
|
174
|
+
focusIndex(first)
|
|
175
|
+
emit('change', internalValue.value)
|
|
176
|
+
}
|
|
177
|
+
} else if (e.key === 'End') {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
const last = findEnabledIndex(len - 1, -1)
|
|
180
|
+
if (last >= 0) {
|
|
181
|
+
internalValue.value = props.options[last].value
|
|
182
|
+
await nextTick()
|
|
183
|
+
focusIndex(last)
|
|
184
|
+
emit('change', internalValue.value)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</script>
|