@automattic/charts 0.59.0 → 1.0.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/CHANGELOG.md +18 -0
- package/dist/index.cjs +90 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +3 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +90 -26
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/charts/geo-chart/geo-chart.tsx +48 -21
- package/src/charts/geo-chart/test/geo-chart.test.tsx +48 -10
- package/src/charts/private/chart-layout/chart-layout.module.scss +8 -0
- package/src/providers/chart-context/global-charts-provider.tsx +13 -19
- package/src/providers/chart-context/test/chart-context.test.tsx +124 -3
- package/src/utils/color-utils.ts +23 -12
- package/src/utils/sanitize-html.ts +49 -0
- package/src/utils/test/color-utils.test.ts +110 -0
- package/src/utils/test/sanitize-html.test.ts +101 -0
|
@@ -716,6 +716,21 @@ describe( 'normalizeColorToHex', () => {
|
|
|
716
716
|
} );
|
|
717
717
|
} );
|
|
718
718
|
|
|
719
|
+
describe( 'HSLA strings', () => {
|
|
720
|
+
it( 'converts hsla(0, 100%, 50%, 1) to #ff0000', () => {
|
|
721
|
+
expect( normalizeColorToHex( 'hsla(0, 100%, 50%, 1)' ) ).toBe( '#ff0000' );
|
|
722
|
+
} );
|
|
723
|
+
|
|
724
|
+
it( 'converts hsla(120, 100%, 50%, 0.5) to #00ff00', () => {
|
|
725
|
+
expect( normalizeColorToHex( 'hsla(120, 100%, 50%, 0.5)' ) ).toBe( '#00ff00' );
|
|
726
|
+
} );
|
|
727
|
+
|
|
728
|
+
it( 'converts fully transparent hsla to #000000', () => {
|
|
729
|
+
// d3-color converts fully transparent colors (alpha=0) to black
|
|
730
|
+
expect( normalizeColorToHex( 'hsla(240, 100%, 50%, 0)' ) ).toBe( '#000000' );
|
|
731
|
+
} );
|
|
732
|
+
} );
|
|
733
|
+
|
|
719
734
|
describe( 'RGB strings', () => {
|
|
720
735
|
it( 'converts rgb(255, 0, 0) to #ff0000', () => {
|
|
721
736
|
expect( normalizeColorToHex( 'rgb(255, 0, 0)' ) ).toBe( '#ff0000' );
|
|
@@ -726,6 +741,36 @@ describe( 'normalizeColorToHex', () => {
|
|
|
726
741
|
} );
|
|
727
742
|
} );
|
|
728
743
|
|
|
744
|
+
describe( 'RGBA strings', () => {
|
|
745
|
+
it( 'converts rgba(255, 0, 0, 1) to #ff0000', () => {
|
|
746
|
+
expect( normalizeColorToHex( 'rgba(255, 0, 0, 1)' ) ).toBe( '#ff0000' );
|
|
747
|
+
} );
|
|
748
|
+
|
|
749
|
+
it( 'converts rgba(0, 0, 255, 0.5) to #0000ff', () => {
|
|
750
|
+
// Alpha channel is stripped in hex conversion
|
|
751
|
+
expect( normalizeColorToHex( 'rgba(0, 0, 255, 0.5)' ) ).toBe( '#0000ff' );
|
|
752
|
+
} );
|
|
753
|
+
|
|
754
|
+
it( 'converts fully transparent rgba to #000000', () => {
|
|
755
|
+
// d3-color converts fully transparent colors (alpha=0) to black
|
|
756
|
+
expect( normalizeColorToHex( 'rgba(128, 128, 128, 0)' ) ).toBe( '#000000' );
|
|
757
|
+
} );
|
|
758
|
+
} );
|
|
759
|
+
|
|
760
|
+
describe( 'Named CSS colors', () => {
|
|
761
|
+
it( 'converts steelblue to hex', () => {
|
|
762
|
+
expect( normalizeColorToHex( 'steelblue' ) ).toBe( '#4682b4' );
|
|
763
|
+
} );
|
|
764
|
+
|
|
765
|
+
it( 'converts red to hex', () => {
|
|
766
|
+
expect( normalizeColorToHex( 'red' ) ).toBe( '#ff0000' );
|
|
767
|
+
} );
|
|
768
|
+
|
|
769
|
+
it( 'returns unknown strings as-is', () => {
|
|
770
|
+
expect( normalizeColorToHex( 'notacolor' ) ).toBe( 'notacolor' );
|
|
771
|
+
} );
|
|
772
|
+
} );
|
|
773
|
+
|
|
729
774
|
describe( 'CSS variables', () => {
|
|
730
775
|
it( 'returns original if no resolveCss function provided', () => {
|
|
731
776
|
expect( normalizeColorToHex( '--my-color' ) ).toBe( '--my-color' );
|
|
@@ -755,6 +800,71 @@ describe( 'normalizeColorToHex', () => {
|
|
|
755
800
|
const mockResolve = jest.fn().mockReturnValue( null );
|
|
756
801
|
expect( normalizeColorToHex( '--my-color', null, mockResolve ) ).toBe( '--my-color' );
|
|
757
802
|
} );
|
|
803
|
+
|
|
804
|
+
it( 'returns original when CSS variable resolves to itself', () => {
|
|
805
|
+
const mockResolve = jest.fn().mockImplementation( ( v: string ) => v );
|
|
806
|
+
expect( normalizeColorToHex( '--loop', null, mockResolve ) ).toBe( '--loop' );
|
|
807
|
+
} );
|
|
808
|
+
|
|
809
|
+
it( 'returns original when CSS variable resolves to empty string', () => {
|
|
810
|
+
const mockResolve = jest.fn().mockReturnValue( '' );
|
|
811
|
+
expect( normalizeColorToHex( '--empty', null, mockResolve ) ).toBe( '--empty' );
|
|
812
|
+
} );
|
|
813
|
+
|
|
814
|
+
it( 'does not infinite loop on indirect CSS variable cycle', () => {
|
|
815
|
+
const mockResolve = jest.fn().mockImplementation( ( v: string ) => {
|
|
816
|
+
if ( v === '--a' ) return 'var(--b)';
|
|
817
|
+
if ( v === 'var(--b)' ) return '--a';
|
|
818
|
+
|
|
819
|
+
return null;
|
|
820
|
+
} );
|
|
821
|
+
|
|
822
|
+
expect( () => normalizeColorToHex( '--a', null, mockResolve ) ).not.toThrow();
|
|
823
|
+
} );
|
|
824
|
+
|
|
825
|
+
it( 'resolves multi-hop CSS variable chain', () => {
|
|
826
|
+
const mockResolve = jest.fn().mockImplementation( ( v: string ) => {
|
|
827
|
+
if ( v === '--a' ) return 'var(--b)';
|
|
828
|
+
if ( v === 'var(--b)' ) return 'hsl(0, 100%, 50%)';
|
|
829
|
+
|
|
830
|
+
return null;
|
|
831
|
+
} );
|
|
832
|
+
|
|
833
|
+
expect( normalizeColorToHex( '--a', null, mockResolve ) ).toBe( '#ff0000' );
|
|
834
|
+
} );
|
|
835
|
+
} );
|
|
836
|
+
|
|
837
|
+
describe( 'Whitespace handling', () => {
|
|
838
|
+
it( 'trims leading and trailing spaces from hex', () => {
|
|
839
|
+
expect( normalizeColorToHex( ' #ff0000 ' ) ).toBe( '#ff0000' );
|
|
840
|
+
} );
|
|
841
|
+
|
|
842
|
+
it( 'trims spaces from HSL string', () => {
|
|
843
|
+
expect( normalizeColorToHex( ' hsl(0, 100%, 50%) ' ) ).toBe( '#ff0000' );
|
|
844
|
+
} );
|
|
845
|
+
|
|
846
|
+
it( 'trims spaces from named color', () => {
|
|
847
|
+
expect( normalizeColorToHex( ' red ' ) ).toBe( '#ff0000' );
|
|
848
|
+
} );
|
|
849
|
+
} );
|
|
850
|
+
|
|
851
|
+
describe( 'Case insensitivity', () => {
|
|
852
|
+
it( 'converts uppercase HSL', () => {
|
|
853
|
+
expect( normalizeColorToHex( 'HSL(0, 100%, 50%)' ) ).toBe( '#ff0000' );
|
|
854
|
+
} );
|
|
855
|
+
|
|
856
|
+
it( 'converts uppercase RGB', () => {
|
|
857
|
+
expect( normalizeColorToHex( 'RGB(255, 0, 0)' ) ).toBe( '#ff0000' );
|
|
858
|
+
} );
|
|
859
|
+
|
|
860
|
+
it( 'converts uppercase RGBA', () => {
|
|
861
|
+
expect( normalizeColorToHex( 'RGBA(0, 0, 255, 1)' ) ).toBe( '#0000ff' );
|
|
862
|
+
} );
|
|
863
|
+
|
|
864
|
+
it( 'handles uppercase VAR() syntax', () => {
|
|
865
|
+
const mockResolve = jest.fn().mockReturnValue( '#ff0000' );
|
|
866
|
+
expect( normalizeColorToHex( 'VAR(--my-color)', null, mockResolve ) ).toBe( '#ff0000' );
|
|
867
|
+
} );
|
|
758
868
|
} );
|
|
759
869
|
|
|
760
870
|
describe( 'Invalid inputs', () => {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { sanitizeHtml } from '../sanitize-html';
|
|
2
|
+
|
|
3
|
+
describe( 'sanitizeHtml', () => {
|
|
4
|
+
test( 'preserves safe formatting tags', () => {
|
|
5
|
+
const input = '<b>Bold</b> <em>italic</em> <strong>strong</strong>';
|
|
6
|
+
expect( sanitizeHtml( input ) ).toBe( '<b>Bold</b> <em>italic</em> <strong>strong</strong>' );
|
|
7
|
+
} );
|
|
8
|
+
|
|
9
|
+
test( 'strips style attributes', () => {
|
|
10
|
+
const input = '<div style="padding: 12px;"><span style="color: red;">text</span></div>';
|
|
11
|
+
expect( sanitizeHtml( input ) ).toBe( '<div><span>text</span></div>' );
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
test( 'preserves br tags', () => {
|
|
15
|
+
const input = 'line one<br>line two';
|
|
16
|
+
expect( sanitizeHtml( input ) ).toBe( 'line one<br>line two' );
|
|
17
|
+
} );
|
|
18
|
+
|
|
19
|
+
test( 'removes script tags', () => {
|
|
20
|
+
const input = '<b>Safe</b><script>alert("xss")</script>';
|
|
21
|
+
expect( sanitizeHtml( input ) ).toBe( '<b>Safe</b>' );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
test( 'removes img tags with onerror handlers', () => {
|
|
25
|
+
const input = '<b>Safe</b><img src=x onerror="alert(document.cookie)">';
|
|
26
|
+
expect( sanitizeHtml( input ) ).toBe( '<b>Safe</b>' );
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
test( 'removes iframe tags', () => {
|
|
30
|
+
const input = '<div>text</div><iframe src="https://evil.com"></iframe>';
|
|
31
|
+
expect( sanitizeHtml( input ) ).toBe( '<div>text</div>' );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
test( 'removes event handler attributes from allowed tags', () => {
|
|
35
|
+
const input = '<div onclick="alert(1)" onmouseover="steal()">text</div>';
|
|
36
|
+
expect( sanitizeHtml( input ) ).toBe( '<div>text</div>' );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
test( 'removes javascript: URLs from href', () => {
|
|
40
|
+
const input = '<a href="javascript:alert(1)">click</a>';
|
|
41
|
+
expect( sanitizeHtml( input ) ).toBe( '<a>click</a>' );
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
test( 'preserves safe href values', () => {
|
|
45
|
+
const input = '<a href="https://example.com" target="_blank" rel="noopener">link</a>';
|
|
46
|
+
expect( sanitizeHtml( input ) ).toBe(
|
|
47
|
+
'<a href="https://example.com" target="_blank" rel="noopener noreferrer">link</a>'
|
|
48
|
+
);
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
test( 'removes object and embed tags', () => {
|
|
52
|
+
const input = '<div>safe</div><object data="x"></object><embed src="y">';
|
|
53
|
+
expect( sanitizeHtml( input ) ).toBe( '<div>safe</div>' );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
test( 'removes form tags but preserves safe child content', () => {
|
|
57
|
+
const input = '<form action="https://evil.com"><div>trap</div></form>';
|
|
58
|
+
const result = sanitizeHtml( input );
|
|
59
|
+
expect( result ).not.toContain( '<form' );
|
|
60
|
+
expect( result ).not.toContain( 'action' );
|
|
61
|
+
expect( result ).toContain( '<div>trap</div>' );
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
test( 'handles complex tooltip HTML and strips style attributes', () => {
|
|
65
|
+
const input = `<div style="padding: 12px; font-family: sans-serif;">
|
|
66
|
+
<div style="font-weight: bold;">United States</div>
|
|
67
|
+
<div style="color: #666;">Orders: <strong>1,000</strong></div>
|
|
68
|
+
</div>`;
|
|
69
|
+
const result = sanitizeHtml( input );
|
|
70
|
+
expect( result ).toContain( 'United States' );
|
|
71
|
+
expect( result ).toContain( '<strong>1,000</strong>' );
|
|
72
|
+
expect( result ).not.toContain( 'style' );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
test( 'handles empty string', () => {
|
|
76
|
+
expect( sanitizeHtml( '' ) ).toBe( '' );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
test( 'handles plain text', () => {
|
|
80
|
+
expect( sanitizeHtml( 'just text' ) ).toBe( 'just text' );
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
test( 'removes disallowed attributes like id and style', () => {
|
|
84
|
+
const input = '<div id="evil" style="color: red;">text</div>';
|
|
85
|
+
expect( sanitizeHtml( input ) ).toBe( '<div>text</div>' );
|
|
86
|
+
} );
|
|
87
|
+
|
|
88
|
+
test( 'enforces rel="noopener noreferrer" on target="_blank" links', () => {
|
|
89
|
+
const input = '<a href="https://example.com" target="_blank">link</a>';
|
|
90
|
+
expect( sanitizeHtml( input ) ).toBe(
|
|
91
|
+
'<a href="https://example.com" target="_blank" rel="noopener noreferrer">link</a>'
|
|
92
|
+
);
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
test( 'overrides insufficient rel on target="_blank" links', () => {
|
|
96
|
+
const input = '<a href="https://example.com" target="_blank" rel="noopener">link</a>';
|
|
97
|
+
expect( sanitizeHtml( input ) ).toBe(
|
|
98
|
+
'<a href="https://example.com" target="_blank" rel="noopener noreferrer">link</a>'
|
|
99
|
+
);
|
|
100
|
+
} );
|
|
101
|
+
} );
|