@flux-ui/components 3.0.0-next.72 → 3.0.0-next.74
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/component/FluxFormField.vue.d.ts +2 -2
- package/dist/component/FluxKanbanColumn.vue.d.ts +1 -1
- package/dist/component/FluxSegmentedControl.vue.d.ts +18 -8
- package/dist/component/FluxSegmentedControlItem.vue.d.ts +20 -0
- package/dist/component/FluxSpacing.vue.d.ts +2 -1
- package/dist/component/index.d.ts +1 -1
- package/dist/component/primitive/FilterMenuRenderer.d.ts +3 -3
- package/dist/composable/index.d.ts +1 -0
- package/dist/composable/useSegmentedControlInjection.d.ts +2 -0
- package/dist/data/di.d.ts +10 -1
- package/dist/index.css +398 -368
- package/dist/index.js +1264 -1164
- package/dist/index.js.map +1 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/sanitizeUrl.d.ts +7 -0
- package/package.json +9 -9
- package/src/component/FluxBoxedIcon.vue +1 -1
- package/src/component/FluxCalendar.vue +6 -6
- package/src/component/FluxColorPicker.vue +6 -6
- package/src/component/FluxDropZone.vue +0 -1
- package/src/component/FluxDynamicView.vue +2 -2
- package/src/component/FluxExpandableGroup.vue +0 -1
- package/src/component/FluxFilterBase.vue +11 -11
- package/src/component/FluxFormField.vue +4 -2
- package/src/component/FluxFormFieldAddition.vue +2 -2
- package/src/component/FluxInfo.vue +2 -2
- package/src/component/FluxInfoStack.vue +2 -2
- package/src/component/FluxKanbanColumn.vue +3 -5
- package/src/component/FluxLayerPane.vue +1 -1
- package/src/component/FluxPagination.vue +4 -4
- package/src/component/FluxPressable.vue +14 -5
- package/src/component/FluxSegmentedControl.vue +64 -67
- package/src/component/FluxSegmentedControlItem.vue +98 -0
- package/src/component/FluxSpacing.vue +5 -1
- package/src/component/FluxSplitButton.vue +2 -2
- package/src/component/FluxTable.vue +3 -1
- package/src/component/FluxToolbar.vue +1 -1
- package/src/component/index.ts +1 -1
- package/src/component/primitive/DialogLayout.vue +3 -3
- package/src/component/primitive/FilterBadge.vue +11 -2
- package/src/component/primitive/FilterMenuRenderer.ts +4 -4
- package/src/composable/index.ts +1 -0
- package/src/composable/useSegmentedControlInjection.ts +13 -0
- package/src/css/component/Form.module.scss +2 -2
- package/src/css/component/SegmentedControl.module.scss +53 -24
- package/src/css/component/Spinner.module.scss +1 -0
- package/src/css/component/Visual.module.scss +1 -0
- package/src/css/mixin/tree-node.scss +3 -3
- package/src/data/di.ts +13 -1
- package/src/data/iconRegistry.ts +1 -1
- package/src/util/createDialogRenderer.ts +1 -1
- package/src/util/index.ts +1 -0
- package/src/util/sanitizeUrl.ts +40 -0
- package/dist/component/FluxSegmentedView.vue.d.ts +0 -9
- package/src/component/FluxSegmentedView.vue +0 -15
package/dist/util/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { default as createDialogRenderer } from './createDialogRenderer';
|
|
|
2
2
|
export { default as createLabelForDateRange } from './createLabelForDateRange';
|
|
3
3
|
export { default as defineFilter, type FluxFilterDefinitionFactory } from './defineFilter';
|
|
4
4
|
export { generateMultiOptionsLabel, isFluxFilterOptionHeader, isFluxFilterOptionItem, isResettable, pickFilterCommon } from './filter';
|
|
5
|
+
export { default as sanitizeUrl } from './sanitizeUrl';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes a URL intended for an `href` attribute. Returns `undefined` when the
|
|
3
|
+
* URL uses a dangerous scheme (`javascript:`, `vbscript:`, `data:`) so the attribute
|
|
4
|
+
* is omitted instead of becoming a script-execution or data-injection vector.
|
|
5
|
+
* Safe and relative URLs are returned unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export default function (href?: string): string | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flux-ui/components",
|
|
3
3
|
"description": "A set of opiniated UI components.",
|
|
4
|
-
"version": "3.0.0-next.
|
|
4
|
+
"version": "3.0.0-next.74",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://github.com/sponsors/basmilius",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"**/dist/index.css"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@basmilius/common": "^3.
|
|
59
|
-
"@basmilius/utils": "^3.
|
|
60
|
-
"@flux-ui/internals": "3.0.0-next.
|
|
61
|
-
"@flux-ui/types": "3.0.0-next.
|
|
58
|
+
"@basmilius/common": "^3.40.0",
|
|
59
|
+
"@basmilius/utils": "^3.40.0",
|
|
60
|
+
"@flux-ui/internals": "3.0.0-next.74",
|
|
61
|
+
"@flux-ui/types": "3.0.0-next.74",
|
|
62
62
|
"@fortawesome/fontawesome-common-types": "^7.2.0",
|
|
63
63
|
"clsx": "^2.1.1",
|
|
64
64
|
"imask": "^7.6.1",
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"luxon": "^3.7.2",
|
|
69
|
-
"vue": "^3.6.0-beta.
|
|
69
|
+
"vue": "^3.6.0-beta.13"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@basmilius/vite-preset": "^3.
|
|
72
|
+
"@basmilius/vite-preset": "^3.40.0",
|
|
73
73
|
"@types/lodash-es": "^4.17.12",
|
|
74
74
|
"@types/luxon": "^3.7.1",
|
|
75
75
|
"@types/node": "^25.9.1",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@vue/tsconfig": "^0.9.1",
|
|
78
78
|
"sass-embedded": "^1.100.0",
|
|
79
79
|
"typescript": "^6.0.3",
|
|
80
|
-
"vite": "^8.0.
|
|
81
|
-
"vue-tsc": "^3.3.
|
|
80
|
+
"vite": "^8.0.16",
|
|
81
|
+
"vue-tsc": "^3.3.3"
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
color === 'danger' && $style.iconBoxedDanger,
|
|
8
8
|
color === 'info' && $style.iconBoxedInfo,
|
|
9
9
|
color === 'success' && $style.iconBoxedSuccess,
|
|
10
|
-
color === 'warning' && $style.iconBoxedWarning
|
|
10
|
+
color === 'warning' && $style.iconBoxedWarning
|
|
11
11
|
)"
|
|
12
12
|
:style="{
|
|
13
13
|
fontSize: size && `${size}px`
|
|
@@ -265,13 +265,13 @@
|
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
const timeGridDayCount = computed<1 | 2 | 7>(() => {
|
|
268
|
-
const
|
|
268
|
+
const resolved = unref(resolvedView);
|
|
269
269
|
|
|
270
|
-
if (
|
|
270
|
+
if (resolved === 'week') {
|
|
271
271
|
return 7;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
if (
|
|
274
|
+
if (resolved === 'two-days') {
|
|
275
275
|
return 2;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -521,13 +521,13 @@
|
|
|
521
521
|
return;
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
-
const
|
|
525
|
-
const delta = (
|
|
524
|
+
const resolved = unref(resolvedView);
|
|
525
|
+
const delta = (resolved === 'month' ? MONTH_KEY_DELTAS : TIME_GRID_KEY_DELTAS)[direction];
|
|
526
526
|
const newDate = currentDate.plus(delta);
|
|
527
527
|
|
|
528
528
|
emit('reschedule', {id, fromDate: currentDate, toDate: newDate});
|
|
529
529
|
|
|
530
|
-
if (
|
|
530
|
+
if (resolved === 'month') {
|
|
531
531
|
monthFocusedDate.value = newDate.startOf('day');
|
|
532
532
|
|
|
533
533
|
if (newDate.month !== unref(monthViewDate).month) {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
<div
|
|
37
37
|
:class="$style.colorPickerPreview"
|
|
38
38
|
:style="{
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
'--color': rgb
|
|
40
|
+
}"
|
|
41
41
|
aria-hidden="true"/>
|
|
42
42
|
|
|
43
43
|
<FluxFormField
|
|
@@ -134,10 +134,10 @@
|
|
|
134
134
|
import { blue500 } from '@flux-ui/internals';
|
|
135
135
|
import { computed, type ComputedRef, ref, unref, watch } from 'vue';
|
|
136
136
|
import { useTranslate } from '~flux/components/composable/private';
|
|
137
|
-
import CoordinatePicker from './primitive/CoordinatePicker.vue';
|
|
138
137
|
import FluxFormField from './FluxFormField.vue';
|
|
139
138
|
import FluxFormInput from './FluxFormInput.vue';
|
|
140
139
|
import FluxFormSlider from './FluxFormSlider.vue';
|
|
140
|
+
import CoordinatePicker from './primitive/CoordinatePicker.vue';
|
|
141
141
|
import $style from '~flux/components/css/component/Color.module.scss';
|
|
142
142
|
|
|
143
143
|
const modelValue = defineModel<string | [number, number, number]>({
|
|
@@ -180,9 +180,9 @@
|
|
|
180
180
|
|
|
181
181
|
const saturationValue = computed({
|
|
182
182
|
get: (): [number, number] => [unref(saturation), unref(value)],
|
|
183
|
-
set: ([
|
|
184
|
-
saturation.value =
|
|
185
|
-
value.value =
|
|
183
|
+
set: ([nextSaturation, nextValue]: [number, number]) => {
|
|
184
|
+
saturation.value = nextSaturation;
|
|
185
|
+
value.value = nextValue;
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
const flattenedFilters = computed(() => flattenVNodeTree(slots.filters?.() ?? []));
|
|
42
42
|
|
|
43
43
|
const buttons = computed(() => {
|
|
44
|
-
const
|
|
44
|
+
const result: Record<string, FluxFilterDefinition> = {};
|
|
45
45
|
const items = unref(flattenedFilters);
|
|
46
46
|
|
|
47
47
|
for (const item of items) {
|
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
result[definition.name] = definition;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
return
|
|
57
|
+
return result;
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
const filters = computed<Record<string, VNode>>(() => {
|
|
61
|
-
const
|
|
61
|
+
const result: { [key: string]: VNode; } = {};
|
|
62
62
|
const items = unref(flattenedFilters);
|
|
63
63
|
|
|
64
64
|
for (const item of items) {
|
|
@@ -68,33 +68,33 @@
|
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
result[definition.name] = item;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
return
|
|
74
|
+
return result;
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
const menuItems = computed<(FluxFilterDefinition | VNode)[][]>(() => {
|
|
78
|
-
const
|
|
78
|
+
const result: (FluxFilterDefinition | VNode)[][] = [[]];
|
|
79
79
|
const items = unref(flattenedFilters);
|
|
80
80
|
|
|
81
81
|
for (const item of items) {
|
|
82
82
|
if (getComponentName(item) === 'FluxSeparator') {
|
|
83
|
-
|
|
83
|
+
result.push([]);
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const definition = resolveDefinition(item);
|
|
88
88
|
|
|
89
89
|
if (definition) {
|
|
90
|
-
|
|
90
|
+
result[result.length - 1].push(definition);
|
|
91
91
|
continue;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
result[result.length - 1].push(item);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
return
|
|
97
|
+
return result;
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
watchEffect(() => {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
<label
|
|
4
4
|
:for="id"
|
|
5
5
|
:class="$style.formFieldHeader">
|
|
6
|
-
<span
|
|
6
|
+
<span
|
|
7
|
+
v-if="label"
|
|
8
|
+
:class="$style.formFieldLabel">
|
|
7
9
|
{{ label }}
|
|
8
10
|
</span>
|
|
9
11
|
|
|
@@ -61,7 +63,7 @@
|
|
|
61
63
|
readonly error?: string;
|
|
62
64
|
readonly hint?: string;
|
|
63
65
|
readonly isOptional?: boolean;
|
|
64
|
-
readonly label
|
|
66
|
+
readonly label?: string;
|
|
65
67
|
readonly maxLength?: number;
|
|
66
68
|
}>();
|
|
67
69
|
|
|
@@ -65,9 +65,7 @@
|
|
|
65
65
|
<footer
|
|
66
66
|
v-if="hasFooter"
|
|
67
67
|
:class="$style.kanbanColumnFooter">
|
|
68
|
-
<slot
|
|
69
|
-
v-if="hasFooter"
|
|
70
|
-
name="footer"/>
|
|
68
|
+
<slot name="footer"/>
|
|
71
69
|
</footer>
|
|
72
70
|
</div>
|
|
73
71
|
</template>
|
|
@@ -80,9 +78,9 @@
|
|
|
80
78
|
import { Comment, computed, onBeforeUnmount, onMounted, provide, Text, toRef, unref, useSlots, useTemplateRef, watch } from 'vue';
|
|
81
79
|
import { useDisabled, useKanbanInjection } from '~flux/components/composable';
|
|
82
80
|
import { FluxDisabledInjectionKey } from '~flux/components/data';
|
|
83
|
-
import $style from '~flux/components/css/component/Kanban.module.scss';
|
|
84
81
|
import FluxBadge from './FluxBadge.vue';
|
|
85
82
|
import FluxIcon from './FluxIcon.vue';
|
|
83
|
+
import $style from '~flux/components/css/component/Kanban.module.scss';
|
|
86
84
|
|
|
87
85
|
const {
|
|
88
86
|
columnId,
|
|
@@ -90,7 +88,7 @@
|
|
|
90
88
|
label
|
|
91
89
|
} = defineProps<{
|
|
92
90
|
readonly columnId: string | number;
|
|
93
|
-
readonly count
|
|
91
|
+
readonly count?: string | number;
|
|
94
92
|
readonly disabled?: boolean;
|
|
95
93
|
readonly icon?: FluxIconName;
|
|
96
94
|
readonly label: string;
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<template v-else>
|
|
37
37
|
<FluxPaginationButton
|
|
38
|
-
|
|
38
|
+
is-current
|
|
39
39
|
@click="prompt"
|
|
40
40
|
#before>
|
|
41
41
|
<strong>{{ page }}</strong>
|
|
@@ -140,12 +140,12 @@
|
|
|
140
140
|
fieldLabel: translate('flux.paginationNavigatePage')
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
const
|
|
143
|
+
const target = Number(pageStr);
|
|
144
144
|
|
|
145
|
-
if (isNaN(
|
|
145
|
+
if (isNaN(target) || target > unref(pages) || target <= 0) {
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
navigate(
|
|
149
|
+
navigate(target);
|
|
150
150
|
}
|
|
151
151
|
</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
v-if="componentType === 'route'"
|
|
4
4
|
v-bind="$attrs"
|
|
5
5
|
v-on="hoverListeners"
|
|
6
|
-
:rel="
|
|
6
|
+
:rel="resolvedRel"
|
|
7
7
|
:target="target"
|
|
8
8
|
:to="to as any"
|
|
9
9
|
@click="onClick($event)">
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
v-else-if="componentType === 'link'"
|
|
15
15
|
v-bind="$attrs"
|
|
16
16
|
v-on="hoverListeners"
|
|
17
|
-
:href="href"
|
|
18
|
-
:rel="
|
|
17
|
+
:href="sanitizeUrl(href)"
|
|
18
|
+
:rel="resolvedRel"
|
|
19
19
|
:target="target"
|
|
20
20
|
@click="onClick($event)">
|
|
21
21
|
<slot/>
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
lang="ts"
|
|
43
43
|
setup>
|
|
44
44
|
import type { FluxPressableType, FluxTo } from '@flux-ui/types';
|
|
45
|
-
import type
|
|
45
|
+
import { computed, type VNode } from 'vue';
|
|
46
|
+
import { sanitizeUrl } from '~flux/components/util';
|
|
46
47
|
|
|
47
48
|
const emit = defineEmits<{
|
|
48
49
|
click: [MouseEvent];
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
mouseleave: [MouseEvent];
|
|
51
52
|
}>();
|
|
52
53
|
|
|
53
|
-
defineProps<{
|
|
54
|
+
const {rel, target} = defineProps<{
|
|
54
55
|
readonly componentType?: FluxPressableType;
|
|
55
56
|
readonly href?: string;
|
|
56
57
|
readonly rel?: string;
|
|
@@ -62,6 +63,14 @@
|
|
|
62
63
|
default(): VNode[];
|
|
63
64
|
}>();
|
|
64
65
|
|
|
66
|
+
const resolvedRel = computed(() => {
|
|
67
|
+
if (rel) {
|
|
68
|
+
return rel;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return target === '_blank' ? 'noopener noreferrer' : undefined;
|
|
72
|
+
});
|
|
73
|
+
|
|
65
74
|
const hoverListeners = {
|
|
66
75
|
onMouseenter: (evt: MouseEvent) => emit('mouseenter', evt),
|
|
67
76
|
onMouseleave: (evt: MouseEvent) => emit('mouseleave', evt)
|
|
@@ -13,98 +13,90 @@
|
|
|
13
13
|
width: `${activeItemWidth}px`
|
|
14
14
|
}"/>
|
|
15
15
|
|
|
16
|
-
<
|
|
17
|
-
<div
|
|
18
|
-
v-if="index > 0"
|
|
19
|
-
:class="clsx(
|
|
20
|
-
$style.segmentedControlSeparator,
|
|
21
|
-
(index === modelValue || index === modelValue + 1) && $style.isActive
|
|
22
|
-
)"
|
|
23
|
-
role="separator"/>
|
|
24
|
-
|
|
25
|
-
<button
|
|
26
|
-
ref="items"
|
|
27
|
-
:class="clsx(
|
|
28
|
-
$style.segmentedControlItem,
|
|
29
|
-
index === modelValue && $style.isActive
|
|
30
|
-
)"
|
|
31
|
-
role="radio"
|
|
32
|
-
:aria-checked="index === modelValue"
|
|
33
|
-
:aria-label="item.label"
|
|
34
|
-
:tabindex="index === modelValue ? 0 : -1"
|
|
35
|
-
type="button"
|
|
36
|
-
@click="activate(index)">
|
|
37
|
-
<FluxIcon
|
|
38
|
-
v-if="item.icon"
|
|
39
|
-
:name="item.icon"
|
|
40
|
-
:size="15"/>
|
|
41
|
-
|
|
42
|
-
<span v-if="item.label">{{ item.label }}</span>
|
|
43
|
-
</button>
|
|
44
|
-
</template>
|
|
16
|
+
<slot/>
|
|
45
17
|
</div>
|
|
46
18
|
</template>
|
|
47
19
|
|
|
48
20
|
<script
|
|
49
21
|
lang="ts"
|
|
50
22
|
setup>
|
|
51
|
-
import { useResizeObserver } from '@basmilius/common';
|
|
52
|
-
import type {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import FluxIcon from './FluxIcon.vue';
|
|
23
|
+
import { useMutationObserver, useResizeObserver } from '@basmilius/common';
|
|
24
|
+
import type { FluxSize } from '@flux-ui/types';
|
|
25
|
+
import { onMounted, provide, ref, toRef, useTemplateRef, watch, type VNode } from 'vue';
|
|
26
|
+
import { FluxSegmentedControlInjectionKey, type FluxSegmentedControlValue } from '~flux/components/data';
|
|
56
27
|
import $style from '~flux/components/css/component/SegmentedControl.module.scss';
|
|
57
28
|
|
|
58
|
-
const modelValue = defineModel<
|
|
59
|
-
default: 0
|
|
60
|
-
});
|
|
29
|
+
const modelValue = defineModel<FluxSegmentedControlValue>();
|
|
61
30
|
|
|
62
|
-
defineProps<{
|
|
31
|
+
const { size = 'medium' } = defineProps<{
|
|
63
32
|
readonly ariaLabel?: string;
|
|
64
33
|
readonly isFill?: boolean;
|
|
65
|
-
readonly
|
|
34
|
+
readonly size?: FluxSize;
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
defineSlots<{
|
|
38
|
+
default(): VNode[];
|
|
66
39
|
}>();
|
|
67
40
|
|
|
68
41
|
const controlRef = useTemplateRef<HTMLElement>('control');
|
|
69
|
-
const itemRefs = useTemplateRef<HTMLButtonElement[]>('items');
|
|
70
42
|
|
|
71
43
|
const activeItemX = ref(0);
|
|
72
44
|
const activeItemWidth = ref(0);
|
|
73
|
-
const isAlive = ref(true);
|
|
74
45
|
|
|
75
|
-
|
|
76
|
-
|
|
46
|
+
const items = new Map<HTMLElement, FluxSegmentedControlValue>();
|
|
47
|
+
|
|
48
|
+
provide(FluxSegmentedControlInjectionKey, {
|
|
49
|
+
modelValue,
|
|
50
|
+
size: toRef(() => size),
|
|
51
|
+
select,
|
|
52
|
+
registerItem(element, value) {
|
|
53
|
+
items.set(element, value);
|
|
54
|
+
updateHighlight();
|
|
55
|
+
},
|
|
56
|
+
unregisterItem(element) {
|
|
57
|
+
items.delete(element);
|
|
58
|
+
updateHighlight();
|
|
59
|
+
}
|
|
77
60
|
});
|
|
78
61
|
|
|
79
|
-
|
|
62
|
+
onMounted(() => updateHighlight());
|
|
80
63
|
|
|
81
|
-
|
|
64
|
+
watch(modelValue, () => updateHighlight(), {flush: 'post'});
|
|
82
65
|
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
useMutationObserver(controlRef, () => updateHighlight(), {childList: true, subtree: true});
|
|
67
|
+
useResizeObserver(controlRef, () => updateHighlight());
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
function select(value: FluxSegmentedControlValue): void {
|
|
70
|
+
modelValue.value = value;
|
|
88
71
|
}
|
|
89
72
|
|
|
90
73
|
function onKeyDown(evt: KeyboardEvent): void {
|
|
91
|
-
const
|
|
74
|
+
const control = controlRef.value;
|
|
75
|
+
|
|
76
|
+
if (!control) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const radios = Array.from(control.querySelectorAll<HTMLButtonElement>('[role=radio]:not([disabled])'));
|
|
92
81
|
|
|
93
|
-
if (
|
|
82
|
+
if (radios.length === 0) {
|
|
94
83
|
return;
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
|
|
86
|
+
const activeElement = control.querySelector<HTMLButtonElement>('[role=radio][aria-checked=true]');
|
|
87
|
+
const currentIndex = activeElement ? radios.indexOf(activeElement) : -1;
|
|
88
|
+
|
|
89
|
+
let newIndex: number;
|
|
98
90
|
|
|
99
91
|
switch (evt.key) {
|
|
100
92
|
case 'ArrowLeft':
|
|
101
93
|
case 'ArrowUp':
|
|
102
|
-
newIndex = Math.max(0,
|
|
94
|
+
newIndex = Math.max(0, currentIndex - 1);
|
|
103
95
|
break;
|
|
104
96
|
|
|
105
97
|
case 'ArrowRight':
|
|
106
98
|
case 'ArrowDown':
|
|
107
|
-
newIndex = Math.min(
|
|
99
|
+
newIndex = Math.min(radios.length - 1, currentIndex + 1);
|
|
108
100
|
break;
|
|
109
101
|
|
|
110
102
|
case 'Home':
|
|
@@ -112,40 +104,45 @@
|
|
|
112
104
|
break;
|
|
113
105
|
|
|
114
106
|
case 'End':
|
|
115
|
-
newIndex =
|
|
107
|
+
newIndex = radios.length - 1;
|
|
116
108
|
break;
|
|
117
109
|
|
|
118
110
|
default:
|
|
119
111
|
return;
|
|
120
112
|
}
|
|
121
113
|
|
|
122
|
-
|
|
114
|
+
const target = radios[newIndex];
|
|
115
|
+
const value = items.get(target);
|
|
116
|
+
|
|
117
|
+
if (value !== undefined) {
|
|
118
|
+
select(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
target.focus();
|
|
123
122
|
evt.preventDefault();
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
function updateHighlight(
|
|
127
|
-
|
|
125
|
+
function updateHighlight(): void {
|
|
126
|
+
const control = controlRef.value;
|
|
127
|
+
|
|
128
|
+
if (!control) {
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
const
|
|
132
|
-
const control = controlRef.value;
|
|
132
|
+
const activeElement = control.querySelector<HTMLElement>('[role=radio][aria-checked=true]');
|
|
133
133
|
|
|
134
|
-
if (!
|
|
134
|
+
if (!activeElement) {
|
|
135
|
+
activeItemWidth.value = 0;
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
const width =
|
|
139
|
+
const width = activeElement.offsetWidth;
|
|
139
140
|
|
|
140
141
|
if (width === 0) {
|
|
141
142
|
return;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
const itemRect = itemRef.getBoundingClientRect();
|
|
146
|
-
const scaleX = control.offsetWidth > 0 ? controlRect.width / control.offsetWidth : 1;
|
|
147
|
-
|
|
148
|
-
activeItemX.value = (itemRect.left - controlRect.left) / scaleX;
|
|
145
|
+
activeItemX.value = activeElement.offsetLeft;
|
|
149
146
|
activeItemWidth.value = width;
|
|
150
147
|
}
|
|
151
148
|
</script>
|