mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Fix Sparkline empty render, stale tween attributes and one-value line (#3227)
This commit is contained in:
1 parent
6199b80b28
commit
01857d234a
4 files changed
+87
-10
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@tabler/core": patch
|
||||||
|
"@tabler/docs": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed `Sparkline` wiping author content when empty or disposed, keeping stale tween attributes and skipping single-value lines.
|
||||||
@@ -262,7 +262,9 @@ class Sparkline extends BaseComponent {
|
|||||||
this._element.querySelector(`.${CLASS_NAME_LABEL}`)?.remove()
|
this._element.querySelector(`.${CLASS_NAME_LABEL}`)?.remove()
|
||||||
|
|
||||||
if (this._config.values.length === 0) {
|
if (this._config.values.length === 0) {
|
||||||
this._element.innerHTML = ''
|
cancelAnimationFrame(this._frame)
|
||||||
|
previous?.remove()
|
||||||
|
EventHandler.trigger(this._element, EVENT_RENDERED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +310,8 @@ class Sparkline extends BaseComponent {
|
|||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
cancelAnimationFrame(this._frame)
|
cancelAnimationFrame(this._frame)
|
||||||
this._element.innerHTML = ''
|
this._element.querySelector('svg')?.remove()
|
||||||
|
this._element.querySelector(`.${CLASS_NAME_LABEL}`)?.remove()
|
||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +368,12 @@ class Sparkline extends BaseComponent {
|
|||||||
const pairs = Array.from(to.children).map((target, i) => {
|
const pairs = Array.from(to.children).map((target, i) => {
|
||||||
const source = from.children[i]
|
const source = from.children[i]
|
||||||
const tweens: [string, string, string][] = []
|
const tweens: [string, string, string][] = []
|
||||||
|
for (const { name } of Array.from(source.attributes)) {
|
||||||
|
if (!target.hasAttribute(name)) {
|
||||||
|
source.removeAttribute(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const { name, value } of Array.from(target.attributes)) {
|
for (const { name, value } of Array.from(target.attributes)) {
|
||||||
const start = source.getAttribute(name)
|
const start = source.getAttribute(name)
|
||||||
if (start === null || start === value) {
|
if (start === null || start === value) {
|
||||||
@@ -431,8 +440,8 @@ class Sparkline extends BaseComponent {
|
|||||||
|
|
||||||
const { min, span } = rangeOf(cfg.values, cfg.min, cfg.max, ...(cfg.threshold === null ? [] : [cfg.threshold]))
|
const { min, span } = rangeOf(cfg.values, cfg.min, cfg.max, ...(cfg.threshold === null ? [] : [cfg.threshold]))
|
||||||
const yOf = (value: number): number => cfg.pad + (1 - (value - min) / span) * (cfg.height - cfg.pad * 2)
|
const yOf = (value: number): number => cfg.pad + (1 - (value - min) / span) * (cfg.height - cfg.pad * 2)
|
||||||
const ys = cfg.values.map(yOf)
|
const ys = cfg.values.length === 1 ? [yOf(cfg.values[0]), yOf(cfg.values[0])] : cfg.values.map(yOf)
|
||||||
const step = ys.length > 1 ? cfg.width / (ys.length - 1) : cfg.width
|
const step = cfg.width / (ys.length - 1)
|
||||||
|
|
||||||
if (cfg.threshold !== null) {
|
if (cfg.threshold !== null) {
|
||||||
this._hairline(svg, yOf(cfg.threshold), 'threshold', true)
|
this._hairline(svg, yOf(cfg.threshold), 'threshold', true)
|
||||||
@@ -463,7 +472,7 @@ class Sparkline extends BaseComponent {
|
|||||||
if (spotIndex >= 0) {
|
if (spotIndex >= 0) {
|
||||||
const spot = svgEl('circle')
|
const spot = svgEl('circle')
|
||||||
setAttr(spot, {
|
setAttr(spot, {
|
||||||
cx: spotIndex * step,
|
cx: cfg.values.length === 1 ? cfg.width / 2 : spotIndex * step,
|
||||||
cy: ys[spotIndex],
|
cy: ys[spotIndex],
|
||||||
r: getCssNumber(this._element, 'spot-size', 2),
|
r: getCssNumber(this._element, 'spot-size', 2),
|
||||||
fill: cssVar('spot'),
|
fill: cssVar('spot'),
|
||||||
@@ -560,7 +569,7 @@ class Sparkline extends BaseComponent {
|
|||||||
const cfg = this._config
|
const cfg = this._config
|
||||||
const svg = this._createSvg()
|
const svg = this._createSvg()
|
||||||
|
|
||||||
const strokeWidth = getCssNumber(this._element, 'stroke-width', 3)
|
const strokeWidth = getCssNumber(this._element, 'stroke-width', 2)
|
||||||
const radius = Math.max(0, Math.min(cfg.width, cfg.height) / 2 - strokeWidth / 2)
|
const radius = Math.max(0, Math.min(cfg.width, cfg.height) / 2 - strokeWidth / 2)
|
||||||
const cx = cfg.width / 2
|
const cx = cfg.width / 2
|
||||||
const cy = cfg.height / 2
|
const cy = cfg.height / 2
|
||||||
|
|||||||
@@ -156,9 +156,42 @@ describe('Sparkline', () => {
|
|||||||
it('should render nothing without values', () => {
|
it('should render nothing without values', () => {
|
||||||
fixtureEl.innerHTML = '<span class="sparkline" data-bs-toggle="sparkline"></span>'
|
fixtureEl.innerHTML = '<span class="sparkline" data-bs-toggle="sparkline"></span>'
|
||||||
|
|
||||||
|
const spy = vi.fn()
|
||||||
|
el().addEventListener('rendered.bs.sparkline', spy)
|
||||||
new Sparkline(el())
|
new Sparkline(el())
|
||||||
|
|
||||||
expect(el().innerHTML).toBe('')
|
expect(el().innerHTML).toBe('')
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep author content when rendering without values', () => {
|
||||||
|
fixtureEl.innerHTML = '<span class="sparkline"><b class="note">n/a</b></span>'
|
||||||
|
|
||||||
|
const sparkline = new Sparkline(el(), { values: [1, 2], label: 'x' })
|
||||||
|
sparkline.update([])
|
||||||
|
|
||||||
|
expect(el().querySelector('svg')).toBeNull()
|
||||||
|
expect(el().querySelector('.sparkline-label')).toBeNull()
|
||||||
|
expect(el().querySelector('.note')).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should draw a flat line for a single value', () => {
|
||||||
|
fixtureEl.innerHTML = '<span class="sparkline" data-bs-fill="auto" data-bs-spot="last" data-bs-values="5"></span>'
|
||||||
|
|
||||||
|
new Sparkline(el())
|
||||||
|
|
||||||
|
const y = el().querySelector('polyline')!.getAttribute('points')!.split(' ')[0].split(',')[1]
|
||||||
|
expect(el().querySelector('polyline')!.getAttribute('points')).toBe(`0,${y} 80,${y}`)
|
||||||
|
expect(el().querySelector('path')!.getAttribute('d')).toBe(`M 0 ${y} L 80 ${y} L 80 24 L 0 24 Z`)
|
||||||
|
expect(el().querySelector('circle')!.getAttribute('cx')).toBe('40')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to a stroke width of 2 without computed styles', () => {
|
||||||
|
const element = document.createElement('span')
|
||||||
|
|
||||||
|
new Sparkline(element, { type: 'circle', values: [50] })
|
||||||
|
|
||||||
|
expect(element.querySelector('circle')!.getAttribute('stroke-width')).toBe('2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should accept values from the config object', () => {
|
it('should accept values from the config object', () => {
|
||||||
@@ -209,6 +242,35 @@ describe('Sparkline', () => {
|
|||||||
expect(heights).toEqual(['24', '12'])
|
expect(heights).toEqual(['24', '12'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should drop attributes the new element does not have', async () => {
|
||||||
|
fixtureEl.innerHTML = '<span class="sparkline" data-bs-type="bar" data-bs-threshold="1.5" data-bs-animation="50" data-bs-values="1,2"></span>'
|
||||||
|
|
||||||
|
const sparkline = new Sparkline(el())
|
||||||
|
const line = el().querySelector('line')!
|
||||||
|
expect(line.getAttribute('stroke-dasharray')).toBe('3 2')
|
||||||
|
|
||||||
|
el().removeAttribute('data-bs-threshold')
|
||||||
|
sparkline.update([1, -2])
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
expect(el().querySelector('line')).toBe(line)
|
||||||
|
expect(line.hasAttribute('stroke-dasharray')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stop a running tween when the values are cleared', async () => {
|
||||||
|
fixtureEl.innerHTML = '<span class="sparkline"></span>'
|
||||||
|
|
||||||
|
const sparkline = new Sparkline(el(), { type: 'bar', values: [1, 2], animation: 1000 })
|
||||||
|
const rect = el().querySelector('rect')!
|
||||||
|
sparkline.update([2, 1])
|
||||||
|
sparkline.update([])
|
||||||
|
const height = rect.getAttribute('height')
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(rect.isConnected).toBe(false)
|
||||||
|
expect(rect.getAttribute('height')).toBe(height)
|
||||||
|
})
|
||||||
|
|
||||||
it('should replace the svg when the shape changes or animation is off', () => {
|
it('should replace the svg when the shape changes or animation is off', () => {
|
||||||
fixtureEl.innerHTML = '<span class="sparkline"></span>'
|
fixtureEl.innerHTML = '<span class="sparkline"></span>'
|
||||||
|
|
||||||
@@ -221,13 +283,13 @@ describe('Sparkline', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('dispose', () => {
|
describe('dispose', () => {
|
||||||
it('should empty the element and drop the instance', () => {
|
it('should remove the chart and drop the instance', () => {
|
||||||
fixtureEl.innerHTML = '<span class="sparkline" data-bs-toggle="sparkline" data-bs-values="1,2"></span>'
|
fixtureEl.innerHTML = '<span class="sparkline" data-bs-toggle="sparkline" data-bs-label="auto" data-bs-values="1,2"><b class="note">n/a</b></span>'
|
||||||
|
|
||||||
const sparkline = new Sparkline(el())
|
const sparkline = new Sparkline(el())
|
||||||
sparkline.dispose()
|
sparkline.dispose()
|
||||||
|
|
||||||
expect(el().innerHTML).toBe('')
|
expect(el().innerHTML).toBe('<b class="note">n/a</b>')
|
||||||
expect(Sparkline.getInstance(el())).toBeNull()
|
expect(Sparkline.getInstance(el())).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ document.getElementById('orders').addEventListener('updated.bs.sparkline', () =>
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `update(values)` | Redraws with new values, given as an array, a number or a string. Fires `updated.bs.sparkline`. |
|
| `update(values)` | Redraws with new values, given as an array, a number or a string. Fires `updated.bs.sparkline`. |
|
||||||
| `render()` | Reads the attributes again and redraws. Fires `rendered.bs.sparkline`. |
|
| `render()` | Reads the attributes again and redraws. Fires `rendered.bs.sparkline`. |
|
||||||
| `dispose()` | Removes the SVG and the component from the element. |
|
| `dispose()` | Removes the SVG, the label and the component from the element. Other content in the element stays. |
|
||||||
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
|
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
|
||||||
| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. |
|
| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. |
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user