From 495c35950c46d2b69b4b902bfe576e42895b63c0 Mon Sep 17 00:00:00 2001
From: Dan Manastireanu <498419+danmana@users.noreply.github.com>
Date: Mon, 26 Oct 2020 16:05:24 +0200
Subject: [PATCH] Add borderRadius to bar charts. Closes #7701 (#7951)
* Add helper to parse border radius options
* feat: Implement borderRadius for bar charts
* chore: add demo of bar charts with border radius
* chore: document bar borderRadius
* chore: update typescript with bar borderRadius property
* fix horizontal borders test failing due to antialiasing
* chore: Add border-radius visual test
---
docs/docs/charts/bar.mdx | 10 +-
samples/charts/bar/border-radius.html | 147 ++++++++++++++++++
samples/samples.js | 3 +
src/controllers/controller.bar.js | 1 +
src/elements/element.bar.js | 87 ++++++++++-
src/helpers/helpers.options.js | 27 ++++
test/fixtures/controller.bar/border-radius.js | 45 ++++++
.../fixtures/controller.bar/border-radius.png | Bin 0 -> 6007 bytes
.../controller.bar/horizontal-borders.png | Bin 4122 -> 4201 bytes
test/specs/helpers.options.tests.js | 39 ++++-
types/elements/index.d.ts | 17 +-
11 files changed, 366 insertions(+), 10 deletions(-)
create mode 100644 samples/charts/bar/border-radius.html
create mode 100644 test/fixtures/controller.bar/border-radius.js
create mode 100644 test/fixtures/controller.bar/border-radius.png
diff --git a/docs/docs/charts/bar.mdx b/docs/docs/charts/bar.mdx
index eeb39d928..2a96460a1 100644
--- a/docs/docs/charts/bar.mdx
+++ b/docs/docs/charts/bar.mdx
@@ -83,11 +83,13 @@ the color of the bars is generally set this way.
| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'`
| [`borderSkipped`](#borderskipped) | `string` | Yes | Yes | `'start'`
| [`borderWidth`](#borderwidth) | number|object | Yes | Yes | `0`
+| [`borderRadius`](#borderradius) | number|object | Yes | Yes | `0`
| [`clip`](#general) | number|object | - | - | `undefined`
| [`data`](#data-structure) | `object[]` | - | - | **required**
| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1`
+| [`hoverBorderRadius`](#interactions) | `number` | Yes | Yes | `0`
| [`indexAxis`](#general) | `string` | `'x'` | The base axis for the dataset. Use `'y'` for horizontal bar.
| [`label`](#general) | `string` | - | - | `''`
| [`order`](#general) | `number` | - | - | `0`
@@ -116,13 +118,14 @@ The style of each bar can be controlled with the following properties:
| `borderColor` | The bar border color.
| [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar.
| [`borderWidth`](#borderwidth) | The bar border width (in pixels).
+| [`borderRadius`](#borderradius) | The bar border radius (in pixels).
| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}`
All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options.
#### borderSkipped
-This setting is used to avoid drawing the bar stroke at the base of the fill.
+This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius.
In general, this does not need to be changed except when creating chart types
that derive from a bar chart.
@@ -142,6 +145,10 @@ Options are:
If this value is a number, it is applied to all sides of the rectangle (left, top, right, bottom), except [`borderSkipped`](#borderskipped). If this value is an object, the `left` property defines the left border width. Similarly, the `right`, `top`, and `bottom` properties can also be specified. Omitted borders and [`borderSkipped`](#borderskipped) are skipped.
+#### borderRadius
+
+If this value is a number, it is applied to all corners of the rectangle (topLeft, topRight, bottomLeft, bottomRight), except corners touching the [`borderSkipped`](#borderskipped). If this value is an object, the `topLeft` property defines the top-left corners border radius. Similarly, the `topRight`, `bottomLeft`, and `bottomRight` properties can also be specified. Omitted corners and those touching the [`borderSkipped`](#borderskipped) are skipped. For example if the `top` border is skipped, the border radius for the corners `topLeft` and `topRight` will be skipped as well.
+
### Interactions
The interaction with each bar can be controlled with the following properties:
@@ -151,6 +158,7 @@ The interaction with each bar can be controlled with the following properties:
| `hoverBackgroundColor` | The bar background color when hovered.
| `hoverBorderColor` | The bar border color when hovered.
| `hoverBorderWidth` | The bar border width when hovered (in pixels).
+| `hoverBorderRadius` | The bar border radius when hovered (in pixels).
All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options.
diff --git a/samples/charts/bar/border-radius.html b/samples/charts/bar/border-radius.html
new file mode 100644
index 000000000..b2abcfa16
--- /dev/null
+++ b/samples/charts/bar/border-radius.html
@@ -0,0 +1,147 @@
+
+
+
+
+ Bar Chart
+
+
+
+
+
+
+
+
+
+ Randomize Data
+ Add Dataset
+ Remove Dataset
+ Add Data
+ Remove Data
+
+
+
+
diff --git a/samples/samples.js b/samples/samples.js
index 57cac6b3a..eb569df52 100644
--- a/samples/samples.js
+++ b/samples/samples.js
@@ -22,6 +22,9 @@
}, {
title: 'Floating',
path: 'charts/bar/float.html'
+ }, {
+ title: 'Border Radius',
+ path: 'charts/bar/border-radius.html'
}]
}, {
title: 'Line charts',
diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js
index 6ca1e4f67..128f0fc03 100644
--- a/src/controllers/controller.bar.js
+++ b/src/controllers/controller.bar.js
@@ -521,6 +521,7 @@ BarController.defaults = {
'borderColor',
'borderSkipped',
'borderWidth',
+ 'borderRadius',
'barPercentage',
'barThickness',
'base',
diff --git a/src/elements/element.bar.js b/src/elements/element.bar.js
index 5e4f04ab2..1375e9276 100644
--- a/src/elements/element.bar.js
+++ b/src/elements/element.bar.js
@@ -1,5 +1,6 @@
import Element from '../core/core.element';
-import {toTRBL} from '../helpers/helpers.options';
+import {toTRBL, toTRBLCorners} from '../helpers/helpers.options';
+import {PI, HALF_PI} from '../helpers/helpers.math';
/**
* Helper function to get the bounds of the bar regardless of the orientation
@@ -81,24 +82,46 @@ function parseBorderWidth(bar, maxW, maxH) {
};
}
+function parseBorderRadius(bar, maxW, maxH) {
+ const value = bar.options.borderRadius;
+ const o = toTRBLCorners(value);
+ const maxR = Math.min(maxW, maxH);
+ const skip = parseBorderSkipped(bar);
+
+ return {
+ topLeft: skipOrLimit(skip.top || skip.left, o.topLeft, 0, maxR),
+ topRight: skipOrLimit(skip.top || skip.right, o.topRight, 0, maxR),
+ bottomLeft: skipOrLimit(skip.bottom || skip.left, o.bottomLeft, 0, maxR),
+ bottomRight: skipOrLimit(skip.bottom || skip.right, o.bottomRight, 0, maxR)
+ };
+}
+
function boundingRects(bar) {
const bounds = getBarBounds(bar);
const width = bounds.right - bounds.left;
const height = bounds.bottom - bounds.top;
const border = parseBorderWidth(bar, width / 2, height / 2);
+ const radius = parseBorderRadius(bar, width / 2, height / 2);
return {
outer: {
x: bounds.left,
y: bounds.top,
w: width,
- h: height
+ h: height,
+ radius
},
inner: {
x: bounds.left + border.l,
y: bounds.top + border.t,
w: width - border.l - border.r,
- h: height - border.t - border.b
+ h: height - border.t - border.b,
+ radius: {
+ topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
+ topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
+ bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
+ bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
+ }
}
};
}
@@ -114,6 +137,52 @@ function inRange(bar, x, y, useFinalPosition) {
&& (skipY || y >= bounds.top && y <= bounds.bottom);
}
+function hasRadius(radius) {
+ return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
+}
+
+/**
+ * Add a path of a rectangle with rounded corners to the current sub-path
+ * @param {CanvasRenderingContext2D} ctx Context
+ * @param {*} rect Bounding rect
+ */
+function addRoundedRectPath(ctx, rect) {
+ const {x, y, w, h, radius} = rect;
+
+ // top left arc
+ ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true);
+
+ // line from top left to bottom left
+ ctx.lineTo(x, y + h - radius.bottomLeft);
+
+ // bottom left arc
+ ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);
+
+ // line from bottom left to bottom right
+ ctx.lineTo(x + w - radius.bottomRight, y + h);
+
+ // bottom right arc
+ ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);
+
+ // line from bottom right to top right
+ ctx.lineTo(x + w, y + radius.topRight);
+
+ // top right arc
+ ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);
+
+ // line from top right to top left
+ ctx.lineTo(x + radius.topLeft, y);
+}
+
+/**
+ * Add a path of a rectangle to the current sub-path
+ * @param {CanvasRenderingContext2D} ctx Context
+ * @param {*} rect Bounding rect
+ */
+function addNormalRectPath(ctx, rect) {
+ ctx.rect(rect.x, rect.y, rect.w, rect.h);
+}
+
export default class BarElement extends Element {
constructor(cfg) {
@@ -133,20 +202,23 @@ export default class BarElement extends Element {
draw(ctx) {
const options = this.options;
const {inner, outer} = boundingRects(this);
+ const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
ctx.save();
if (outer.w !== inner.w || outer.h !== inner.h) {
ctx.beginPath();
- ctx.rect(outer.x, outer.y, outer.w, outer.h);
+ addRectPath(ctx, outer);
ctx.clip();
- ctx.rect(inner.x, inner.y, inner.w, inner.h);
+ addRectPath(ctx, inner);
ctx.fillStyle = options.borderColor;
ctx.fill('evenodd');
}
+ ctx.beginPath();
+ addRectPath(ctx, inner);
ctx.fillStyle = options.backgroundColor;
- ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
+ ctx.fill();
ctx.restore();
}
@@ -183,7 +255,8 @@ BarElement.id = 'bar';
*/
BarElement.defaults = {
borderSkipped: 'start',
- borderWidth: 0
+ borderWidth: 0,
+ borderRadius: 0
};
/**
diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js
index ba61f5e52..980ca1563 100644
--- a/src/helpers/helpers.options.js
+++ b/src/helpers/helpers.options.js
@@ -64,6 +64,33 @@ export function toTRBL(value) {
};
}
+/**
+ * Converts the given value into a TRBL corners object (similar with css border-radius).
+ * @param {number|object} value - If a number, set the value to all TRBL corner components,
+ * else, if an object, use defined properties and sets undefined ones to 0.
+ * @returns {object} The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight)
+ * @since 3.0.0
+ */
+export function toTRBLCorners(value) {
+ let tl, tr, bl, br;
+
+ if (isObject(value)) {
+ tl = numberOrZero(value.topLeft);
+ tr = numberOrZero(value.topRight);
+ bl = numberOrZero(value.bottomLeft);
+ br = numberOrZero(value.bottomRight);
+ } else {
+ tl = tr = bl = br = numberOrZero(value);
+ }
+
+ return {
+ topLeft: tl,
+ topRight: tr,
+ bottomLeft: bl,
+ bottomRight: br
+ };
+}
+
/**
* Converts the given value into a padding object with pre-computed width/height.
* @param {number|object} value - If a number, set the value to all TRBL component,
diff --git a/test/fixtures/controller.bar/border-radius.js b/test/fixtures/controller.bar/border-radius.js
new file mode 100644
index 000000000..67c579ff0
--- /dev/null
+++ b/test/fixtures/controller.bar/border-radius.js
@@ -0,0 +1,45 @@
+module.exports = {
+ threshold: 0.01,
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderWidth: 2,
+ borderRadius: 5
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5],
+ borderSkipped: false,
+ borderRadius: Number.MAX_VALUE
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ indexAxis: 'y',
+ elements: {
+ bar: {
+ backgroundColor: '#AAAAAA80',
+ borderColor: '#80808080',
+ borderWidth: {bottom: 6, left: 15, top: 6, right: 15}
+ }
+ },
+ scales: {
+ x: {display: false},
+ y: {display: false}
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
diff --git a/test/fixtures/controller.bar/border-radius.png b/test/fixtures/controller.bar/border-radius.png
new file mode 100644
index 0000000000000000000000000000000000000000..68e7c0dd2916c1e46bf790706f3a00d634b0c011
GIT binary patch
literal 6007
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
literal 0
HcmV?d00001
diff --git a/test/fixtures/controller.bar/horizontal-borders.png b/test/fixtures/controller.bar/horizontal-borders.png
index 1cd6913acfc05dee86e993471c3b7abb9302f706..73adeead5619ec9a0b636a129912c78659d33617 100644
GIT binary patch
literal 4201
zcmeHLd010d7QgT11+x?l0@ccv+DZ^D#Hs^|$b%y2uvSzkMNCHuE>sXvwm@FA!f0Jk
zS_+jVj%}sNRuBaX@iNr6LWBGzgM8iOVcb?s2>TjZ_
z)GytFIbZ)aK~TkLQOXWltCe{&EBiY!O%5OIs@W;21
zpa0W5&}roE$4A`?;eA<5eA*JYZL*l$UBEnP;dFJn)Stxa8oI{2af`KL^`b0()O1Qp
zN?U)8F;%C|P1%A<#_STy>CVnCCGpC%+
zZ$U$tRD?14%f;}DVUoiHYYo>5W!{8IeTAql8^NG>mJy|5s#jNAU}_}wBIy7N=*PFB8k(->}VI{RYI-P&qf)p+Amn~}kKN&Q%mp9hKM
zeUZ|_3)n2{XUb6ig1eHno><Ft
z%$?@S+#_aoKXj5LI~rOc$N0iU!U_Q3u2BP1`Fpam3B}$D55YWNWI3m4gsqh-dM?Hk
z9r{o(=wn!JB?Yp~h~H^g~fNz2rh0|5u^75qTX@@9VtxijUhv^h414}F5v=eihMSJt=41Lk)
zyLQVkC%l*e#4w=D;MiBD#s^z$Or|!xNxxUgcn8oYxBV&4nm+x)p2uMV0ICrLf{~)5=+kmUItA>
zGdsXsom86wW;a(f9S)|0q$LbYokr*srN7Tf&IB(~n`L5CKKJP;Sx8xgVEa=hNy7i~oQHeaH;*#!4etNEcDcB`d3r2DWP&3Lkaw^XUo)O4FT1}iJj&~H<=BMT7g
zQ+B09Gt*Xl>bM6$UfPdG+MCKCotBXa36HpkB0>Fk1_ziYZLG45Qid!BQ|^RQgYCsgZMF4yg5Wq40klK~1KC**kvwCAGBG
z-(GwU)MyC35)576Nfv^;Ggb9KS8W4Abuph8*R5K!km}OQ&<+kw|B3V<`ToivR2X%`
zZk04#+InyMWonS!3oUC!`i#IjwC;H|MSfiBMkCHG$UPch;
zx{X$!Jo{bn7GP_O`wl7^PMgTMb;x&le)K@J&vJHOJE!B;+X6;cMCd2I!{T1;++{dT?Euuk{0;CCzv
zE8oBSacJ_s)jjj!8lPo2{ICSW?*d|eVe7BWm2i`Ukt6qS%45kB?jl_PG07h~RiVP7
zBB#d4;4twA(wJW#Jew$qiMGRuP$=7>8+J9V%H`1Ak@LriD+Nmd;P@E)DLYEXZNQfujyfkl(cxL#Z5E7-(nd8%$zW7R?HVHQJtdv1==q
zmyqtgUaPJg%4AJKHN?P&UiJ3bOk7XCa-8I4u9s*2gQh}?X1r>z1Vq-AY$*bN#W{
zkDB!NoJZl*@F~KEB(ZXem*6(A7&$iX|3n{~Y|~B;e^Z9S&jt@<2BX72DwI~EU8wUd
zwh;jCDDnN>WtHgi>7tRK|LxrF`OjNZFSt^#Pa2;ybR8Udp|ic7uGJ0jgEafyFV*ss
nA`Rf~*7Hs{>p0nJq-I2^J`|YgxfQ}+2x!APZ`VQ>(ZPQLRa?}g
literal 4122
zcmeHKX;@QN8a{V1+!!D!p)A5c5Nm3s5K3he!*xXm1*=xDY*E`{E0Q1-5HY!7aI}RM
zryUCl2(4PCYJq93hBdcV(N+%91>(Pf5ChJfW?OOYqkJ@!mkJ*STI~Wb~_dTZpVf-E4QUa54ASD^Gr8V
z*Wr{$E2)LPrLi47EpMlY9LmVg=i`|woee9K+`hAvu5Vd6+!Yt8O2%Iz9B;T?>Dzd@
zZWcNJ;M~ag$T$8a=i7Sc&P1PY9v1rOgD8NYIMIcl>ayZ&G(EJ~0ZW7#I&w?0B$fK+W9;F3#kN-L?JSFO`CfSWS|D>nGkIqua3PbEI)NW?DrdjRAl~elb;4
z;tdKgbSHup@2Eu@2U|hoas-`%P#P22f%-NJP{W6~x!M3>6bPi9Gd5HrDR9$RkYA)@
z#Xna0fHWaVJ5V}%5qM}uI{-6?NhvrP3%55pY(cfLd0Li0&N8QTh?Yeo0%ma!TzsB}A_No2K{X-+1Wz|R)}RRbNT}_p&!6)6bnZD#^i<%c!9XmIDVD~H4}tOa
z;dWE)lI(<|{47p10qiQd5~ESEU>~@I-GxX`d2-E8STk^is{FeWRjEffcu^#}g1P
zHHrMrN98guRT5excj(PusBiU{*sn7b1_u<1R0R|-wcQ;(Zd_5X-EY9yCit`czT?&0Mw
zMy|?-h33Ch#WS)Q+#<*;;iRlG(wsM61iVityBG-zZU_;D+74$LbHpu{fVg!-tBe#T
zaYC5FTG+-M_czkue)faI#4VwyE>+wfRpE-_mv|y@X>+m#`oVl%l=cIg&Pzlb#Bv-p
zXobK2bn27G-OjJq_)M*(=rUJry-)k>r#?O`nfa+f^Aa^-c%u>p14
zUt6d(%rghVOrUhYXcs{vtS_xFc%vo|HwQ;D$WR(QdduAt!DxE{si^}pg5YVN2eA~n
znFhp%?k^~dI6523Yh|%e0>=Gw^C&68usAbDQgnTNZjAg%kR?uB#A~Pf&$mZf&1|AR
zYdialR*LHFj$tc{mr6iVRc--YZ`bK+juS7=GpAl3*}{kFKt|Hws52OChD6i559wnk
z!MXUU_xY-H4qb#LTM}#Fz@E`vd=&vz0#TMi-WZDBZ`2%tn*?Ouv@N83pnm*1)5b{5
z6-Xbd^zYag4q)2BAHNtOtOVs{^gvTtdqkQUi{9c_fvgoxQQVK+
zFdvh=eX@aX^kUHI%m
z0qk=)>~pS%KV+?cnSv_GvoTQ!1Iu52@yGBVJqh_8fKUo|XRr3D?ZO|xtCko+rlC>F
zo4~GB_db6QN7t){+O$tOgUEax<$JcS4=omI`$~%RXLDcKh2rk$6sAcHuyP>yg#KeI;i$gyB}VLYmyCj
zeh$~+lMasm*Ow+9(iTOALE1|8@H5gD&qn18LW{(6-h%>v#0)_+k$AZUJfWYUaa|Gd)H}E!S
zb>Bmk<*)?znDJ^@P#_Avh5S-+=*IyV=I|Ruu;@Dz9A{YcLo;&r667doaW2v45KXSx
zm)xB3M56k@SkK(qlwH8>_?REsoo
zN2ZR1X7T^vegoVZ8n?Jj_Vs|897uxeIDSLPpZ8XI;}9
zpo47!n$(PD9M?<)Q$-G}Vic220iEVAT)k7t`Z`0`2ch}`bN3e!)Q3gR@>(n)#oOiX
zsZ)$TJnTc(_n(FRya&BxqZnb4Rf@i5v<+HS^(;>_q(;)P#sC7E(v(lxkN;aR|nLd#>??JJmPpmjDkZqfZygw!8BiU180K@7TOy+a{~2SUr;qd!Ce6+yfisq
zT@0o4&2z!+HdnYkA3a|CD!4$sH
zqRMOL#=m9Jjw@6gvcd`bsW?l5O<`-8!HnTm~8!8ccxHzdBqh3@lyXUMqvNabFuyw%Y6oj%e!dz3`EO`&QE;(GlnbgSb_#ht25t@vV{JhOPHB
zi;2O75-@m0+^}{9G@iS1mVak(>Mz|Ou|zP*drmF$Ox|!C^oD~UhXFUaHfYJ|w&M!@7Yx0*{l?wD7+bCiBhc8wG_}Q>FWKHQRNybm&hXFVM
diff --git a/test/specs/helpers.options.tests.js b/test/specs/helpers.options.tests.js
index f742b1bf5..6bad385c1 100644
--- a/test/specs/helpers.options.tests.js
+++ b/test/specs/helpers.options.tests.js
@@ -1,4 +1,4 @@
-const {toLineHeight, toPadding, toFont, resolve} = Chart.helpers; // from '../../src/helpers/helpers.options';
+const {toLineHeight, toPadding, toFont, resolve, toTRBLCorners} = Chart.helpers; // from '../../src/helpers/helpers.options';
describe('Chart.helpers.options', function() {
describe('toLineHeight', function() {
@@ -23,6 +23,43 @@ describe('Chart.helpers.options', function() {
});
});
+ describe('toTRBLCorners', function() {
+ it('should support number values', function() {
+ expect(toTRBLCorners(4)).toEqual(
+ {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4});
+ expect(toTRBLCorners(4.5)).toEqual(
+ {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5});
+ });
+ it('should support string values', function() {
+ expect(toTRBLCorners('4')).toEqual(
+ {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4});
+ expect(toTRBLCorners('4.5')).toEqual(
+ {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5});
+ });
+ it('should support object values', function() {
+ expect(toTRBLCorners({topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4})).toEqual(
+ {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4});
+ expect(toTRBLCorners({topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5})).toEqual(
+ {topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5});
+ expect(toTRBLCorners({topLeft: '1', topRight: '2', bottomLeft: '3', bottomRight: '4'})).toEqual(
+ {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4});
+ });
+ it('should fallback to 0 for invalid values', function() {
+ expect(toTRBLCorners({topLeft: 'foo', topRight: 'foo', bottomLeft: 'foo', bottomRight: 'foo'})).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners({topLeft: null, topRight: null, bottomLeft: null, bottomRight: null})).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners({})).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners('foo')).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners(null)).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners(undefined)).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ });
+ });
+
describe('toPadding', function() {
it ('should support number values', function() {
expect(toPadding(4)).toEqual(
diff --git a/types/elements/index.d.ts b/types/elements/index.d.ts
index ce3c200a5..2558c4d1c 100644
--- a/types/elements/index.d.ts
+++ b/types/elements/index.d.ts
@@ -263,9 +263,24 @@ export interface IBarOptions extends ICommonOptions {
* @default 'start'
*/
borderSkipped: 'start' | 'end' | 'left' | 'right' | 'bottom' | 'top';
+
+ /**
+ * Border radius
+ * @default 0
+ */
+ borderRadius: number | IBorderRadius;
}
-export interface IBarHoverOptions extends ICommonHoverOptions {}
+export interface IBorderRadius {
+ topLeft: number;
+ topRight: number;
+ bottomLeft: number;
+ bottomRight: number;
+}
+
+export interface IBarHoverOptions extends ICommonHoverOptions {
+ hoverBorderRadius: number | IBorderRadius;
+}
export interface BarElement<
T extends IBarProps = IBarProps,