frappe-ui 1.0.0-beta.64 → 1.0.0-beta.66

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.
Files changed (160) hide show
  1. package/experimental.ts +23 -0
  2. package/package.json +3 -2
  3. package/scripts/migrate-destinations-v1.js +229 -0
  4. package/src/charts/AreaChart.cy.ts +2 -4
  5. package/src/charts/AreaChart.vue +7 -8
  6. package/src/charts/BarChart.cy.ts +17 -9
  7. package/src/charts/BarChart.vue +9 -10
  8. package/src/charts/DonutChart.cy.ts +68 -6
  9. package/src/charts/DonutChart.vue +32 -25
  10. package/src/charts/FunnelChart.cy.ts +38 -10
  11. package/src/charts/FunnelChart.vue +39 -26
  12. package/src/charts/HeatmapChart.cy.ts +16 -2
  13. package/src/charts/HeatmapChart.vue +7 -3
  14. package/src/charts/LineChart.cy.ts +3 -5
  15. package/src/charts/LineChart.vue +7 -8
  16. package/src/charts/NumberCard.cy.ts +21 -5
  17. package/src/charts/NumberCard.vue +11 -7
  18. package/src/charts/SankeyChart.cy.ts +20 -1
  19. package/src/charts/SankeyChart.vue +29 -18
  20. package/src/charts/ScatterChart.vue +10 -6
  21. package/src/charts/areaChartOptions.test.ts +11 -16
  22. package/src/charts/axisChartCommon.ts +25 -17
  23. package/src/charts/axisChartOptions.ts +9 -23
  24. package/src/charts/axisFormat.test.ts +1 -1
  25. package/src/charts/barChartOptions.test.ts +3 -3
  26. package/src/charts/categoryAxisFit.test.ts +1 -1
  27. package/src/charts/comboChartOptions.test.ts +9 -22
  28. package/src/charts/components/ChartContainer.cy.ts +44 -4
  29. package/src/charts/components/ChartContainer.vue +22 -17
  30. package/src/charts/components/ChartTooltip.cy.ts +13 -2
  31. package/src/charts/components/ChartTooltip.vue +9 -11
  32. package/src/charts/core/useAxisChart.test.ts +127 -0
  33. package/src/charts/core/useAxisChart.ts +64 -37
  34. package/src/charts/core/usePlotKeyboard.test.ts +7 -3
  35. package/src/charts/docs/AreaChart.api.md +15 -15
  36. package/src/charts/docs/AreaChart.md +2 -2
  37. package/src/charts/docs/BarChart.api.md +15 -15
  38. package/src/charts/docs/BarChart.md +4 -2
  39. package/src/charts/docs/ChartCard.md +2 -2
  40. package/src/charts/docs/ChartContainer.api.md +5 -5
  41. package/src/charts/docs/ChartContainer.md +7 -5
  42. package/src/charts/docs/ChartTooltip.api.md +5 -5
  43. package/src/charts/docs/ChartTooltip.md +6 -2
  44. package/src/charts/docs/DonutChart.api.md +20 -8
  45. package/src/charts/docs/DonutChart.md +7 -1
  46. package/src/charts/docs/FunnelChart.api.md +4 -11
  47. package/src/charts/docs/FunnelChart.md +9 -5
  48. package/src/charts/docs/HeatmapChart.api.md +4 -4
  49. package/src/charts/docs/HeatmapChart.md +1 -1
  50. package/src/charts/docs/LineChart.api.md +15 -15
  51. package/src/charts/docs/LineChart.md +7 -7
  52. package/src/charts/docs/NumberCard.api.md +7 -7
  53. package/src/charts/docs/NumberCard.md +6 -5
  54. package/src/charts/docs/Overview.md +69 -10
  55. package/src/charts/docs/SankeyChart.api.md +6 -6
  56. package/src/charts/docs/SankeyChart.md +1 -1
  57. package/src/charts/docs/ScatterChart.api.md +5 -5
  58. package/src/charts/docs/States.md +3 -1
  59. package/src/charts/donutChartOptions.test.ts +4 -4
  60. package/src/charts/donutChartOptions.ts +11 -9
  61. package/src/charts/format.test.ts +3 -0
  62. package/src/charts/format.ts +3 -2
  63. package/src/charts/funnelGeometry.test.ts +33 -2
  64. package/src/charts/funnelGeometry.ts +32 -11
  65. package/src/charts/heatmapOptions.test.ts +7 -5
  66. package/src/charts/heatmapOptions.ts +12 -10
  67. package/src/charts/index.ts +6 -9
  68. package/src/charts/lineChartOptions.test.ts +15 -14
  69. package/src/charts/normalizedStackOptions.test.ts +6 -6
  70. package/src/charts/numberCardFormat.test.ts +21 -9
  71. package/src/charts/numberCardFormat.ts +8 -8
  72. package/src/charts/paletteColors.test.ts +123 -0
  73. package/src/charts/referenceLines.test.ts +25 -13
  74. package/src/charts/sankeyOptions.test.ts +2 -2
  75. package/src/charts/sankeyOptions.ts +8 -6
  76. package/src/charts/scatterOptions.test.ts +21 -5
  77. package/src/charts/scatterOptions.ts +7 -5
  78. package/src/charts/seriesData.test.ts +20 -1
  79. package/src/charts/seriesData.ts +1 -0
  80. package/src/charts/stories/AreaLatency.vue +0 -1
  81. package/src/charts/stories/BarSelect.vue +1 -1
  82. package/src/charts/stories/CustomRadar.vue +2 -2
  83. package/src/charts/stories/Dashboard.vue +10 -4
  84. package/src/charts/stories/DonutHalf.vue +1 -1
  85. package/src/charts/stories/DonutOthers.vue +1 -1
  86. package/src/charts/stories/FunnelDeals.vue +1 -2
  87. package/src/charts/stories/HeatmapSignups.vue +1 -1
  88. package/src/charts/stories/HeatmapTickets.vue +1 -1
  89. package/src/charts/stories/LineDualAxis.vue +1 -1
  90. package/src/charts/stories/LineFilledSeries.vue +1 -1
  91. package/src/charts/stories/LineTrend.vue +1 -1
  92. package/src/charts/stories/NumberKpis.vue +8 -2
  93. package/src/charts/stories/NumberSparklines.vue +7 -1
  94. package/src/charts/stories/SankeySpend.vue +1 -1
  95. package/src/charts/stories/TooltipCustom.vue +5 -0
  96. package/src/charts/tokens.ts +37 -76
  97. package/src/charts/tooltipItems.test.ts +3 -3
  98. package/src/charts/tooltipItems.ts +4 -4
  99. package/src/charts/types.ts +167 -115
  100. package/src/charts/utils.test.ts +50 -0
  101. package/src/charts/utils.ts +52 -1
  102. package/src/charts/valueAxisOptions.test.ts +36 -2
  103. package/src/components/Button/Button.api.md +2 -2
  104. package/src/components/Button/Button.cy.ts +4 -4
  105. package/src/components/Button/Button.vue +2 -2
  106. package/src/components/Button/types.ts +2 -2
  107. package/src/components/DatePicker/CalendarPanel.vue +7 -55
  108. package/src/components/DatePicker/DateCalendar.cy.ts +149 -0
  109. package/src/components/DatePicker/DateCalendar.vue +114 -0
  110. package/src/components/DatePicker/DatePicker.cy.ts +42 -6
  111. package/src/components/DatePicker/DatePicker.md +3 -0
  112. package/src/components/DatePicker/DatePicker.vue +14 -104
  113. package/src/components/DatePicker/DateRangeCalendar.cy.ts +186 -0
  114. package/src/components/DatePicker/DateRangeCalendar.vue +211 -0
  115. package/src/components/DatePicker/DateRangePicker.vue +50 -265
  116. package/src/components/DatePicker/DateTimePicker.cy.ts +42 -4
  117. package/src/components/DatePicker/DateTimePicker.vue +29 -96
  118. package/src/components/DatePicker/calendarTypes.ts +143 -0
  119. package/src/components/DatePicker/composables.ts +64 -2
  120. package/src/components/DatePicker/stories/DateCalendar.vue +18 -0
  121. package/src/components/DatePicker/stories/DateRangeCalendar.vue +27 -0
  122. package/src/components/DatePicker/utils.ts +72 -1
  123. package/src/components/MobileNav/MobileNav.api.md +8 -2
  124. package/src/components/MobileNav/MobileNav.cy.ts +18 -5
  125. package/src/components/MobileNav/MobileNav.md +6 -5
  126. package/src/components/MobileNav/MobileNavItem.vue +10 -6
  127. package/src/components/MobileNav/types.ts +5 -2
  128. package/src/components/PageHeader/PageHeader.api.md +1 -1
  129. package/src/components/PageHeader/PageHeader.cy.ts +24 -0
  130. package/src/components/PageHeader/PageHeader.md +3 -2
  131. package/src/components/PageHeader/PageHeaderBackButton.vue +1 -1
  132. package/src/components/PageHeader/types.ts +1 -1
  133. package/src/components/PageHeader/useHeaderBack.ts +8 -8
  134. package/src/components/Popover/Popover.api.md +25 -5
  135. package/src/components/Popover/Popover.cy.ts +116 -2
  136. package/src/components/Popover/Popover.md +22 -3
  137. package/src/components/Popover/Popover.vue +43 -7
  138. package/src/components/Popover/stories/Typeahead.vue +66 -0
  139. package/src/components/Popover/types.ts +25 -1
  140. package/src/components/Sidebar/Sidebar.api.md +8 -2
  141. package/src/components/Sidebar/Sidebar.cy.ts +13 -4
  142. package/src/components/Sidebar/Sidebar.md +5 -3
  143. package/src/components/Sidebar/SidebarItem.vue +19 -8
  144. package/src/components/Sidebar/types.ts +5 -2
  145. package/src/components/SidebarRail/SidebarRail.api.md +7 -1
  146. package/src/components/SidebarRail/SidebarRail.cy.ts +14 -3
  147. package/src/components/SidebarRail/SidebarRail.md +2 -1
  148. package/src/components/SidebarRail/SidebarRailItem.vue +9 -9
  149. package/src/components/SidebarRail/types.ts +4 -1
  150. package/src/components/TabButtons/TabButtons.md +4 -5
  151. package/src/components/TabButtons/TabButtons.vue +3 -3
  152. package/src/components/TabButtons/types.ts +11 -4
  153. package/src/components/shared/picker/PickerShell.vue +38 -109
  154. package/src/components/shared/picker/types.ts +81 -0
  155. package/src/molecules/list/List.cy.ts +6 -4
  156. package/src/molecules/list/ListRow.vue +4 -1
  157. package/src/molecules/list/list.api.md +8 -2
  158. package/src/molecules/list/list.md +4 -3
  159. package/src/molecules/list/types.ts +5 -2
  160. package/src/charts/chartColors.test.ts +0 -207
