@react-magma/charts 14.0.0-next.2 → 14.0.0-next.4
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/dist/charts.js +851 -69
- package/dist/charts.js.map +1 -1
- package/dist/charts.modern.module.js +847 -72
- package/dist/charts.modern.module.js.map +1 -1
- package/dist/charts.umd.js +3088 -680
- package/dist/charts.umd.js.map +1 -1
- package/dist/components/CarbonChart/CarbonChart.d.ts +46 -0
- package/dist/components/CarbonChart/CarbonChartBar.stories.d.ts +27 -9
- package/dist/components/ChartTable/ChartDataTable.d.ts +19 -0
- package/dist/components/ChartTable/ChartFullscreenButton.d.ts +26 -0
- package/dist/components/ChartTable/ChartMoreOptionsButton.d.ts +18 -0
- package/dist/components/ChartTable/ChartTable.stories.d.ts +116 -0
- package/dist/components/ChartTable/ChartTableButton.d.ts +24 -0
- package/dist/components/ChartTable/ChartTableModal.d.ts +44 -0
- package/dist/components/ChartTable/ChartToolbar.d.ts +19 -0
- package/dist/components/ChartTable/chartToolbarI18n.d.ts +17 -0
- package/dist/components/ChartTable/index.d.ts +14 -0
- package/dist/components/LineChart/DataTable.d.ts +1 -1
- package/dist/hooks/useCarbonModalFocusManagement.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/package.json +5 -5
- package/src/components/CarbonChart/CarbonChart.test.js +645 -2
- package/src/components/CarbonChart/CarbonChart.tsx +950 -59
- package/src/components/CarbonChart/CarbonChartArea.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartAreaStacked.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBar.stories.tsx +9 -2
- package/src/components/CarbonChart/CarbonChartBarFloating.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarGrouped.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBarStacked.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBoxplot.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBubble.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartBullet.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartCombo.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartDonut.stories.tsx +3 -1
- package/src/components/CarbonChart/CarbonChartGauge.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartHistogram.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartLine.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartLollipop.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartMeter.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartPie.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartRadar.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartScatter.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartSparkline.stories.tsx +1 -1
- package/src/components/CarbonChart/CarbonChartStep.stories.tsx +1 -1
- package/src/components/ChartTable/ChartDataTable.tsx +72 -0
- package/src/components/ChartTable/ChartFullscreenButton.tsx +59 -0
- package/src/components/ChartTable/ChartMoreOptionsButton.tsx +48 -0
- package/src/components/ChartTable/ChartTable.stories.tsx +152 -0
- package/src/components/ChartTable/ChartTable.test.js +444 -0
- package/src/components/ChartTable/ChartTableButton.tsx +55 -0
- package/src/components/ChartTable/ChartTableModal.tsx +135 -0
- package/src/components/ChartTable/ChartToolbar.tsx +50 -0
- package/src/components/ChartTable/chartToolbarI18n.ts +59 -0
- package/src/components/ChartTable/index.ts +23 -0
- package/src/components/LineChart/DataTable.tsx +3 -3
- package/src/components/LineChart/LineChart.tsx +1 -1
- package/src/hooks/useCarbonModalFocusManagement.ts +173 -0
- package/src/index.ts +1 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { render } from '@testing-library/react';
|
|
4
|
-
import
|
|
3
|
+
import { act, render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import {
|
|
6
|
+
ThemeContext,
|
|
7
|
+
magma,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
I18nContext,
|
|
10
|
+
defaultI18n,
|
|
11
|
+
} from 'react-magma-dom';
|
|
5
12
|
|
|
6
13
|
import { CarbonChart, CarbonChartType } from '.';
|
|
7
14
|
|
|
@@ -11,6 +18,20 @@ global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
|
|
11
18
|
disconnect: jest.fn(),
|
|
12
19
|
}));
|
|
13
20
|
|
|
21
|
+
// Capture MutationObserver callbacks so we can trigger them manually.
|
|
22
|
+
// Multiple observers may be created per component; we collect all of them and
|
|
23
|
+
// call each one so that no observer is silently skipped.
|
|
24
|
+
let mutationObserverCallbacks = [];
|
|
25
|
+
const mutationObserverCallback = mutations =>
|
|
26
|
+
mutationObserverCallbacks.forEach(cb => cb(mutations));
|
|
27
|
+
global.MutationObserver = jest.fn().mockImplementation(callback => {
|
|
28
|
+
mutationObserverCallbacks.push(callback);
|
|
29
|
+
return {
|
|
30
|
+
observe: jest.fn(),
|
|
31
|
+
disconnect: jest.fn(),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
14
35
|
const dataSet = [
|
|
15
36
|
{
|
|
16
37
|
group: 'Qty',
|
|
@@ -578,5 +599,627 @@ describe('CarbonChart', () => {
|
|
|
578
599
|
}
|
|
579
600
|
);
|
|
580
601
|
});
|
|
602
|
+
|
|
603
|
+
it('should move focus back after closing the More options dropdown and chart toolbar', () => {
|
|
604
|
+
const testId = 'modal-footer-border-test';
|
|
605
|
+
const { getByTestId, getByText } = render(
|
|
606
|
+
<ThemeContext.Provider value={magma}>
|
|
607
|
+
<CarbonChart
|
|
608
|
+
testId={testId}
|
|
609
|
+
dataSet={dataSet}
|
|
610
|
+
options={chartOptions}
|
|
611
|
+
type={CarbonChartType.bar}
|
|
612
|
+
isInverse={false}
|
|
613
|
+
chartToolbar={{}}
|
|
614
|
+
/>
|
|
615
|
+
</ThemeContext.Provider>
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const moreOptionsButton = getByTestId('chart-more-options-button');
|
|
619
|
+
userEvent.click(moreOptionsButton);
|
|
620
|
+
|
|
621
|
+
expect(getByText('Download as CSV')).toBeVisible();
|
|
622
|
+
expect(getByText('Download as PNG')).toBeVisible();
|
|
623
|
+
expect(getByText('Download as JPG')).toBeVisible();
|
|
624
|
+
|
|
625
|
+
userEvent.keyboard('{esc}');
|
|
626
|
+
expect(moreOptionsButton).toHaveFocus();
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
describe('Modal Focus Management', () => {
|
|
631
|
+
let wrapper;
|
|
632
|
+
let modal;
|
|
633
|
+
let closeButton;
|
|
634
|
+
let otherButton;
|
|
635
|
+
|
|
636
|
+
beforeEach(() => {
|
|
637
|
+
mutationObserverCallbacks = [];
|
|
638
|
+
jest.useFakeTimers();
|
|
639
|
+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
|
|
640
|
+
cb(0);
|
|
641
|
+
return 0;
|
|
642
|
+
});
|
|
643
|
+
const testId = 'focus-mgmt-test';
|
|
644
|
+
const { getByTestId } = render(
|
|
645
|
+
<CarbonChart
|
|
646
|
+
testId={testId}
|
|
647
|
+
dataSet={dataSet}
|
|
648
|
+
options={chartOptions}
|
|
649
|
+
type={CarbonChartType.bar}
|
|
650
|
+
/>
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
wrapper = getByTestId(testId);
|
|
654
|
+
|
|
655
|
+
// Manually create Carbon modal DOM inside the wrapper
|
|
656
|
+
modal = document.createElement('div');
|
|
657
|
+
modal.className = 'cds--modal';
|
|
658
|
+
|
|
659
|
+
closeButton = document.createElement('button');
|
|
660
|
+
closeButton.className = 'cds--modal-close';
|
|
661
|
+
closeButton.textContent = 'Close';
|
|
662
|
+
|
|
663
|
+
otherButton = document.createElement('button');
|
|
664
|
+
otherButton.textContent = 'Copy';
|
|
665
|
+
|
|
666
|
+
modal.appendChild(closeButton);
|
|
667
|
+
modal.appendChild(otherButton);
|
|
668
|
+
wrapper.appendChild(modal);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
afterEach(() => {
|
|
672
|
+
window.requestAnimationFrame.mockRestore();
|
|
673
|
+
jest.useRealTimers();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Simulate Carbon's handleShowModal: sets aria-modal, role, style
|
|
677
|
+
function simulateModalOpen() {
|
|
678
|
+
modal.setAttribute('aria-modal', 'true');
|
|
679
|
+
modal.setAttribute('role', 'dialog');
|
|
680
|
+
modal.style.visibility = 'visible';
|
|
681
|
+
modal.style.opacity = '1';
|
|
682
|
+
act(() => {
|
|
683
|
+
mutationObserverCallback([
|
|
684
|
+
{ type: 'attributes', attributeName: 'aria-modal', target: modal },
|
|
685
|
+
{ type: 'attributes', attributeName: 'style', target: modal },
|
|
686
|
+
]);
|
|
687
|
+
});
|
|
688
|
+
jest.runAllTimers();
|
|
689
|
+
act(() => {
|
|
690
|
+
jest.advanceTimersByTime(0);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Simulate Carbon's handleHideModal: removes aria-modal, role, sets hidden
|
|
695
|
+
function simulateModalClose() {
|
|
696
|
+
modal.removeAttribute('aria-modal');
|
|
697
|
+
modal.removeAttribute('role');
|
|
698
|
+
modal.style.visibility = 'hidden';
|
|
699
|
+
modal.style.opacity = '0';
|
|
700
|
+
act(() => {
|
|
701
|
+
mutationObserverCallback([
|
|
702
|
+
{ type: 'attributes', attributeName: 'aria-modal', target: modal },
|
|
703
|
+
{ type: 'attributes', attributeName: 'style', target: modal },
|
|
704
|
+
]);
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
it('should move focus to the close button when modal opens', () => {
|
|
709
|
+
const triggerButton = document.createElement('button');
|
|
710
|
+
triggerButton.textContent = 'Show as table';
|
|
711
|
+
wrapper.appendChild(triggerButton);
|
|
712
|
+
triggerButton.focus();
|
|
713
|
+
|
|
714
|
+
simulateModalOpen();
|
|
715
|
+
|
|
716
|
+
expect(document.activeElement).toBe(closeButton);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('should restore focus to the previously focused element when modal closes', () => {
|
|
720
|
+
const triggerButton = document.createElement('button');
|
|
721
|
+
triggerButton.textContent = 'Show as table';
|
|
722
|
+
wrapper.appendChild(triggerButton);
|
|
723
|
+
triggerButton.focus();
|
|
724
|
+
|
|
725
|
+
simulateModalOpen();
|
|
726
|
+
simulateModalClose();
|
|
727
|
+
|
|
728
|
+
expect(document.activeElement).toBe(triggerButton);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('should move focus into modal on second open after close', () => {
|
|
732
|
+
const triggerButton = document.createElement('button');
|
|
733
|
+
triggerButton.textContent = 'Show as table';
|
|
734
|
+
wrapper.appendChild(triggerButton);
|
|
735
|
+
triggerButton.focus();
|
|
736
|
+
|
|
737
|
+
// First open/close cycle
|
|
738
|
+
simulateModalOpen();
|
|
739
|
+
simulateModalClose();
|
|
740
|
+
expect(document.activeElement).toBe(triggerButton);
|
|
741
|
+
|
|
742
|
+
// Second open - focus should move into modal again
|
|
743
|
+
simulateModalOpen();
|
|
744
|
+
expect(document.activeElement).toBe(closeButton);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should trap focus with Tab wrapping from last to first element', () => {
|
|
748
|
+
simulateModalOpen();
|
|
749
|
+
|
|
750
|
+
otherButton.focus();
|
|
751
|
+
expect(document.activeElement).toBe(otherButton);
|
|
752
|
+
|
|
753
|
+
const tabEvent = new KeyboardEvent('keydown', {
|
|
754
|
+
key: 'Tab',
|
|
755
|
+
bubbles: true,
|
|
756
|
+
});
|
|
757
|
+
Object.defineProperty(tabEvent, 'shiftKey', { value: false });
|
|
758
|
+
document.dispatchEvent(tabEvent);
|
|
759
|
+
|
|
760
|
+
expect(document.activeElement).toBe(closeButton);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should trap focus with Shift+Tab wrapping from first to last element', () => {
|
|
764
|
+
simulateModalOpen();
|
|
765
|
+
|
|
766
|
+
closeButton.focus();
|
|
767
|
+
expect(document.activeElement).toBe(closeButton);
|
|
768
|
+
|
|
769
|
+
const shiftTabEvent = new KeyboardEvent('keydown', {
|
|
770
|
+
key: 'Tab',
|
|
771
|
+
shiftKey: true,
|
|
772
|
+
bubbles: true,
|
|
773
|
+
});
|
|
774
|
+
document.dispatchEvent(shiftTabEvent);
|
|
775
|
+
|
|
776
|
+
expect(document.activeElement).toBe(otherButton);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('should redirect focus back to modal when focus escapes to an outside element', () => {
|
|
780
|
+
simulateModalOpen();
|
|
781
|
+
expect(document.activeElement).toBe(closeButton);
|
|
782
|
+
|
|
783
|
+
// Simulate overflow menu stealing focus (what Carbon does on second open)
|
|
784
|
+
const outsideButton = document.createElement('button');
|
|
785
|
+
outsideButton.className = 'cds--overflow-menu__trigger';
|
|
786
|
+
wrapper.appendChild(outsideButton);
|
|
787
|
+
outsideButton.focus();
|
|
788
|
+
|
|
789
|
+
// The redirect is deferred via setTimeout(0) to avoid re-entrancy issues
|
|
790
|
+
jest.runAllTimers();
|
|
791
|
+
|
|
792
|
+
expect(document.activeElement).toBe(closeButton);
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe('chartToolbar prop', () => {
|
|
797
|
+
const toolbarProps = {
|
|
798
|
+
dataSet,
|
|
799
|
+
options: chartOptions,
|
|
800
|
+
type: CarbonChartType.bar,
|
|
801
|
+
chartToolbar: {},
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
it('should render the show-as-table button with aria-haspopup="dialog"', () => {
|
|
805
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
806
|
+
|
|
807
|
+
const button = screen.getByRole('button', {
|
|
808
|
+
name: chartOptions.title,
|
|
809
|
+
});
|
|
810
|
+
expect(button).toHaveAttribute('aria-haspopup', 'dialog');
|
|
811
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('should render the fullscreen button without aria-haspopup', () => {
|
|
815
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
816
|
+
|
|
817
|
+
const button = screen.getByRole('button', {
|
|
818
|
+
name: `View ${chartOptions.title} full screen`,
|
|
819
|
+
});
|
|
820
|
+
expect(button).not.toHaveAttribute('aria-haspopup');
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it('should open the table modal when show-as-table button is clicked', () => {
|
|
824
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
825
|
+
|
|
826
|
+
fireEvent.click(screen.getByRole('button', { name: chartOptions.title }));
|
|
827
|
+
|
|
828
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
829
|
+
expect(
|
|
830
|
+
screen.getByRole('heading', {
|
|
831
|
+
level: 2,
|
|
832
|
+
name: `Tabular representation ${chartOptions.title}`,
|
|
833
|
+
})
|
|
834
|
+
).toBeInTheDocument();
|
|
835
|
+
expect(
|
|
836
|
+
screen.getByRole('columnheader', { name: 'Group' })
|
|
837
|
+
).toBeInTheDocument();
|
|
838
|
+
expect(
|
|
839
|
+
screen.getByRole('columnheader', { name: 'Value' })
|
|
840
|
+
).toBeInTheDocument();
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('should render chart data in the table modal', () => {
|
|
844
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
845
|
+
|
|
846
|
+
fireEvent.click(screen.getByRole('button', { name: chartOptions.title }));
|
|
847
|
+
|
|
848
|
+
expect(screen.getByRole('cell', { name: 'Qty' })).toBeInTheDocument();
|
|
849
|
+
expect(screen.getByRole('cell', { name: '65000' })).toBeInTheDocument();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('should set aria-expanded to true when modal is open', () => {
|
|
853
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
854
|
+
|
|
855
|
+
const tableButton = screen.getByRole('button', {
|
|
856
|
+
name: chartOptions.title,
|
|
857
|
+
});
|
|
858
|
+
fireEvent.click(tableButton);
|
|
859
|
+
|
|
860
|
+
expect(tableButton).toHaveAttribute('aria-expanded', 'true');
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('should not render the table button when showAsTable is false', () => {
|
|
864
|
+
render(
|
|
865
|
+
<CarbonChart {...toolbarProps} chartToolbar={{ showAsTable: false }} />
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
expect(
|
|
869
|
+
screen.queryByRole('button', { name: chartOptions.title })
|
|
870
|
+
).not.toBeInTheDocument();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it('should not render the fullscreen button when fullscreen is false', () => {
|
|
874
|
+
render(
|
|
875
|
+
<CarbonChart {...toolbarProps} chartToolbar={{ fullscreen: false }} />
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
expect(
|
|
879
|
+
screen.queryByRole('button', { name: /full screen/i })
|
|
880
|
+
).not.toBeInTheDocument();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('should render the chart title as h2 by default', () => {
|
|
884
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
885
|
+
|
|
886
|
+
expect(
|
|
887
|
+
screen.getByRole('heading', { level: 2, name: chartOptions.title })
|
|
888
|
+
).toBeInTheDocument();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should render the chart title at the level set by titleLevel', () => {
|
|
892
|
+
render(
|
|
893
|
+
<CarbonChart {...toolbarProps} chartToolbar={{ titleLevel: 3 }} />
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
expect(
|
|
897
|
+
screen.getByRole('heading', { level: 3, name: chartOptions.title })
|
|
898
|
+
).toBeInTheDocument();
|
|
899
|
+
expect(
|
|
900
|
+
screen.queryByRole('heading', { level: 2, name: chartOptions.title })
|
|
901
|
+
).not.toBeInTheDocument();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('should always render the more options dropdown with built-in download items', () => {
|
|
905
|
+
render(<CarbonChart {...toolbarProps} />);
|
|
906
|
+
|
|
907
|
+
const moreBtn = screen.getByRole('button', { name: 'More options' });
|
|
908
|
+
expect(moreBtn).toBeInTheDocument();
|
|
909
|
+
|
|
910
|
+
fireEvent.click(moreBtn);
|
|
911
|
+
expect(screen.getByText('Download as CSV')).toBeVisible();
|
|
912
|
+
expect(screen.getByText('Download as PNG')).toBeVisible();
|
|
913
|
+
expect(screen.getByText('Download as JPG')).toBeVisible();
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('should render additional moreOptions items below built-in downloads', () => {
|
|
917
|
+
render(
|
|
918
|
+
<CarbonChart
|
|
919
|
+
{...toolbarProps}
|
|
920
|
+
chartToolbar={{
|
|
921
|
+
moreOptions: <DropdownMenuItem>Custom Action</DropdownMenuItem>,
|
|
922
|
+
}}
|
|
923
|
+
/>
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
const moreBtn = screen.getByRole('button', { name: 'More options' });
|
|
927
|
+
fireEvent.click(moreBtn);
|
|
928
|
+
expect(screen.getByText('Download as CSV')).toBeVisible();
|
|
929
|
+
expect(screen.getByText('Download as PNG')).toBeVisible();
|
|
930
|
+
expect(screen.getByText('Download as JPG')).toBeVisible();
|
|
931
|
+
expect(screen.getByText('Custom Action')).toBeVisible();
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('should support custom table columns', () => {
|
|
935
|
+
render(
|
|
936
|
+
<CarbonChart
|
|
937
|
+
{...toolbarProps}
|
|
938
|
+
chartToolbar={{
|
|
939
|
+
tableColumns: [
|
|
940
|
+
{ header: 'Category', key: 'group' },
|
|
941
|
+
{ header: 'Count', key: 'value' },
|
|
942
|
+
],
|
|
943
|
+
}}
|
|
944
|
+
/>
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
fireEvent.click(screen.getByRole('button', { name: chartOptions.title }));
|
|
948
|
+
|
|
949
|
+
expect(
|
|
950
|
+
screen.getByRole('columnheader', { name: 'Category' })
|
|
951
|
+
).toBeInTheDocument();
|
|
952
|
+
expect(
|
|
953
|
+
screen.getByRole('columnheader', { name: 'Count' })
|
|
954
|
+
).toBeInTheDocument();
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
describe('dot keyboard accessibility', () => {
|
|
959
|
+
let rafCallbacks;
|
|
960
|
+
|
|
961
|
+
beforeEach(() => {
|
|
962
|
+
rafCallbacks = [];
|
|
963
|
+
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => {
|
|
964
|
+
rafCallbacks.push(cb);
|
|
965
|
+
return rafCallbacks.length - 1;
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
afterEach(() => {
|
|
970
|
+
window.requestAnimationFrame.mockRestore();
|
|
971
|
+
rafCallbacks = [];
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
function renderChart() {
|
|
975
|
+
const { getByTestId } = render(
|
|
976
|
+
<CarbonChart
|
|
977
|
+
testId="dot-tab-test"
|
|
978
|
+
dataSet={dataSet}
|
|
979
|
+
options={chartOptions}
|
|
980
|
+
type={CarbonChartType.scatter}
|
|
981
|
+
/>
|
|
982
|
+
);
|
|
983
|
+
return getByTestId('dot-tab-test');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function addDot(wrapper) {
|
|
987
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
988
|
+
const dot = document.createElementNS(
|
|
989
|
+
'http://www.w3.org/2000/svg',
|
|
990
|
+
'circle'
|
|
991
|
+
);
|
|
992
|
+
dot.classList.add('dot');
|
|
993
|
+
svg.appendChild(dot);
|
|
994
|
+
wrapper.appendChild(svg);
|
|
995
|
+
return dot;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
describe('tabbing', () => {
|
|
999
|
+
it('should stamp tabindex="0" on circle.dot elements so they are reachable by Tab', () => {
|
|
1000
|
+
const wrapper = renderChart();
|
|
1001
|
+
const dot = addDot(wrapper);
|
|
1002
|
+
|
|
1003
|
+
// The last captured RAF is the dots-stamping callback (no ariaLabel provided)
|
|
1004
|
+
act(() => rafCallbacks[rafCallbacks.length - 1](0));
|
|
1005
|
+
|
|
1006
|
+
expect(dot).toHaveAttribute('tabindex', '0');
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it('should not overwrite an existing tabindex on circle.dot', () => {
|
|
1010
|
+
const wrapper = renderChart();
|
|
1011
|
+
const dot = addDot(wrapper);
|
|
1012
|
+
dot.setAttribute('tabindex', '-1');
|
|
1013
|
+
|
|
1014
|
+
act(() => rafCallbacks[rafCallbacks.length - 1](0));
|
|
1015
|
+
|
|
1016
|
+
expect(dot).toHaveAttribute('tabindex', '-1');
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
describe('focus on dot content', () => {
|
|
1021
|
+
it('should set opacity to 1 when a dot receives focus', () => {
|
|
1022
|
+
const wrapper = renderChart();
|
|
1023
|
+
const dot = addDot(wrapper);
|
|
1024
|
+
|
|
1025
|
+
fireEvent.focusIn(dot);
|
|
1026
|
+
|
|
1027
|
+
expect(dot.style.opacity).toBe('1');
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('should dispatch mouseover on dot focusin to reveal tooltip data', () => {
|
|
1031
|
+
const wrapper = renderChart();
|
|
1032
|
+
const dot = addDot(wrapper);
|
|
1033
|
+
|
|
1034
|
+
const mouseoverSpy = jest.fn();
|
|
1035
|
+
dot.addEventListener('mouseover', mouseoverSpy);
|
|
1036
|
+
|
|
1037
|
+
fireEvent.focusIn(dot);
|
|
1038
|
+
|
|
1039
|
+
expect(mouseoverSpy).toHaveBeenCalledTimes(1);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('should dispatch mousemove on dot focusin', () => {
|
|
1043
|
+
const wrapper = renderChart();
|
|
1044
|
+
const dot = addDot(wrapper);
|
|
1045
|
+
|
|
1046
|
+
const mousemoveSpy = jest.fn();
|
|
1047
|
+
dot.addEventListener('mousemove', mousemoveSpy);
|
|
1048
|
+
|
|
1049
|
+
fireEvent.focusIn(dot);
|
|
1050
|
+
|
|
1051
|
+
expect(mousemoveSpy).toHaveBeenCalledTimes(1);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('should not change opacity for non-dot circle elements on focusin', () => {
|
|
1055
|
+
const wrapper = renderChart();
|
|
1056
|
+
const nonDot = document.createElementNS(
|
|
1057
|
+
'http://www.w3.org/2000/svg',
|
|
1058
|
+
'circle'
|
|
1059
|
+
);
|
|
1060
|
+
wrapper.appendChild(nonDot);
|
|
1061
|
+
|
|
1062
|
+
fireEvent.focusIn(nonDot);
|
|
1063
|
+
|
|
1064
|
+
expect(nonDot.style.opacity).toBe('');
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
describe('data visibility', () => {
|
|
1069
|
+
it('should reset dot opacity when dot loses focus', () => {
|
|
1070
|
+
const wrapper = renderChart();
|
|
1071
|
+
const dot = addDot(wrapper);
|
|
1072
|
+
|
|
1073
|
+
fireEvent.focusIn(dot);
|
|
1074
|
+
expect(dot.style.opacity).toBe('1');
|
|
1075
|
+
|
|
1076
|
+
fireEvent.focusOut(dot);
|
|
1077
|
+
expect(dot.style.opacity).toBe('');
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it('should dispatch mouseout on dot focusout to hide tooltip', () => {
|
|
1081
|
+
const wrapper = renderChart();
|
|
1082
|
+
const dot = addDot(wrapper);
|
|
1083
|
+
|
|
1084
|
+
const mouseoutSpy = jest.fn();
|
|
1085
|
+
dot.addEventListener('mouseout', mouseoutSpy);
|
|
1086
|
+
|
|
1087
|
+
fireEvent.focusIn(dot);
|
|
1088
|
+
fireEvent.focusOut(dot);
|
|
1089
|
+
|
|
1090
|
+
expect(mouseoutSpy).toHaveBeenCalledTimes(1);
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
describe('Legend accessibility wrap', () => {
|
|
1096
|
+
const testId = 'legend-a11y';
|
|
1097
|
+
|
|
1098
|
+
beforeEach(() => {
|
|
1099
|
+
mutationObserverCallbacks = [];
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it('wraps the Carbon legend in a fieldset with a legend caption derived from the chart title', () => {
|
|
1103
|
+
const { getByTestId } = render(
|
|
1104
|
+
<CarbonChart
|
|
1105
|
+
testId={testId}
|
|
1106
|
+
dataSet={dataSet}
|
|
1107
|
+
options={chartOptions}
|
|
1108
|
+
type={CarbonChartType.bar}
|
|
1109
|
+
/>
|
|
1110
|
+
);
|
|
1111
|
+
const wrapper = getByTestId(testId);
|
|
1112
|
+
|
|
1113
|
+
const fieldset = wrapper.querySelector('.cds--cc--legend-fieldset');
|
|
1114
|
+
expect(fieldset).not.toBeNull();
|
|
1115
|
+
expect(fieldset.tagName).toBe('FIELDSET');
|
|
1116
|
+
expect(fieldset.querySelector('.cds--cc--legend')).not.toBeNull();
|
|
1117
|
+
|
|
1118
|
+
const caption = fieldset.querySelector('legend');
|
|
1119
|
+
expect(caption).not.toBeNull();
|
|
1120
|
+
expect(caption.textContent).toBe(
|
|
1121
|
+
`${chartOptions.title}. Checking these checkboxes will update the chart.`
|
|
1122
|
+
);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it('uses ariaLabel for the legend caption when provided', () => {
|
|
1126
|
+
const ariaLabel = 'Quarterly sales chart';
|
|
1127
|
+
const { getByTestId } = render(
|
|
1128
|
+
<CarbonChart
|
|
1129
|
+
testId={testId}
|
|
1130
|
+
ariaLabel={ariaLabel}
|
|
1131
|
+
dataSet={dataSet}
|
|
1132
|
+
options={chartOptions}
|
|
1133
|
+
type={CarbonChartType.bar}
|
|
1134
|
+
/>
|
|
1135
|
+
);
|
|
1136
|
+
const wrapper = getByTestId(testId);
|
|
1137
|
+
|
|
1138
|
+
const caption = wrapper.querySelector(
|
|
1139
|
+
'.cds--cc--legend-fieldset > legend'
|
|
1140
|
+
);
|
|
1141
|
+
expect(caption).not.toBeNull();
|
|
1142
|
+
expect(caption.textContent).toBe(
|
|
1143
|
+
`${ariaLabel}. Checking these checkboxes will update the chart.`
|
|
1144
|
+
);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('does not duplicate the fieldset when the mutation observer fires again with no DOM changes', () => {
|
|
1148
|
+
const { getByTestId } = render(
|
|
1149
|
+
<CarbonChart
|
|
1150
|
+
testId={testId}
|
|
1151
|
+
dataSet={dataSet}
|
|
1152
|
+
options={chartOptions}
|
|
1153
|
+
type={CarbonChartType.bar}
|
|
1154
|
+
/>
|
|
1155
|
+
);
|
|
1156
|
+
const wrapper = getByTestId(testId);
|
|
1157
|
+
|
|
1158
|
+
const before = wrapper.querySelectorAll('.cds--cc--legend-fieldset');
|
|
1159
|
+
expect(before).toHaveLength(1);
|
|
1160
|
+
|
|
1161
|
+
act(() => {
|
|
1162
|
+
mutationObserverCallback?.();
|
|
1163
|
+
mutationObserverCallback?.();
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
const after = wrapper.querySelectorAll('.cds--cc--legend-fieldset');
|
|
1167
|
+
expect(after).toHaveLength(1);
|
|
1168
|
+
expect(after[0]).toBe(before[0]);
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it('replaces Carbon\'s generic "Data groups" aria-label with semantic list roles on the legend and its items', () => {
|
|
1172
|
+
const { getByTestId } = render(
|
|
1173
|
+
<CarbonChart
|
|
1174
|
+
testId={testId}
|
|
1175
|
+
dataSet={dataSet}
|
|
1176
|
+
options={chartOptions}
|
|
1177
|
+
type={CarbonChartType.bar}
|
|
1178
|
+
/>
|
|
1179
|
+
);
|
|
1180
|
+
const wrapper = getByTestId(testId);
|
|
1181
|
+
|
|
1182
|
+
const legend = wrapper.querySelector('.cds--cc--legend');
|
|
1183
|
+
expect(legend).not.toBeNull();
|
|
1184
|
+
expect(legend.getAttribute('aria-label')).toBeNull();
|
|
1185
|
+
expect(legend.getAttribute('role')).toBe('list');
|
|
1186
|
+
|
|
1187
|
+
const items = legend.querySelectorAll('.legend-item');
|
|
1188
|
+
expect(items.length).toBeGreaterThan(0);
|
|
1189
|
+
items.forEach(item => {
|
|
1190
|
+
expect(item.getAttribute('role')).toBe('listitem');
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it('uses translated legend instructions from I18nContext', () => {
|
|
1195
|
+
const translatedInstructions =
|
|
1196
|
+
'Marquer ces cases mettra à jour le graphique.';
|
|
1197
|
+
const i18nValue = {
|
|
1198
|
+
...defaultI18n,
|
|
1199
|
+
charts: {
|
|
1200
|
+
...defaultI18n.charts,
|
|
1201
|
+
toolbar: { legendInstructions: translatedInstructions },
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const { getByTestId } = render(
|
|
1206
|
+
<I18nContext.Provider value={i18nValue}>
|
|
1207
|
+
<CarbonChart
|
|
1208
|
+
testId={testId}
|
|
1209
|
+
dataSet={dataSet}
|
|
1210
|
+
options={chartOptions}
|
|
1211
|
+
type={CarbonChartType.bar}
|
|
1212
|
+
/>
|
|
1213
|
+
</I18nContext.Provider>
|
|
1214
|
+
);
|
|
1215
|
+
const wrapper = getByTestId(testId);
|
|
1216
|
+
|
|
1217
|
+
const caption = wrapper.querySelector(
|
|
1218
|
+
'.cds--cc--legend-fieldset > legend'
|
|
1219
|
+
);
|
|
1220
|
+
expect(caption.textContent).toBe(
|
|
1221
|
+
`${chartOptions.title}. ${translatedInstructions}`
|
|
1222
|
+
);
|
|
1223
|
+
});
|
|
581
1224
|
});
|
|
582
1225
|
});
|