@flux-ui/statistics 3.1.12 → 3.2.0-beta.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flux-ui/statistics",
3
3
  "description": "Statistics components for the Flux UI library.",
4
- "version": "3.1.12",
4
+ "version": "3.2.0-beta.1",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/basmilius",
@@ -51,21 +51,21 @@
51
51
  "dependencies": {
52
52
  "@basmilius/common": "^3.45.0",
53
53
  "@basmilius/utils": "^3.45.0",
54
- "@flux-ui/components": "3.1.12",
55
- "@flux-ui/internals": "3.1.12",
56
- "@flux-ui/types": "3.1.12",
54
+ "@flux-ui/components": "3.2.0-beta.1",
55
+ "@flux-ui/internals": "3.2.0-beta.1",
56
+ "@flux-ui/types": "3.2.0-beta.1",
57
57
  "clsx": "^2.1.1"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "echarts": "^6.1.0",
61
61
  "lodash-es": "^4.18.1",
62
- "vue": "^3.6.0-beta.16",
62
+ "vue": "^3.6.0-beta.17",
63
63
  "vue-i18n": "^11.4.6"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@basmilius/vite-preset": "^3.45.0",
67
67
  "@types/lodash-es": "^4.17.12",
68
- "@types/node": "^26.0.0",
68
+ "@types/node": "^26.0.1",
69
69
  "@vitejs/plugin-vue": "^6.0.7",
70
70
  "@vue/tsconfig": "^0.9.1",
71
71
  "sass-embedded": "^1.100.0",
@@ -1,10 +1,15 @@
1
1
  <template>
2
- <div :class="$style.detailsTable">
2
+ <div
3
+ role="table"
4
+ :aria-label="title"
5
+ :class="$style.detailsTable">
3
6
  <div :class="$style.detailsTableTitle">
4
7
  {{ title }}
5
8
  </div>
6
9
 
7
- <div :class="$style.detailsTableRows">
10
+ <div
11
+ role="rowgroup"
12
+ :class="$style.detailsTableRows">
8
13
  <slot/>
9
14
  </div>
10
15
  </div>
@@ -1,10 +1,16 @@
1
1
  <template>
2
- <div :class="$style.detailsTableRow">
3
- <span :class="$style.detailsTableRowLabel">
2
+ <div
3
+ role="row"
4
+ :class="$style.detailsTableRow">
5
+ <span
6
+ role="rowheader"
7
+ :class="$style.detailsTableRowLabel">
4
8
  {{ label }}
5
9
  </span>
6
10
 
7
- <span :class="$style.detailsTableRowValue">
11
+ <span
12
+ role="cell"
13
+ :class="$style.detailsTableRowValue">
8
14
  {{ value }}
9
15
  </span>
10
16
  </div>
@@ -1,5 +1,8 @@
1
1
  <template>
2
- <div :class="containerClass">
2
+ <div
3
+ ref="list"
4
+ role="list"
5
+ :class="containerClass">
3
6
  <slot v-if="hasSlot"/>
4
7
  <FluxStatisticsLegendItem
5
8
  v-else
@@ -11,16 +14,19 @@
11
14
  :label="item.label"
12
15
  :value="item.value"
13
16
  @mouseenter="onItemMouseEnter(index)"
14
- @mouseleave="onItemMouseLeave"/>
17
+ @mouseleave="onItemMouseLeave"
18
+ @focus="onItemMouseEnter(index)"
19
+ @blur="onItemMouseLeave"/>
15
20
  </div>
16
21
  </template>
17
22
 
18
23
  <script
19
24
  lang="ts"
20
25
  setup>
26
+ import { useFocusZone } from '@flux-ui/internals';
21
27
  import type { FluxDirection } from '@flux-ui/types';
22
28
  import { clsx } from 'clsx';
23
- import { computed, inject, provide, toRef, useSlots } from 'vue';
29
+ import { computed, inject, provide, toRef, useSlots, useTemplateRef } from 'vue';
24
30
  import { FluxStatisticsChartLegendInjectionKey, type FluxStatisticsLegendVariant, FluxStatisticsLegendVariantInjectionKey } from '~flux/statistics/composable';
25
31
  import FluxStatisticsLegendItem from './FluxStatisticsLegendItem.vue';
26
32
  import $style from '~flux/statistics/css/Legend.module.scss';
@@ -35,6 +41,12 @@
35
41
 
36
42
  const slots = useSlots();
37
43
  const legendContext = inject(FluxStatisticsChartLegendInjectionKey, null);
44
+ const listRef = useTemplateRef('list');
45
+
46
+ // One tab stop for the whole legend; arrow keys move focus between items. Bidirectional handles
47
+ // every layout (detailed column, compact row/wrap/column) since it resolves the next item
48
+ // geometrically. Focusing an item drives the same hover sync as the mouse.
49
+ useFocusZone(listRef, {direction: 'bidirectional'});
38
50
 
39
51
  const hasSlot = computed(() => !!slots.default);
40
52
  const autoItems = computed(() => legendContext?.items.value ?? []);
@@ -1,5 +1,7 @@
1
1
  <template>
2
- <div
2
+ <button
3
+ type="button"
4
+ role="listitem"
3
5
  :class="clsx(
4
6
  variant === 'compact' ? $style.statisticsLegendItemCompact : $style.statisticsLegendItem,
5
7
  variant !== 'compact' && isHovered && $style.isHovered
@@ -28,7 +30,7 @@
28
30
  :class="$style.statisticsLegendItemValue">
29
31
  {{ value }}
30
32
  </span>
31
- </div>
33
+ </button>
32
34
  </template>
33
35
 
34
36
  <script
@@ -39,6 +41,7 @@
39
41
  import { clsx } from 'clsx';
40
42
  import { computed, inject } from 'vue';
41
43
  import { FluxStatisticsLegendVariantInjectionKey } from '~flux/statistics/composable';
44
+ import { resolveChartColor } from '~flux/statistics/util';
42
45
  import $style from '~flux/statistics/css/Legend.module.scss';
43
46
 
44
47
  const {
@@ -54,15 +57,5 @@
54
57
  const variantRef = inject(FluxStatisticsLegendVariantInjectionKey, null);
55
58
  const variant = computed(() => variantRef?.value ?? 'detailed');
56
59
 
57
- const colorValue = computed(() => {
58
- if (!color) {
59
- return;
60
- }
61
-
62
- if (['gray', 'primary', 'danger', 'info', 'success', 'warning'].includes(color)) {
63
- return `var(--${color}-600)`;
64
- }
65
-
66
- return color;
67
- });
60
+ const colorValue = computed(() => resolveChartColor(color));
68
61
  </script>
@@ -1,8 +1,11 @@
1
1
  <template>
2
- <div :class="$style.statisticsPercentageBar">
2
+ <div
3
+ role="img"
4
+ :aria-label="ariaLabel"
5
+ :class="$style.statisticsPercentageBar">
3
6
  <div :class="clsx($style.statisticsPercentageBarTrack, hoveredIndex !== null && $style.isHoverActive)">
4
7
  <FluxTooltip
5
- v-for="(item, index) of items"
8
+ v-for="(item, index) of normalizedItems"
6
9
  :key="index">
7
10
  <template #content>
8
11
  <div :class="$style.statisticsPercentageBarTooltip">
@@ -37,16 +40,22 @@
37
40
  import { clsx } from 'clsx';
38
41
  import { computed, inject, watchEffect } from 'vue';
39
42
  import { type ChartLegendItem, FluxStatisticsChartLegendInjectionKey } from '~flux/statistics/composable';
43
+ import { resolveChartColor } from '~flux/statistics/util';
40
44
  import $style from '~flux/statistics/css/PercentageBar.module.scss';
41
45
 
42
- const SEMANTIC_COLORS = ['gray', 'primary', 'danger', 'info', 'success', 'warning'] as const;
43
-
44
46
  const props = defineProps<{
45
47
  readonly items: FluxStatisticsPercentageBarItemObject[];
46
48
  }>();
47
49
 
48
50
  const legendContext = inject(FluxStatisticsChartLegendInjectionKey, null);
49
51
 
52
+ const normalizedItems = computed(() =>
53
+ props.items.map(item => ({
54
+ ...item,
55
+ value: Math.max(0, item.value)
56
+ }))
57
+ );
58
+
50
59
  const legendItems = computed<ChartLegendItem[]>(() =>
51
60
  props.items.map(item => ({
52
61
  color: resolveColor(item.color),
@@ -56,6 +65,10 @@
56
65
  }))
57
66
  );
58
67
 
68
+ const ariaLabel = computed(() =>
69
+ props.items.map(item => `${formatPercentage(Math.max(0, item.value))} ${item.label}`).join(', ')
70
+ );
71
+
59
72
  const hoveredIndex = computed<number | null>(() => legendContext?.hoveredIndex.value ?? null);
60
73
 
61
74
  watchEffect(() => {
@@ -65,15 +78,7 @@
65
78
  });
66
79
 
67
80
  function resolveColor(color?: FluxStatisticsChartColor): string | undefined {
68
- if (!color) {
69
- return;
70
- }
71
-
72
- if (SEMANTIC_COLORS.includes(color as typeof SEMANTIC_COLORS[number])) {
73
- return `var(--${color}-600)`;
74
- }
75
-
76
- return color;
81
+ return resolveChartColor(color);
77
82
  }
78
83
 
79
84
  function onSegmentEnter(index: number): void {
@@ -18,17 +18,27 @@
18
18
  --color: var(--primary-600);
19
19
 
20
20
  position: relative;
21
+ appearance: none;
21
22
  display: flex;
22
23
  padding: 6px;
23
24
  margin: 0 -9px;
24
25
  align-items: flex-start;
25
26
  flex-flow: row nowrap;
26
27
  gap: 9px;
28
+ color: inherit;
29
+ font: inherit;
27
30
  font-size: 14px;
28
31
  line-height: 20px;
32
+ text-align: inherit;
29
33
  white-space: nowrap;
34
+ background: none;
35
+ border: 0;
30
36
  border-radius: var(--radius-half);
37
+ cursor: default;
31
38
  transition: background 120ms var(--swift-out);
39
+ z-index: 0;
40
+
41
+ @include mixin.focus-ring();
32
42
  }
33
43
 
34
44
  .statisticsLegendItem.isHovered {
@@ -61,13 +71,14 @@
61
71
  text-align: right;
62
72
  }
63
73
 
64
- .statisticsLegendItem + .statisticsLegendItem::before {
74
+ .statisticsLegendItem:not(:focus-visible) + .statisticsLegendItem:not(:focus-visible)::before {
65
75
  position: absolute;
66
76
  top: -2px;
67
77
  left: 0;
68
78
  right: 0;
69
79
  content: '';
70
80
  border-top: 1px dashed var(--surface-stroke);
81
+ z-index: -1;
71
82
  }
72
83
 
73
84
  .statisticsLegendCompact {
@@ -88,13 +99,23 @@
88
99
  .statisticsLegendItemCompact {
89
100
  --color: var(--primary-600);
90
101
 
102
+ appearance: none;
91
103
  display: flex;
104
+ padding: 0;
92
105
  align-items: center;
93
106
  flex-flow: row nowrap;
94
107
  gap: 6px;
108
+ color: inherit;
109
+ font: inherit;
95
110
  font-size: 14px;
96
111
  line-height: 1;
112
+ text-align: inherit;
97
113
  white-space: nowrap;
114
+ background: none;
115
+ border: 0;
116
+ cursor: default;
117
+
118
+ @include mixin.focus-ring();
98
119
 
99
120
  .statisticsLegendItemColor {
100
121
  margin: 0;