package/experimental.ts CHANGED
@@ -35,12 +35,35 @@ export * from './experimental/Charts'
35
35
  // rebuilt here as seven composable parts. Unstable: the parts stay here until
36
36
  // gameplan, helpdesk and this repo's docs site all run on them.
37
37
  export * from './experimental/CommandPalette'
38
+ // Date calendars. `DateCalendar` and `DateRangeCalendar` are the calendars
39
+ // inside the date pickers, as standalone components. Incubating while their
40
+ // props and events settle.
41
+ export { default as DateCalendar } from './src/components/DatePicker/DateCalendar.vue'
42
+ export { default as DateRangeCalendar } from './src/components/DatePicker/DateRangeCalendar.vue'
43
+ export type {
44
+ DateCalendarProps,
45
+ DateCalendarEmits,
46
+ DateCalendarExposed,
47
+ DateRangeCalendarProps,
48
+ DateRangeCalendarEmits,
49
+ DateRangeCalendarExposed,
50
+ } from './src/components/DatePicker/calendarTypes'
38
51
  // ListView family. Moved out of root (#985) — `frappe-ui/list` is
39
52
  // composition-based by design (P3) and doesn't replicate ListView's
40
53
  // config-driven columns (resizable widths, per-column getLabel/prefix
41
54
  // functions, tooltips, disabled-row exclusion, the select banner), so this
