@pgcorp/ui-kit 0.6.0 → 0.7.1
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/README.md +57 -0
- package/docs/public-api.md +6 -0
- package/docs/theming.md +58 -0
- package/package.json +5 -1
- package/src/components/shared/_internal/SToastItem.css +8 -8
- package/src/components/shared/_internal/SToastItem.vue +8 -1
- package/src/components/shared/controls/SButton.css +6 -6
- package/src/components/shared/controls/SButton.vue +9 -1
- package/src/components/shared/controls/SInteractiveSurface.css +3 -3
- package/src/components/shared/controls/SInteractiveSurface.vue +9 -2
- package/src/components/shared/controls/SMarker.css +16 -16
- package/src/components/shared/controls/SMarker.vue +27 -1
- package/src/components/shared/data-display/SActionListItem.css +2 -2
- package/src/components/shared/data-display/SActionListItem.vue +8 -1
- package/src/components/shared/data-display/SMessage.css +8 -8
- package/src/components/shared/data-display/SMessage.vue +8 -1
- package/src/components/shared/data-display/SProgressBar.css +12 -12
- package/src/components/shared/data-display/SProgressBar.vue +12 -6
- package/src/components/shared/data-display/SProgressIndicator.css +5 -5
- package/src/components/shared/data-display/SProgressIndicator.vue +9 -1
- package/src/components/shared/data-display/SStatus.css +10 -10
- package/src/components/shared/data-display/SStatus.vue +9 -1
- package/src/components/shared/data-display/STable.vue +27 -4
- package/src/components/shared/data-display/STooltip.css +4 -4
- package/src/components/shared/data-display/STooltip.vue +159 -29
- package/src/components/shared/data-display/STooltipTarget.css +26 -0
- package/src/components/shared/data-display/STooltipTarget.vue +94 -0
- package/src/components/shared/navigation/STabs.vue +11 -3
- package/src/internal/ownedAttrs.ts +3 -0
- package/src/internal/pillSurface.ts +4 -3
- package/src/internal/semanticTone.ts +23 -0
- package/src/styles/style.css +7 -7
|
@@ -22,22 +22,22 @@
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/* Severity Colors */
|
|
25
|
-
.s-progress-bar-value[data-
|
|
25
|
+
.s-progress-bar-value[data-s-tone='primary'] {
|
|
26
26
|
background-color: var(--s-progress-bar-fill-primary);
|
|
27
27
|
}
|
|
28
|
-
.s-progress-bar-value[data-
|
|
28
|
+
.s-progress-bar-value[data-s-tone='success'] {
|
|
29
29
|
background-color: var(--s-progress-bar-fill-success);
|
|
30
30
|
}
|
|
31
|
-
.s-progress-bar-value[data-
|
|
31
|
+
.s-progress-bar-value[data-s-tone='info'] {
|
|
32
32
|
background-color: var(--s-progress-bar-fill-info);
|
|
33
33
|
}
|
|
34
|
-
.s-progress-bar-value[data-
|
|
34
|
+
.s-progress-bar-value[data-s-tone='warn'] {
|
|
35
35
|
background-color: var(--s-progress-bar-fill-warn);
|
|
36
36
|
}
|
|
37
|
-
.s-progress-bar-value[data-
|
|
37
|
+
.s-progress-bar-value[data-s-tone='danger'] {
|
|
38
38
|
background-color: var(--s-progress-bar-fill-danger);
|
|
39
39
|
}
|
|
40
|
-
.s-progress-bar-value[data-
|
|
40
|
+
.s-progress-bar-value[data-s-tone='neutral'] {
|
|
41
41
|
background-color: var(--s-progress-bar-fill-neutral);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -64,21 +64,21 @@
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/* Indeterminate Severity Colors */
|
|
67
|
-
.s-progress-bar-value--indeterminate[data-
|
|
67
|
+
.s-progress-bar-value--indeterminate[data-s-tone='primary']::before {
|
|
68
68
|
background-color: var(--s-progress-bar-fill-primary);
|
|
69
69
|
}
|
|
70
|
-
.s-progress-bar-value--indeterminate[data-
|
|
70
|
+
.s-progress-bar-value--indeterminate[data-s-tone='success']::before {
|
|
71
71
|
background-color: var(--s-progress-bar-fill-success);
|
|
72
72
|
}
|
|
73
|
-
.s-progress-bar-value--indeterminate[data-
|
|
73
|
+
.s-progress-bar-value--indeterminate[data-s-tone='info']::before {
|
|
74
74
|
background-color: var(--s-progress-bar-fill-info);
|
|
75
75
|
}
|
|
76
|
-
.s-progress-bar-value--indeterminate[data-
|
|
76
|
+
.s-progress-bar-value--indeterminate[data-s-tone='warn']::before {
|
|
77
77
|
background-color: var(--s-progress-bar-fill-warn);
|
|
78
78
|
}
|
|
79
|
-
.s-progress-bar-value--indeterminate[data-
|
|
79
|
+
.s-progress-bar-value--indeterminate[data-s-tone='danger']::before {
|
|
80
80
|
background-color: var(--s-progress-bar-fill-danger);
|
|
81
81
|
}
|
|
82
|
-
.s-progress-bar-value--indeterminate[data-
|
|
82
|
+
.s-progress-bar-value--indeterminate[data-s-tone='neutral']::before {
|
|
83
83
|
background-color: var(--s-progress-bar-indeterminate-neutral-fill);
|
|
84
84
|
}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
>
|
|
15
15
|
<div
|
|
16
16
|
v-if="mode === 'determinate'"
|
|
17
|
+
:data-s-tone="toneBinding['data-s-tone']"
|
|
17
18
|
class="s-progress-bar-value"
|
|
18
|
-
:data-severity="severity"
|
|
19
19
|
:style="{ width: `${value}%` }"
|
|
20
20
|
>
|
|
21
21
|
<span v-if="showValue" class="s-progress-bar-label">
|
|
@@ -24,19 +24,21 @@
|
|
|
24
24
|
</div>
|
|
25
25
|
<div
|
|
26
26
|
v-else
|
|
27
|
+
:data-s-tone="toneBinding['data-s-tone']"
|
|
27
28
|
class="s-progress-bar-value--indeterminate"
|
|
28
|
-
:data-severity="severity"
|
|
29
29
|
></div>
|
|
30
30
|
</div>
|
|
31
31
|
</template>
|
|
32
32
|
|
|
33
33
|
<script setup lang="ts">
|
|
34
|
-
import { watchEffect } from 'vue';
|
|
34
|
+
import { computed, watchEffect } from 'vue';
|
|
35
35
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
36
|
+
import { resolveSemanticToneBinding } from '../../../internal/semanticTone';
|
|
36
37
|
|
|
37
38
|
defineOptions({ inheritAttrs: false });
|
|
38
39
|
|
|
39
40
|
export type Severity = 'primary' | 'success' | 'info' | 'warn' | 'danger' | 'neutral';
|
|
41
|
+
const PROGRESS_SEVERITIES = ['primary', 'success', 'info', 'warn', 'danger', 'neutral'] as const satisfies readonly Severity[];
|
|
40
42
|
export type ProgressBarMode = 'determinate' | 'indeterminate';
|
|
41
43
|
export type SProgressBarAccessibility =
|
|
42
44
|
| { readonly mode: 'decorative' }
|
|
@@ -62,6 +64,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
const ownedAttrs = useOwnedAttrs({ component: 'SProgressBar', owner: 'progressbar root' });
|
|
67
|
+
const toneBinding = computed(() => resolveSemanticToneBinding(
|
|
68
|
+
'SProgressBar',
|
|
69
|
+
'severity',
|
|
70
|
+
props.severity,
|
|
71
|
+
PROGRESS_SEVERITIES,
|
|
72
|
+
));
|
|
65
73
|
|
|
66
74
|
watchEffect(() => {
|
|
67
75
|
if (!Number.isFinite(props.value) || props.value < 0 || props.value > 100) {
|
|
@@ -70,9 +78,7 @@ watchEffect(() => {
|
|
|
70
78
|
if (!['determinate', 'indeterminate'].includes(props.mode)) {
|
|
71
79
|
throw new TypeError(`SProgressBar: неизвестный mode "${String(props.mode)}"`);
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
throw new TypeError(`SProgressBar: неизвестный severity "${String(props.severity)}"`);
|
|
75
|
-
}
|
|
81
|
+
void toneBinding.value;
|
|
76
82
|
if (!['xs', 'sm', 'md'].includes(props.size)) {
|
|
77
83
|
throw new TypeError(`SProgressBar: неизвестный size "${String(props.size)}"`);
|
|
78
84
|
}
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
.s-progress-indicator[data-size='md'] { --s-progress-indicator-size: var(--s-icon-size-md); }
|
|
24
24
|
.s-progress-indicator[data-size='lg'] { --s-progress-indicator-size: var(--s-icon-size-lg); }
|
|
25
25
|
|
|
26
|
-
.s-progress-indicator[data-tone='neutral'] { color: var(--s-progress-indicator-foreground-neutral); }
|
|
27
|
-
.s-progress-indicator[data-tone='info'] { color: var(--s-progress-indicator-foreground-info); }
|
|
28
|
-
.s-progress-indicator[data-tone='success'] { color: var(--s-progress-indicator-foreground-success); }
|
|
29
|
-
.s-progress-indicator[data-tone='warn'] { color: var(--s-progress-indicator-foreground-warn); }
|
|
30
|
-
.s-progress-indicator[data-tone='danger'] { color: var(--s-progress-indicator-foreground-danger); }
|
|
26
|
+
.s-progress-indicator[data-s-tone='neutral'] { color: var(--s-progress-indicator-foreground-neutral); }
|
|
27
|
+
.s-progress-indicator[data-s-tone='info'] { color: var(--s-progress-indicator-foreground-info); }
|
|
28
|
+
.s-progress-indicator[data-s-tone='success'] { color: var(--s-progress-indicator-foreground-success); }
|
|
29
|
+
.s-progress-indicator[data-s-tone='warn'] { color: var(--s-progress-indicator-foreground-warn); }
|
|
30
|
+
.s-progress-indicator[data-s-tone='danger'] { color: var(--s-progress-indicator-foreground-danger); }
|
|
31
31
|
|
|
32
32
|
.s-progress-indicator__graphic {
|
|
33
33
|
width: 100%;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
3
|
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
:data-s-tone="toneBinding['data-s-tone']"
|
|
4
5
|
class="s-progress-indicator"
|
|
5
6
|
:data-size="size"
|
|
6
|
-
:data-tone="tone"
|
|
7
7
|
:data-motion="presentation.motion"
|
|
8
8
|
:data-show-label="validatedShowLabel || undefined"
|
|
9
9
|
:role="liveAttributes.role"
|
|
@@ -30,12 +30,14 @@ import { computed, watchEffect } from 'vue'
|
|
|
30
30
|
|
|
31
31
|
import { resolveFeedbackIndicator, resolveFeedbackLive, type FeedbackLiveMode } from '../../../internal/feedbackPresentation'
|
|
32
32
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
33
|
+
import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
|
|
33
34
|
import { validateBoolean } from '../../../internal/runtimeContract'
|
|
34
35
|
|
|
35
36
|
defineOptions({ inheritAttrs: false })
|
|
36
37
|
|
|
37
38
|
export type SProgressIndicatorSize = 'xs' | 'sm' | 'md' | 'lg'
|
|
38
39
|
export type SProgressIndicatorTone = 'current' | 'neutral' | 'info' | 'success' | 'warn' | 'danger'
|
|
40
|
+
const PROGRESS_INDICATOR_TONES = ['current', 'neutral', 'info', 'success', 'warn', 'danger'] as const satisfies readonly SProgressIndicatorTone[]
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Декоративный progress принадлежит labelled parent; standalone status требует собственного имени.
|
|
@@ -64,6 +66,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
64
66
|
})
|
|
65
67
|
|
|
66
68
|
const ownedAttrs = useOwnedAttrs({ component: 'SProgressIndicator', owner: 'progress indicator root' })
|
|
69
|
+
const toneBinding = computed(() => resolveSemanticToneBinding(
|
|
70
|
+
'SProgressIndicator',
|
|
71
|
+
'tone',
|
|
72
|
+
props.tone,
|
|
73
|
+
PROGRESS_INDICATOR_TONES,
|
|
74
|
+
))
|
|
67
75
|
const presentation = resolveFeedbackIndicator('progress')
|
|
68
76
|
const validatedShowLabel = computed(() => validateBoolean('SProgressIndicator', 'showLabel', props.showLabel))
|
|
69
77
|
const feedbackTone = computed(() => props.tone === 'current' ? 'info' : props.tone)
|
|
@@ -49,42 +49,42 @@
|
|
|
49
49
|
@apply sr-only;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
.s-status[data-tone='neutral'] {
|
|
52
|
+
.s-status[data-s-tone='neutral'] {
|
|
53
53
|
@apply text-surface-500 dark:text-surface-400;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
.s-status[data-tone='info'] {
|
|
56
|
+
.s-status[data-s-tone='info'] {
|
|
57
57
|
@apply text-info-700 dark:text-info-300;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
.s-status[data-tone='success'] {
|
|
60
|
+
.s-status[data-s-tone='success'] {
|
|
61
61
|
@apply text-success-700 dark:text-success-300;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
.s-status[data-tone='warn'] {
|
|
64
|
+
.s-status[data-s-tone='warn'] {
|
|
65
65
|
@apply text-warn-700 dark:text-warn-300;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
.s-status[data-tone='danger'] {
|
|
68
|
+
.s-status[data-s-tone='danger'] {
|
|
69
69
|
@apply text-danger-700 dark:text-danger-300;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
.s-status[data-appearance='soft'][data-tone='neutral'] {
|
|
72
|
+
.s-status[data-appearance='soft'][data-s-tone='neutral'] {
|
|
73
73
|
@apply border-surface-200 bg-surface-100 dark:border-surface-700 dark:bg-surface-800;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
.s-status[data-appearance='soft'][data-tone='info'] {
|
|
76
|
+
.s-status[data-appearance='soft'][data-s-tone='info'] {
|
|
77
77
|
@apply border-info-200 bg-info-50 dark:border-info-800 dark:bg-info-950;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
.s-status[data-appearance='soft'][data-tone='success'] {
|
|
80
|
+
.s-status[data-appearance='soft'][data-s-tone='success'] {
|
|
81
81
|
@apply border-success-200 bg-success-50 dark:border-success-800 dark:bg-success-950;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
.s-status[data-appearance='soft'][data-tone='warn'] {
|
|
84
|
+
.s-status[data-appearance='soft'][data-s-tone='warn'] {
|
|
85
85
|
@apply border-warn-200 bg-warn-50 dark:border-warn-800 dark:bg-warn-950;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
.s-status[data-appearance='soft'][data-tone='danger'] {
|
|
88
|
+
.s-status[data-appearance='soft'][data-s-tone='danger'] {
|
|
89
89
|
@apply border-danger-200 bg-danger-50 dark:border-danger-800 dark:bg-danger-950;
|
|
90
90
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
3
|
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
:data-s-tone="toneBinding['data-s-tone']"
|
|
4
5
|
class="s-status"
|
|
5
|
-
:data-tone="tone"
|
|
6
6
|
:data-appearance="appearance"
|
|
7
7
|
:data-size="size"
|
|
8
8
|
:data-label-visibility="labelVisibility"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
<script setup lang="ts">
|
|
23
23
|
import { computed, markRaw, toRaw, type Component } from 'vue'
|
|
24
24
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
25
|
+
import { resolveSemanticToneBinding } from '../../../internal/semanticTone'
|
|
25
26
|
|
|
26
27
|
import {
|
|
27
28
|
resolveFeedbackLive,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
defineOptions({ inheritAttrs: false })
|
|
34
35
|
|
|
35
36
|
export type StatusTone = FeedbackTone
|
|
37
|
+
const STATUS_TONES = ['neutral', 'info', 'success', 'warn', 'danger'] as const satisfies readonly StatusTone[]
|
|
36
38
|
export type StatusAppearance = 'text' | 'soft'
|
|
37
39
|
export type StatusSize = 'xs' | 'sm' | 'md'
|
|
38
40
|
export type StatusLiveMode = Exclude<FeedbackLiveMode, 'auto'>
|
|
@@ -66,6 +68,12 @@ const props = withDefaults(defineProps<{
|
|
|
66
68
|
|
|
67
69
|
const defaultIcon = computed<Component>(() => resolveFeedbackPresentation(props.tone).icon)
|
|
68
70
|
const ownedAttrs = useOwnedAttrs({ component: 'SStatus', owner: 'status root' })
|
|
71
|
+
const toneBinding = computed(() => resolveSemanticToneBinding(
|
|
72
|
+
'SStatus',
|
|
73
|
+
'tone',
|
|
74
|
+
props.tone,
|
|
75
|
+
STATUS_TONES,
|
|
76
|
+
))
|
|
69
77
|
const liveAttributes = computed(() => resolveFeedbackLive(props.tone, props.live))
|
|
70
78
|
|
|
71
79
|
const resolvedIcon = computed<Component | null>(() => {
|
|
@@ -255,7 +255,7 @@ export interface STableProps<
|
|
|
255
255
|
</script>
|
|
256
256
|
|
|
257
257
|
<script setup lang="ts" generic="Row extends object = Record<string, unknown>, RowKey extends STableRowKey = STableRowKey, Field extends string = Extract<keyof Row, string>">
|
|
258
|
-
import { computed, useId, useSlots } from 'vue'
|
|
258
|
+
import { computed, useId, useSlots, watch } from 'vue'
|
|
259
259
|
import { ChevronDown, ChevronsUpDown, ChevronUp } from '../../icons/sputnigUiIcons'
|
|
260
260
|
import { hasOwn } from '../../../internal/es2020'
|
|
261
261
|
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
@@ -548,6 +548,23 @@ const activeSorts = computed<readonly STableSort<Field>[]>(() => {
|
|
|
548
548
|
return singleSort === null ? [] : [singleSort]
|
|
549
549
|
})
|
|
550
550
|
|
|
551
|
+
const NO_PENDING_SORT = Symbol('STable.no-pending-sort')
|
|
552
|
+
let pendingSingleSort: STableSort<Field> | null | typeof NO_PENDING_SORT = NO_PENDING_SORT
|
|
553
|
+
let pendingMultipleSort: readonly STableSort<Field>[] | typeof NO_PENDING_SORT = NO_PENDING_SORT
|
|
554
|
+
|
|
555
|
+
const clearPendingSortIntent = (): void => {
|
|
556
|
+
pendingSingleSort = NO_PENDING_SORT
|
|
557
|
+
pendingMultipleSort = NO_PENDING_SORT
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
watch(validatedSortMode, clearPendingSortIntent, { flush: 'sync' })
|
|
561
|
+
watch(validatedSingleSort, () => {
|
|
562
|
+
pendingSingleSort = NO_PENDING_SORT
|
|
563
|
+
}, { deep: true, flush: 'sync' })
|
|
564
|
+
watch(validatedMultipleSort, () => {
|
|
565
|
+
pendingMultipleSort = NO_PENDING_SORT
|
|
566
|
+
}, { deep: true, flush: 'sync' })
|
|
567
|
+
|
|
551
568
|
function sortIndex(column: STableColumn<Row, Field>): number {
|
|
552
569
|
return activeSorts.value.findIndex((candidate) => candidate.key === column.field)
|
|
553
570
|
}
|
|
@@ -592,8 +609,10 @@ function requestSort(column: STableColumn<Row, Field>): void {
|
|
|
592
609
|
if (!column.sortable) return
|
|
593
610
|
const key = column.field as Field
|
|
594
611
|
if (validatedSortMode.value === 'multiple') {
|
|
595
|
-
const current =
|
|
596
|
-
|
|
612
|
+
const current = pendingMultipleSort === NO_PENDING_SORT
|
|
613
|
+
? activeSorts.value
|
|
614
|
+
: pendingMultipleSort
|
|
615
|
+
const index = current.findIndex((candidate) => candidate.key === key)
|
|
597
616
|
let next: readonly STableSort<Field>[]
|
|
598
617
|
if (index === -1) next = [...current, { key, direction: 'ascending' }]
|
|
599
618
|
else if (current[index]!.direction === 'ascending') {
|
|
@@ -601,15 +620,19 @@ function requestSort(column: STableColumn<Row, Field>): void {
|
|
|
601
620
|
? { key: entry.key, direction: 'descending' }
|
|
602
621
|
: entry)
|
|
603
622
|
} else next = current.filter((_entry, candidateIndex) => candidateIndex !== index)
|
|
623
|
+
pendingMultipleSort = next.map((entry) => ({ ...entry }))
|
|
604
624
|
emit('update:sorts', next)
|
|
605
625
|
emit('multi-sort', next)
|
|
606
626
|
} else {
|
|
607
|
-
const current =
|
|
627
|
+
const current = pendingSingleSort === NO_PENDING_SORT
|
|
628
|
+
? activeSorts.value[0]
|
|
629
|
+
: pendingSingleSort ?? undefined
|
|
608
630
|
const next: STableSort<Field> | null = current?.key !== key
|
|
609
631
|
? { key, direction: 'ascending' }
|
|
610
632
|
: current.direction === 'ascending'
|
|
611
633
|
? { key, direction: 'descending' }
|
|
612
634
|
: null
|
|
635
|
+
pendingSingleSort = next === null ? null : { ...next }
|
|
613
636
|
emit('update:sort', next)
|
|
614
637
|
emit('sort', next)
|
|
615
638
|
}
|
|
@@ -54,19 +54,19 @@
|
|
|
54
54
|
@apply font-mono;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
.s-tooltip__detail-value[data-tone='info'] {
|
|
57
|
+
.s-tooltip__detail-value[data-s-tone='info'] {
|
|
58
58
|
@apply text-info-700 dark:text-info-300;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
.s-tooltip__detail-value[data-tone='success'] {
|
|
61
|
+
.s-tooltip__detail-value[data-s-tone='success'] {
|
|
62
62
|
@apply text-success-700 dark:text-success-300;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
.s-tooltip__detail-value[data-tone='warn'] {
|
|
65
|
+
.s-tooltip__detail-value[data-s-tone='warn'] {
|
|
66
66
|
@apply text-warn-700 dark:text-warn-300;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.s-tooltip__detail-value[data-tone='danger'] {
|
|
69
|
+
.s-tooltip__detail-value[data-s-tone='danger'] {
|
|
70
70
|
@apply text-danger-700 dark:text-danger-300;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<slot
|
|
2
|
+
<slot
|
|
3
|
+
:trigger="triggerBinding"
|
|
4
|
+
:focus-owner="focusOwnerBinding"
|
|
5
|
+
:pointer-target="pointerTargetBinding"
|
|
6
|
+
:is-open="isOpen"
|
|
7
|
+
/>
|
|
3
8
|
<teleport to="body">
|
|
4
9
|
<transition
|
|
5
10
|
enter-active-class="transition-opacity duration-[var(--s-motion-fast)]"
|
|
@@ -27,7 +32,7 @@
|
|
|
27
32
|
<dd
|
|
28
33
|
class="s-tooltip__detail-value"
|
|
29
34
|
:data-presentation="detail.presentation ?? 'text'"
|
|
30
|
-
:data-tone="detail.tone
|
|
35
|
+
:data-s-tone="resolveDetailToneBinding(detail.tone)['data-s-tone']"
|
|
31
36
|
>
|
|
32
37
|
<SBadge
|
|
33
38
|
v-if="detail.presentation === 'badge'"
|
|
@@ -70,6 +75,7 @@ import {
|
|
|
70
75
|
} from 'vue';
|
|
71
76
|
import { mergeIdReferences, useOwnedAttrs } from '../../../internal/ownedAttrs';
|
|
72
77
|
import { hasOwn } from '../../../internal/es2020';
|
|
78
|
+
import { resolveSemanticToneBinding } from '../../../internal/semanticTone';
|
|
73
79
|
import {
|
|
74
80
|
resolveInteractiveElement,
|
|
75
81
|
type ElementRefTarget,
|
|
@@ -127,9 +133,26 @@ export interface STooltipTriggerBinding {
|
|
|
127
133
|
readonly onFocusout: (event: FocusEvent) => void;
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/** Focus/ARIA owner для split trigger без pointer geometry. / Focus and ARIA owner for a split trigger without pointer geometry. */
|
|
137
|
+
export interface STooltipFocusOwnerBinding {
|
|
138
|
+
readonly ref: VNodeRef;
|
|
139
|
+
readonly 'aria-describedby': string | undefined;
|
|
140
|
+
readonly onFocusin: () => void;
|
|
141
|
+
readonly onFocusout: (event: FocusEvent) => void;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Пассивная pointer/geometry target той же интерактивной композиции. / Passive pointer and geometry target in the same interactive composition. */
|
|
145
|
+
export interface STooltipPointerTargetBinding {
|
|
146
|
+
readonly ref: VNodeRef;
|
|
147
|
+
readonly onPointerenter: (event: PointerEvent) => void;
|
|
148
|
+
readonly onPointerleave: (event: PointerEvent) => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
130
151
|
/** Scoped-slot API явного trigger owner. / Scoped-slot API for the explicit trigger owner. */
|
|
131
152
|
export interface STooltipTriggerSlotProps {
|
|
132
153
|
readonly trigger: STooltipTriggerBinding;
|
|
154
|
+
readonly focusOwner: STooltipFocusOwnerBinding;
|
|
155
|
+
readonly pointerTarget: STooltipPointerTargetBinding;
|
|
133
156
|
readonly isOpen: boolean;
|
|
134
157
|
}
|
|
135
158
|
|
|
@@ -171,8 +194,11 @@ const layerZIndex = ref('var(--s-layer-floating)');
|
|
|
171
194
|
let layerRegistration: LayerRegistration | null = null;
|
|
172
195
|
let hideTimer: number | null = null;
|
|
173
196
|
let showTimer: number | null = null;
|
|
174
|
-
let
|
|
175
|
-
let
|
|
197
|
+
let combinedTarget: ElementRefTarget = null;
|
|
198
|
+
let focusOwnerTarget: ElementRefTarget = null;
|
|
199
|
+
let pointerTarget: ElementRefTarget = null;
|
|
200
|
+
let focusOwnerElement: HTMLElement | null = null;
|
|
201
|
+
let pointerTargetElement: HTMLElement | null = null;
|
|
176
202
|
let pointerInside = false;
|
|
177
203
|
let focusInside = false;
|
|
178
204
|
let touchActive = false;
|
|
@@ -302,9 +328,15 @@ const detailSeverityByTone: Record<TooltipDetailTone, BadgeSeverity> = {
|
|
|
302
328
|
};
|
|
303
329
|
const resolveDetailSeverity = (tone: TooltipDetailTone | undefined): BadgeSeverity =>
|
|
304
330
|
detailSeverityByTone[tone ?? 'neutral'];
|
|
331
|
+
const resolveDetailToneBinding = (tone: TooltipDetailTone | undefined) => resolveSemanticToneBinding(
|
|
332
|
+
'STooltip',
|
|
333
|
+
'detail.tone',
|
|
334
|
+
tone ?? 'neutral',
|
|
335
|
+
['neutral', 'info', 'success', 'warn', 'danger'] as const,
|
|
336
|
+
);
|
|
305
337
|
const placement = computed<FloatingPlacement>(() => `${props.position}-center`);
|
|
306
338
|
const {
|
|
307
|
-
anchorRef:
|
|
339
|
+
anchorRef: positionAnchorRef,
|
|
308
340
|
panelRef: tooltipRef,
|
|
309
341
|
floatingStyle,
|
|
310
342
|
arrowStyle,
|
|
@@ -346,11 +378,11 @@ const closeImmediately = () => {
|
|
|
346
378
|
};
|
|
347
379
|
|
|
348
380
|
const openImmediately = () => {
|
|
349
|
-
if (isOpen.value || !
|
|
381
|
+
if (isOpen.value || !focusOwnerElement || !pointerTargetElement) return;
|
|
350
382
|
layerRegistration = registerLayer({
|
|
351
383
|
kind: 'tooltip',
|
|
352
384
|
root: () => tooltipRef.value,
|
|
353
|
-
anchor: () =>
|
|
385
|
+
anchor: () => positionAnchorRef.value,
|
|
354
386
|
blocksParentFocusTrap: false,
|
|
355
387
|
restoreTarget: null,
|
|
356
388
|
requestDismiss: closeImmediately,
|
|
@@ -360,7 +392,7 @@ const openImmediately = () => {
|
|
|
360
392
|
};
|
|
361
393
|
|
|
362
394
|
const scheduleShow = () => {
|
|
363
|
-
if (props.disabled || !hasContent.value || !
|
|
395
|
+
if (props.disabled || !hasContent.value || !focusOwnerElement || !pointerTargetElement) return;
|
|
364
396
|
clearHideTimer();
|
|
365
397
|
clearShowTimer();
|
|
366
398
|
showTimer = window.setTimeout(() => {
|
|
@@ -412,37 +444,100 @@ const handleFocusIn = () => {
|
|
|
412
444
|
};
|
|
413
445
|
|
|
414
446
|
const handleFocusOut = (event: FocusEvent) => {
|
|
415
|
-
if (event.relatedTarget instanceof Node &&
|
|
447
|
+
if (event.relatedTarget instanceof Node && focusOwnerElement?.contains(event.relatedTarget)) return;
|
|
416
448
|
focusInside = false;
|
|
417
449
|
hideWhenInactive();
|
|
418
450
|
};
|
|
419
451
|
|
|
420
|
-
const
|
|
452
|
+
const validateSplitComposition = (): void => {
|
|
453
|
+
if (!focusOwnerElement || !pointerTargetElement || combinedTarget !== null) return;
|
|
454
|
+
if (focusOwnerElement === pointerTargetElement) {
|
|
455
|
+
throw triggerContractError(
|
|
456
|
+
'focusOwner и pointerTarget должны быть разными владельцами одной интерактивной композиции. '
|
|
457
|
+
+ 'focusOwner and pointerTarget must be distinct owners in one interactive composition.',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const commitFocusOwnerElement = (element: HTMLElement | null) => {
|
|
421
463
|
if (element !== null && !isFocusableElement(element)) {
|
|
422
464
|
throw triggerContractError(
|
|
423
|
-
'trigger должен владеть canonical focus owner; используйте интерактивный UI-kit leaf. '
|
|
424
|
-
+ '
|
|
465
|
+
'trigger/focusOwner должен владеть canonical focus owner; используйте интерактивный UI-kit leaf. '
|
|
466
|
+
+ 'trigger/focusOwner must own a canonical focus owner; use an interactive UI-kit leaf.',
|
|
425
467
|
);
|
|
426
468
|
}
|
|
427
|
-
|
|
428
|
-
triggerRef.value = element;
|
|
469
|
+
focusOwnerElement = element;
|
|
429
470
|
pointerInside = false;
|
|
430
471
|
focusInside = false;
|
|
431
472
|
touchActive = false;
|
|
432
473
|
if (element === null) closeImmediately();
|
|
474
|
+
validateSplitComposition();
|
|
433
475
|
};
|
|
434
476
|
|
|
435
|
-
const
|
|
436
|
-
if (
|
|
437
|
-
if (target !== null && triggerTarget !== null) {
|
|
477
|
+
const commitPointerTargetElement = (element: HTMLElement | null) => {
|
|
478
|
+
if (element !== null && combinedTarget === null && isFocusableElement(element)) {
|
|
438
479
|
throw triggerContractError(
|
|
439
|
-
'
|
|
440
|
-
+ '
|
|
480
|
+
'pointerTarget должен быть пассивным и не может создавать дополнительный tab stop. '
|
|
481
|
+
+ 'pointerTarget must be passive and cannot create an additional tab stop.',
|
|
441
482
|
);
|
|
442
483
|
}
|
|
443
|
-
|
|
484
|
+
pointerTargetElement = element;
|
|
485
|
+
positionAnchorRef.value = element;
|
|
486
|
+
pointerInside = false;
|
|
487
|
+
touchActive = false;
|
|
488
|
+
if (element === null) closeImmediately();
|
|
489
|
+
validateSplitComposition();
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const assertBindingMode = (binding: 'combined' | 'focusOwner' | 'pointerTarget'): void => {
|
|
493
|
+
if (binding === 'combined' && (focusOwnerTarget !== null || pointerTarget !== null)) {
|
|
494
|
+
throw triggerContractError('combined trigger нельзя смешивать со split trigger. combined trigger cannot be mixed with a split trigger.');
|
|
495
|
+
}
|
|
496
|
+
if (binding !== 'combined' && combinedTarget !== null) {
|
|
497
|
+
throw triggerContractError('split trigger нельзя смешивать с combined trigger. A split trigger cannot be mixed with a combined trigger.');
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const bindCombinedElement: VNodeRef = (target) => {
|
|
502
|
+
if (target === combinedTarget) return;
|
|
503
|
+
if (target !== null) {
|
|
504
|
+
assertBindingMode('combined');
|
|
505
|
+
if (combinedTarget !== null) {
|
|
506
|
+
throw triggerContractError('trigger binding применён более чем к одному leaf. The trigger binding was applied to more than one leaf.');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
combinedTarget = target;
|
|
444
510
|
if (target === null || target instanceof HTMLElement) {
|
|
445
|
-
|
|
511
|
+
commitFocusOwnerElement(target);
|
|
512
|
+
commitPointerTargetElement(target);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const bindFocusOwnerElement: VNodeRef = (target) => {
|
|
517
|
+
if (target === focusOwnerTarget) return;
|
|
518
|
+
if (target !== null) {
|
|
519
|
+
assertBindingMode('focusOwner');
|
|
520
|
+
if (focusOwnerTarget !== null) {
|
|
521
|
+
throw triggerContractError('focusOwner binding применён более чем к одному leaf. focusOwner binding was applied to more than one leaf.');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
focusOwnerTarget = target;
|
|
525
|
+
if (target === null || target instanceof HTMLElement) {
|
|
526
|
+
commitFocusOwnerElement(target);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const bindPointerTargetElement: VNodeRef = (target) => {
|
|
531
|
+
if (target === pointerTarget) return;
|
|
532
|
+
if (target !== null) {
|
|
533
|
+
assertBindingMode('pointerTarget');
|
|
534
|
+
if (pointerTarget !== null) {
|
|
535
|
+
throw triggerContractError('pointerTarget binding применён более чем к одному leaf. pointerTarget binding was applied to more than one leaf.');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
pointerTarget = target;
|
|
539
|
+
if (target === null || target instanceof HTMLElement) {
|
|
540
|
+
commitPointerTargetElement(target);
|
|
446
541
|
}
|
|
447
542
|
};
|
|
448
543
|
|
|
@@ -467,7 +562,7 @@ const validatedTriggerDescribedBy = computed(() => {
|
|
|
467
562
|
});
|
|
468
563
|
|
|
469
564
|
const triggerBinding = computed<STooltipTriggerBinding>(() => ({
|
|
470
|
-
ref:
|
|
565
|
+
ref: bindCombinedElement,
|
|
471
566
|
'aria-describedby': mergeIdReferences(
|
|
472
567
|
validatedTriggerDescribedBy.value,
|
|
473
568
|
isOpen.value ? tooltipId : undefined,
|
|
@@ -481,6 +576,22 @@ const triggerBinding = computed<STooltipTriggerBinding>(() => ({
|
|
|
481
576
|
onFocusout: handleFocusOut,
|
|
482
577
|
}));
|
|
483
578
|
|
|
579
|
+
const focusOwnerBinding = computed<STooltipFocusOwnerBinding>(() => ({
|
|
580
|
+
ref: bindFocusOwnerElement,
|
|
581
|
+
'aria-describedby': mergeIdReferences(
|
|
582
|
+
validatedTriggerDescribedBy.value,
|
|
583
|
+
isOpen.value ? tooltipId : undefined,
|
|
584
|
+
),
|
|
585
|
+
onFocusin: handleFocusIn,
|
|
586
|
+
onFocusout: handleFocusOut,
|
|
587
|
+
}));
|
|
588
|
+
|
|
589
|
+
const pointerTargetBinding = computed<STooltipPointerTargetBinding>(() => ({
|
|
590
|
+
ref: bindPointerTargetElement,
|
|
591
|
+
onPointerenter: handlePointerEnter,
|
|
592
|
+
onPointerleave: handlePointerLeave,
|
|
593
|
+
}));
|
|
594
|
+
|
|
484
595
|
const handleWindowBlur = () => {
|
|
485
596
|
if (isOpen.value) {
|
|
486
597
|
closeImmediately();
|
|
@@ -530,20 +641,39 @@ watchEffect(() => {
|
|
|
530
641
|
});
|
|
531
642
|
|
|
532
643
|
onMounted(() => {
|
|
533
|
-
if (
|
|
644
|
+
if (combinedTarget !== null && (focusOwnerTarget !== null || pointerTarget !== null)) {
|
|
645
|
+
throw triggerContractError('combined trigger нельзя смешивать со split trigger. combined trigger cannot be mixed with a split trigger.');
|
|
646
|
+
}
|
|
647
|
+
if (combinedTarget === null && (focusOwnerTarget === null || pointerTarget === null)) {
|
|
648
|
+
throw triggerContractError(
|
|
649
|
+
'default slot обязан применить combined trigger либо обе половины split trigger: focusOwner и pointerTarget. '
|
|
650
|
+
+ 'The default slot must apply the combined trigger or both split trigger bindings: focusOwner and pointerTarget.',
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (combinedTarget !== null) {
|
|
654
|
+
const element = resolveInteractiveElement('STooltip: trigger', combinedTarget);
|
|
655
|
+
commitFocusOwnerElement(element);
|
|
656
|
+
commitPointerTargetElement(element);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (focusOwnerTarget === null || pointerTarget === null) {
|
|
534
660
|
throw triggerContractError(
|
|
535
|
-
'
|
|
536
|
-
+ '
|
|
661
|
+
'split trigger требует focusOwner и pointerTarget. '
|
|
662
|
+
+ 'A split trigger requires focusOwner and pointerTarget.',
|
|
537
663
|
);
|
|
538
664
|
}
|
|
539
|
-
|
|
665
|
+
commitFocusOwnerElement(resolveInteractiveElement('STooltip: focusOwner', focusOwnerTarget));
|
|
666
|
+
commitPointerTargetElement(resolveInteractiveElement('STooltip: pointerTarget', pointerTarget));
|
|
540
667
|
});
|
|
541
668
|
|
|
542
669
|
onBeforeUnmount(() => {
|
|
543
670
|
closeImmediately();
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
671
|
+
combinedTarget = null;
|
|
672
|
+
focusOwnerTarget = null;
|
|
673
|
+
pointerTarget = null;
|
|
674
|
+
focusOwnerElement = null;
|
|
675
|
+
pointerTargetElement = null;
|
|
676
|
+
positionAnchorRef.value = null;
|
|
547
677
|
unbindWindowListeners();
|
|
548
678
|
});
|
|
549
679
|
</script>
|