42
55
  // stays here, unstable, until `frappe-ui/list` reaches parity.
43
56
  export * from './experimental/ListView'
57
+ // PickerShell. The input half of the date pickers: a `TextInput`, a manual
58
+ // `Popover` and the focus wiring between them. Incubating while its props and
59
+ // slots settle.
60
+ export { default as PickerShell } from './src/components/shared/picker/PickerShell.vue'
61
+ export type {
62
+ PickerShellProps,
63
+ PickerShellTriggerSlotProps,
64
+ PickerShellSlots,
65
+ PickerShellExposed,
66
+ } from './src/components/shared/picker/types'
44
67
  // v0 TextEditor family. Moved out of root (#974) — `frappe-ui/editor` is the
45
68
  // composition-based replacement. Parked here (#1007), unstable, as an interim
46
69
  // import path while apps migrate. Deletion stays human-gated (spec/editor.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "1.0.0-beta.64",
3
+ "version": "1.0.0-beta.66",
4
4
  "description": "A set of components and utilities for rapid UI development",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "bin": {
22
22
  "tokens-v2": "./tailwind/migrate-tokens-v2.js",
23
- "shortcuts-v1": "./scripts/migrate-shortcuts-v1.js"
23
+ "shortcuts-v1": "./scripts/migrate-shortcuts-v1.js",
24
+ "destinations-v1": "./scripts/migrate-destinations-v1.js"
24
25
  },
25
26
  "scripts": {
26
27
  "test": "vitest --run",
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /** Rename pre-v1 navigation props to the v1 destination vocabulary.
3
+ * Usage: destinations-v1 [--dry-run] <dir-or-file...>
4
+ */
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const USAGE = 'Usage: destinations-v1 [--dry-run] <dir-or-file...>'
10
+ const EXTENSIONS = new Set(['.vue', '.md'])
11
+ const PROP_RENAMES = {
12
+ Button: { link: 'href' },
13
+ PageHeaderBackButton: { to: 'fallback-route' },
14
+ ListRow: { to: 'route' },
15
+ SidebarItem: { to: 'route' },
16
+ SidebarRailItem: { to: 'route' },
17
+ MobileNavItem: { to: 'route' },
18
+ }
19
+ const kebab = (name) =>
20
+ name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
21
+
22
+ function componentAliases(source) {
23
+ const aliases = new Map()
24
+ const imports =
25
+ /import\s*\{([^}]+)\}\s*from\s*['"]frappe-ui(?:\/[^'"]*)?['"]/g
26
+ for (const match of source.matchAll(imports)) {
27
+ for (const binding of match[1].split(',')) {
28
+ const [imported, local] = binding
29
+ .trim()
30
+ .replace(/^type\s+/, '')
31
+ .split(/\s+as\s+/)
32
+ if (PROP_RENAMES[imported]) {
33
+ const binding = local || imported
34
+ aliases.set(binding, imported)
35
+ aliases.set(kebab(binding), imported)
36
+ }
37
+ }
38
+ }
39
+ // Local template bindings shadow imports with the same local name.
40
+ const localImports =
41
+ /import\s+([^'";]+?)\s+from\s*['"](?!frappe-ui(?:\/|['"]))[^'"]+['"]/g
42
+ for (const match of source.matchAll(localImports)) {
43
+ const clause = match[1].trim()
44
+ const bindings = []
45
+ if (!clause.startsWith('{') && !clause.startsWith('*')) {
46
+ bindings.push(clause.split(',')[0].trim())
47
+ }
48
+ const named = clause.match(/\{([^}]+)\}/)?.[1]
49
+ if (named) {
50
+ for (const binding of named.split(',')) {
51
+ bindings.push(
52
+ binding
53
+ .trim()
54
+ .split(/\s+as\s+/)
55
+ .at(-1),
56
+ )
57
+ }
58
+ }
59
+ for (const name of bindings) {
60
+ if (!PROP_RENAMES[name]) continue
61
+ aliases.delete(name)
62
+ aliases.delete(kebab(name))
63
+ }
64
+ }
65
+ return aliases
66
+ }
67
+
68
+ function renameAttributes(tag, renames) {
69
+ let migrated = ''
70
+ let cursor = 0
71
+ let quote
72
+ while (cursor < tag.length) {
73
+ const character = tag[cursor]
74
+ if (quote) {
75
+ migrated += character
76
+ if (character === quote && tag[cursor - 1] !== '\\') quote = undefined
77
+ cursor += 1
78
+ continue
79
+ }
80
+ if (character === '"' || character === "'") {
81
+ quote = character
82
+ migrated += character
83
+ cursor += 1
84
+ continue
85
+ }
86
+ if (!/\s/.test(character)) {
87
+ migrated += character
88
+ cursor += 1
89
+ continue
90
+ }
91
+ migrated += character
92
+ cursor += 1
93
+ const match = tag
94
+ .slice(cursor)
95
+ .match(
96
+ /^(v-bind:|[:.]?)([A-Za-z][\w-]*)((?:\.[A-Za-z][\w-]*)*)(?=[\s=/>])/,
97
+ )
98
+ if (!match) continue
99
+ const replacement = renames[match[2]]
100
+ if (replacement) {
101
+ const isShorthandBinding =
102
+ match[1] && tag[cursor + match[0].length] !== '='
103
+ migrated += `${match[1]}${replacement}${match[3]}`
104
+ if (isShorthandBinding) migrated += `="${match[2]}"`
105
+ } else {
106
+ migrated += match[0]
107
+ }
108
+ cursor += match[0].length
109
+ }
110
+ return migrated
111
+ }
112
+
113
+ function migrateMarkup(source, aliases) {
114
+ let migrated = ''
115
+ let cursor = 0
116
+ while (cursor < source.length) {
117
+ const start = source.indexOf('<', cursor)
118
+ if (start === -1) return migrated + source.slice(cursor)
119
+ migrated += source.slice(cursor, start)
120
+ if (source.startsWith('<!--', start)) {
121
+ const commentEnd = source.indexOf('-->', start + 4)
122
+ const next = commentEnd === -1 ? source.length : commentEnd + 3
123
+ migrated += source.slice(start, next)
124
+ cursor = next
125
+ continue
126
+ }
127
+ const nameMatch = source.slice(start + 1).match(/^([A-Za-z][\w.-]*)/)
128
+ if (!nameMatch) {
129
+ migrated += '<'
130
+ cursor = start + 1
131
+ continue
132
+ }
133
+ let end = start + 1 + nameMatch[1].length
134
+ let quote
135
+ for (; end < source.length; end += 1) {
136
+ const character = source[end]
137
+ if (quote) {
138
+ if (character === quote && source[end - 1] !== '\\') quote = undefined
139
+ } else if (character === '"' || character === "'") {
140
+ quote = character
141
+ } else if (character === '>') {
142
+ break
143
+ }
144
+ }
145
+ if (end === source.length) return migrated + source.slice(start)
146
+ const tag = source.slice(start, end + 1)
147
+ const component = aliases.get(nameMatch[1])
148
+ if (!component) {
149
+ migrated += tag
150
+ cursor = end + 1
151
+ continue
152
+ }
153
+ migrated += renameAttributes(tag, PROP_RENAMES[component])
154
+ cursor = end + 1
155
+ }
156
+ return migrated
157
+ }
158
+
159
+ /**
160
+ * Rename destination attributes on template components explicitly imported
161
+ * from frappe-ui, leaving scripts and ambiguous global components unchanged.
162
+ */
163
+ export function migrateDestinations(source) {
164
+ const aliases = componentAliases(source)
165
+ const templateStart = source.indexOf('<template')
166
+ const templateEnd = source.lastIndexOf('</template>')
167
+ if (templateStart === -1 || templateEnd < templateStart)
168
+ return migrateMarkup(source, aliases)
169
+ const end = templateEnd + '</template>'.length
170
+ return (
171
+ source.slice(0, templateStart) +
172
+ migrateMarkup(source.slice(templateStart, end), aliases) +
173
+ source.slice(end)
174
+ )
175
+ }
176
+
177
+ function filesIn(target, seen = new Set()) {
178
+ const stat = fs.lstatSync(target)
179
+ if (stat.isSymbolicLink()) return []
180
+ if (stat.isFile()) return EXTENSIONS.has(path.extname(target)) ? [target] : []
181
+ const real = fs.realpathSync(target)
182
+ if (seen.has(real)) return []
183
+ seen.add(real)
184
+ return fs
185
+ .readdirSync(target, { withFileTypes: true })
186
+ .sort((a, b) => a.name.localeCompare(b.name))
187
+ .flatMap((entry) => {
188
+ if (['node_modules', '.git', 'dist', 'build'].includes(entry.name))
189
+ return []
190
+ return filesIn(path.join(target, entry.name), seen)
191
+ })
192
+ }
193
+
194
+ /** Run the destinations-v1 CLI and return its process exit code. */
195
+ export function run(argv) {
196
+ const dryRun = argv.includes('--dry-run')
197
+ const unknown = argv.find((arg) => arg.startsWith('-') && arg !== '--dry-run')
198
+ if (unknown) {
199
+ console.error(`Unknown option: ${unknown}\n${USAGE}`)
200
+ return 1
201
+ }
202
+ const targets = argv.filter((arg) => arg !== '--dry-run')
203
+ if (!targets.length) {
204
+ console.error(USAGE)
205
+ return 1
206
+ }
207
+ let changed = 0
208
+ const files = [
209
+ ...new Set(targets.flatMap((target) => filesIn(path.resolve(target)))),
210
+ ]
211
+ for (const file of files) {
212
+ const source = fs.readFileSync(file, 'utf8')
213
+ const migrated = migrateDestinations(source)
214
+ if (migrated === source) continue
215
+ changed += 1
216
+ if (!dryRun) fs.writeFileSync(file, migrated)
217
+ console.log(`${dryRun ? 'Would migrate' : 'Migrated'} ${file}`)
218
+ }
219
+ console.log(
220
+ `${dryRun ? 'Would migrate' : 'Migrated'} ${changed} file${changed === 1 ? '' : 's'}.`,
221
+ )
222
+ return 0
223
+ }
224
+
225
+ const script = fileURLToPath(import.meta.url)
226
+ const isMain =
227
+ process.argv[1] &&
228
+ fs.realpathSync(process.argv[1]) === fs.realpathSync(script)
229
+ if (isMain) process.exitCode = run(process.argv.slice(2))
@@ -210,8 +210,7 @@ describe('AreaChart', () => {
210
210
  reading().should('contain.text', 'Refunds')
211
211
  plot().type('{enter}')
212
212
  cy.get('@onSelect').should('have.been.calledWithMatch', {
213
- seriesName: 'refunds',
214
- dataIndex: 0,
213
+ name: 'refunds',
215
214
  value: 4,
216
215
  row: { month: 'Jan', refunds: 4 },
217
216
  })
@@ -224,8 +223,7 @@ describe('AreaChart', () => {
224
223
  plot().type('{rightarrow}')
225
224
  plot().type('{enter}')
226
225
  cy.get('@onSelect').should('have.been.calledWithMatch', {
227
- seriesName: 'sales',
228
- dataIndex: 1,
226
+ name: 'sales',
229
227
  value: 20,
230
228
  row: { month: 'Feb', sales: 20 },
231
229
  })
@@ -2,8 +2,8 @@
2
2
  <ChartContainer
3
3
  :title="title"
4
4
  :subtitle="subtitle"
5
- :plot-label="plotLabel"
6
- :plot-label-secondary="plotLabelSecondary"
5
+ :y-axis-title="yAxisTitle"
6
+ :y2-axis-title="y2AxisTitle"
7
7
  :loading="loading"
8
8
  :error="error || renderError"
9
9
  :empty="isEmpty"
@@ -40,7 +40,7 @@
40
40
  :y="tooltip.y"
41
41
  :label="tooltip.label"
42
42
  :items="tooltip.items"
43
- :row="tooltip.row"
43
+ :rows="tooltip.rows"
44
44
  :dir="dir"
45
45
  >
46
46
  <template v-if="$slots.tooltip" #default="slotProps">
@@ -79,7 +79,7 @@ import type {
79
79
  AreaChartProps,
80
80
  AreaChartSlots,
81
81
  AxisChartConfig,
82
- ChartExposed,
82
+ ChartExposedRefs,
83
83
  } from './types'
84
84
 
85
85
  registerChartModules([
@@ -107,7 +107,6 @@ const config = computed<AxisChartConfig>(() => ({
107
107
  type: 'area',
108
108
  stacked: props.stacked,
109
109
  connectNulls: props.connectNulls,
110
- fillOpacity: props.fillOpacity,
111
110
  }))
112
111
 
113
112
  const {
@@ -115,8 +114,8 @@ const {
115
114
  chart,
116
115
  dir,
117
116
  isEmpty,
118
- plotLabel,
119
- plotLabelSecondary,
117
+ yAxisTitle,
118
+ y2AxisTitle,
120
119
  renderError,
121
120
  tooltip,
122
121
  legendItems,
@@ -133,5 +132,5 @@ const {
133
132
  onSelect: (event) => emit('select', event),
134
133
  })
135
134
 
136
- defineExpose<ChartExposed>({ chart: computed(() => chart.value) })
135
+ defineExpose<ChartExposedRefs>({ chart: computed(() => chart.value) })
137
136
  </script>
@@ -81,8 +81,7 @@ describe('BarChart', () => {
81
81
  mountChart({ onSelect: cy.spy().as('onSelect') })
82
82
  bars().first().click()
83
83
  cy.get('@onSelect').should('have.been.calledWithMatch', {
84
- seriesName: 'sales',
85
- dataIndex: 0,
84
+ name: 'sales',
86
85
  value: 10,
87
86
  row: { month: 'Jan', sales: 10 },
88
87
  })
@@ -476,6 +475,18 @@ describe('BarChart', () => {
476
475
  container().should('have.attr', 'data-state', 'empty')
477
476
  })
478
477
 
478
+ it('says so when no row carries a number for a series', () => {
479
+ mountChart({ y: ['typo'] })
480
+ cy.contains('No data to show').should('be.visible')
481
+ container().should('have.attr', 'data-state', 'empty')
482
+ })
483
+
484
+ it('says so once the legend has switched every series off', () => {
485
+ mountChart({ hiddenSeries: ['sales', 'refunds'] })
486
+ cy.contains('No data to show').should('be.visible')
487
+ container().should('have.attr', 'data-state', 'empty')
488
+ })
489
+
479
490
  it('reads as ready once the bars are drawn', () => {
480
491
  mountChart()
481
492
  bars().should('have.length', data.length * 2)
@@ -621,7 +632,7 @@ describe('BarChart', () => {
621
632
  // The row carries every column, `tooltipColumns` or not: a tooltip the
622
633
  // app draws itself needs no prop to reach one.
623
634
  mountChart({ data: data.map((row) => ({ ...row, orders: 1000 })) }, {
624
- tooltip: ({ row }: any) => h('span', `${row.orders} orders`),
635
+ tooltip: ({ rows }: any) => h('span', `${rows[0].orders} orders`),
625
636
  } as any)
626
637
  bars().should('have.length', data.length * 2)
627
638
  plot().focus()
@@ -658,8 +669,7 @@ describe('BarChart', () => {
658
669
  plot().focus()
659
670
  plot().type('{rightarrow}{enter}')
660
671
  cy.get('@onSelect').should('have.been.calledWithMatch', {
661
- seriesName: 'sales',
662
- dataIndex: 1,
672
+ name: 'sales',
663
673
  value: 20,
664
674
  row: { month: 'Feb', sales: 20 },
665
675
  })
@@ -673,8 +683,7 @@ describe('BarChart', () => {
673
683
  plot().focus()
674
684
  plot().type('{downarrow}{enter}')
675
685
  cy.get('@onSelect').should('have.been.calledWithMatch', {
676
- seriesName: 'refunds',
677
- dataIndex: 0,
686
+ name: 'refunds',
678
687
  value: 4,
679
688
  })
680
689
  })
@@ -685,8 +694,7 @@ describe('BarChart', () => {
685
694
  plot().focus()
686
695
  plot().type('{downarrow}{uparrow}{enter}')
687
696
  cy.get('@onSelect').should('have.been.calledWithMatch', {
688
- seriesName: 'sales',
689
- dataIndex: 0,
697
+ name: 'sales',
690
698
  value: 10,
691
699
  })
692
700
  })
@@ -2,9 +2,9 @@
2
2
  <ChartContainer
3
3
  :title="title"
4
4
  :subtitle="subtitle"
5
- :plot-label="plotLabel"
6
- :plot-label-secondary="plotLabelSecondary"
7
- :plot-label-placement="plotLabelPlacement"
5
+ :y-axis-title="yAxisTitle"
6
+ :y2-axis-title="y2AxisTitle"
7
+ :axis-title-placement="axisTitlePlacement"
8
8
  :loading="loading"
9
9
  :error="error || renderError"
10
10
  :empty="isEmpty"
@@ -41,7 +41,7 @@
41
41
  :y="tooltip.y"
42
42
  :label="tooltip.label"
43
43
  :items="tooltip.items"
44
- :row="tooltip.row"
44
+ :rows="tooltip.rows"
45
45
  :dir="dir"
46
46
  >
47
47
  <template v-if="$slots.tooltip" #default="slotProps">
@@ -80,7 +80,7 @@ import type {
80
80
  BarChartEmits,
81
81
  BarChartProps,
82
82
  BarChartSlots,
83
- ChartExposed,
83
+ ChartExposedRefs,
84
84
  } from './types'
85
85
 
86
86
  registerChartModules([
@@ -109,7 +109,6 @@ const config = computed<AxisChartConfig>(() => ({
109
109
  stacked: props.stacked,
110
110
  horizontal: props.horizontal,
111
111
  connectNulls: props.connectNulls,
112
- fillOpacity: props.fillOpacity,
113
112
  }))
114
113
 
115
114
  const {
@@ -117,9 +116,9 @@ const {
117
116
  chart,
118
117
  dir,
119
118
  isEmpty,
120
- plotLabel,
121
- plotLabelSecondary,
122
- plotLabelPlacement,
119
+ yAxisTitle,
120
+ y2AxisTitle,
121
+ axisTitlePlacement,
123
122
  renderError,
124
123
  tooltip,
125
124
  legendItems,
@@ -137,5 +136,5 @@ const {
137
136
  onSelect: (event) => emit('select', event),
138
137
  })
139
138
 
140
- defineExpose<ChartExposed>({ chart: computed(() => chart.value) })
139
+ defineExpose<ChartExposedRefs>({ chart: computed(() => chart.value) })
141
140
  </script>
@@ -1,5 +1,6 @@
1
- import { defineComponent, h } from 'vue'
1
+ import { defineComponent, h, ref } from 'vue'
2
2
  import DonutChart from './DonutChart.vue'
3
+ import { OTHERS_KEY } from './utils'
3
4
  import './style.css'
4
5
 
5
6
  const data = [
@@ -73,6 +74,7 @@ describe('DonutChart', () => {
73
74
  cy.get('@onSelect')
74
75
  .should('have.been.calledWithMatch', {
75
76
  name: 'Search',
77
+ label: 'Search',
76
78
  value: 50,
77
79
  percent: 50,
78
80
  })
@@ -96,8 +98,47 @@ describe('DonutChart', () => {
96
98
  cy.get('[data-slot="chart-legend"]').should('contain.text', '60%')
97
99
  })
98
100
 
101
+ it('round-trips v-model:hiddenSeries', () => {
102
+ const hidden = ref<string[]>([])
103
+ cy.mount(
104
+ defineComponent({
105
+ setup() {
106
+ return () =>
107
+ h('div', { style: 'width: 480px; height: 300px' }, [
108
+ h(DonutChart, {
109
+ data,
110
+ category: 'source',
111
+ value: 'visits',
112
+ echartOptions: { animation: false },
113
+ hiddenSeries: hidden.value,
114
+ 'onUpdate:hiddenSeries': (v: string[]) => (hidden.value = v),
115
+ }),
116
+ ])
117
+ },
118
+ }),
119
+ )
120
+
121
+ cy.get('[aria-label="Hide Search"]')
122
+ .click()
123
+ .then(() => expect(hidden.value).to.deep.equal(['Search']))
124
+ slices().should('have.length', 2)
125
+ })
126
+
127
+ // An empty ring reads as a bug, so the last entry left showing does nothing.
128
+ it('refuses to hide the last slice left on the ring', () => {
129
+ mountChart()
130
+ cy.get('[aria-label="Hide Search"]').click()
131
+ cy.get('[aria-label="Hide Direct"]').click()
132
+ cy.get('[aria-label="Hide Email"]').click()
133
+ cy.get('[aria-label="Hide Email"]').should(
134
+ 'have.attr',
135
+ 'aria-pressed',
136
+ 'true',
137
+ )
138
+ })
139
+
99
140
  it('gives up the readout when the labels move onto the ring', () => {
100
- mountChart({ showInlineLabels: true })
141
+ mountChart({ showDataLabels: true })
101
142
  cy.get('[data-slot="chart-plot"]').should('not.contain.text', 'Visits')
102
143
  cy.get('[data-slot="chart-plot"] svg text').should('contain.text', 'Search')
103
144
  })
@@ -114,6 +155,25 @@ describe('DonutChart', () => {
114
155
  cy.get('[data-slot="chart-legend"]').should('contain.text', 'Others')
115
156
  })
116
157
 
158
+ // The tail is identified by the reserved key, not by what it prints, so a
159
+ // caller can tell it apart from a category that happens to read "Others".
160
+ it('names the tail slice by the reserved key', () => {
161
+ mountChart({ maxSlices: 2, onSelect: cy.spy().as('onSelect') })
162
+ slices().should('have.length', 2)
163
+ cy.get('[data-slot="chart-plot"] [role="img"]')
164
+ .focus()
165
+ .type('{rightarrow}{enter}')
166
+ cy.get('@onSelect').should('have.been.calledWithMatch', {
167
+ name: OTHERS_KEY,
168
+ label: 'Others',
169
+ value: 50,
170
+ rows: [
171
+ { source: 'Direct', visits: 30 },
172
+ { source: 'Email', visits: 20 },
173
+ ],
174
+ })
175
+ })
176
+
117
177
  describe('states', () => {
118
178
  /** The state an app styles the card from, without tracking it itself. */
119
179
  const container = () => cy.get('[data-slot="chart-container"]')
@@ -172,7 +232,8 @@ describe('DonutChart', () => {
172
232
  mountChart(
173
233
  {},
174
234
  {
175
- center: ({ value, label }: any) => h('span', `${value} of ${label}`),
235
+ center: ({ formattedValue, label }: any) =>
236
+ h('span', `${formattedValue} of ${label}`),
176
237
  },
177
238
  )
178
239
  cy.get('[data-slot="chart-plot"]').should('contain.text', '100 of Visits')
@@ -182,8 +243,8 @@ describe('DonutChart', () => {
182
243
  mountChart(
183
244
  {},
184
245
  {
185
- center: ({ label, percent }: any) =>
186
- h('span', `${label} ${percent ?? ''}`),
246
+ center: ({ label, value, percent }: any) =>
247
+ h('span', `${label} ${value} ${Math.round(percent)}%`),
187
248
  },
188
249
  )
189
250
  slices().should('have.length', data.length)
@@ -194,7 +255,7 @@ describe('DonutChart', () => {
194
255
  350,
195
256
  135,
196
257
  )
197
- cy.get('[data-slot="chart-plot"]').should('contain.text', 'Search 50%')
258
+ cy.get('[data-slot="chart-plot"]').should('contain.text', 'Search 50 50%')
198
259
  })
199
260
 
200
261
  it('takes an app’s own tooltip body', () => {
@@ -244,6 +305,7 @@ describe('DonutChart', () => {
244
305
  plot().type('{enter}')
245
306
  cy.get('@onSelect').should('have.been.calledWithMatch', {
246
307
  name: 'Direct',
308
+ label: 'Direct',
247
309
  value: 30,
248
310
  percent: 30,
249
311
  rows: [{ source: 'Direct', visits: 30 }],