Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for adding accessibility features at chart generation #1738

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions spec/bar-chart-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,81 @@ describe('dc.BarChart', () => {
}
});

describe('accessibility bar chart', () => {

function removeEmptyBins (sourceGroup) {
return {
all:function () {
return sourceGroup.all().filter( d => d.value !== 0);
}
};
}

it('internal elements are focusable by keyboard', () => {
chart.keyboardAccessible(true);
chart.render();
chart.selectAll('rect.bar').each(function () {
const bar = d3.select(this);
expect(bar.attr('tabindex')).toEqual('0');
});
});

it('initial internal elements are clickable by pressing enter', () => {
chart.keyboardAccessible(true);
const clickHandlerSpy = jasmine.createSpy();
chart.onClick = clickHandlerSpy;
chart.render();

const event = new Event('keydown');
event.keyCode = 13;

chart.selectAll('rect.bar').each(function (d) {
this.dispatchEvent(event);
expect(clickHandlerSpy).toHaveBeenCalledWith(d);
clickHandlerSpy.calls.reset();
});
});

it('newly added internal elements are also clickable from keyboard', () => {
chart.keyboardAccessible(true);
const clickHandlerSpy = jasmine.createSpy();
chart.onClick = clickHandlerSpy;

const event = new Event('keydown');
event.keyCode = 13;

const stateDimension = data.dimension(d => d.state);
const regionDimension = data.dimension(d => d.region);
const stateGroup = stateDimension.group().reduceSum(d => +d.value);
const ordinalDomainValues = ['California', 'Colorado', 'Delaware', 'Ontario', 'Mississippi', 'Oklahoma'];

chart
.dimension(stateDimension)
.group(removeEmptyBins(stateGroup))
.xUnits(dc.units.ordinal)
.x(d3.scaleBand().domain(ordinalDomainValues))
.elasticX(true)
.barPadding(0)
.outerPadding(0.1)
.transitionDuration(0)
.render();

//force creation of new <rect> elements on existing chart
regionDimension.filterExact('West');
chart.redraw();
regionDimension.filterAll();
chart.redraw();

chart.selectAll('rect.bar').each(function (d) {
this.dispatchEvent(event);
expect(clickHandlerSpy).toHaveBeenCalledWith(d);
clickHandlerSpy.calls.reset();
});

});

});

function nthStack (n) {
const stack = d3.select(chart.selectAll('.stack').nodes()[n]);

Expand Down
30 changes: 30 additions & 0 deletions spec/base-mixin-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,4 +677,34 @@ describe('dc.baseMixin', () => {
expect(chart.filters().length).toEqual(0);
});
});

describe('accessibility base svg', () => {

it('should have default description when keyboardAccessible is true', () => {
chart
.keyboardAccessible(true)
.resetSvg();

expect(chart.svg().attr('tabindex')).toEqual('0');
expect(chart.svg().node().firstChild.innerHTML).toEqual('BaseMixin');
});

it('should have custom description if svgDescription is set', () => {
chart
.svgDescription('I am a chart')
.resetSvg();

expect(chart.svg().attr('tabindex')).toEqual('0');
expect(chart.svg().node().firstChild.innerHTML).toEqual('I am a chart');
});

it('should not have accessibility features if not explicitly enabled', () => {
chart
.resetSvg();

expect(chart.svg().attr('tabindex')).toBeNull();
});


})
});
31 changes: 31 additions & 0 deletions spec/box-plot-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,37 @@ describe('dc.BoxPlot', () => {
});
});

describe('accessibility scatter plot', () => {

it('internal elements are focusable by keyboard', () => {
chart.keyboardAccessible(true);
chart.render();
chart.selectAll('circle').each(function () {
const circle = d3.select(this);
expect(circle.attr('tabindex')).toEqual('0');
});
});

it('internal elements are clickable by pressing enter', () => {

chart.keyboardAccessible(true);
const clickHandlerSpy = jasmine.createSpy();
chart.onClick = clickHandlerSpy;
chart.render();

const event = new Event('keydown');
event.keyCode = 13;

// only boxes are valid targets for keydown events
chart.selectAll('g.box').each(function (d) {
this.dispatchEvent(event);
expect(clickHandlerSpy).toHaveBeenCalledWith(d);
clickHandlerSpy.calls.reset();
});
});

});

function box (n) {
const nthBox = d3.select(chart.selectAll('g.box').nodes()[n]);
nthBox.boxText = function (i) {
Expand Down
38 changes: 38 additions & 0 deletions spec/bubble-chart-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -710,4 +710,42 @@ describe('dc.bubbleChart', () => {
});
});
});

describe('accessibility bubble chart', () => {

it('DOM order follows x values if keyboardAccessible is set', () => {
// default (alphabetical) sort order would put F ahead of T in DOM order
// keyboardAccessible should instead re-order DOM elements based on x-value
// T value is 198; F value is 220
chart.keyboardAccessible(true);
chart.render();
expect(document.querySelectorAll('.node text')[0].innerHTML).toEqual('T')
});

it('internal elements are focusable by keyboard', () => {
chart.keyboardAccessible(true);
chart.render();
chart.selectAll('circle').each(function () {
const bubble = d3.select(this);
expect(bubble.attr('tabindex')).toEqual('0');
});
});

it('internal elements are clickable by pressing enter', () => {
chart.keyboardAccessible(true);
const clickHandlerSpy = jasmine.createSpy();
chart.onClick = clickHandlerSpy;
chart.render();

const event = new Event('keydown');
event.keyCode = 13;

chart.selectAll('circle').each(function (d) {
this.dispatchEvent(event);
expect(clickHandlerSpy).toHaveBeenCalledWith(d);
clickHandlerSpy.calls.reset();
});
});

});
});
49 changes: 49 additions & 0 deletions spec/geo-choropleth-chart-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,53 @@ describe('dc.geoChoropleth', () => {
expect(chart.geoJsons().filter(e => e.name === 'state').length).toEqual(0);
});
});

describe('accessibility choropleth', () => {
// create chart without rendering it
let chart;
beforeEach(() => {
const id = 'accessible-choropleth-chart'
chart = new dc.GeoChoroplethChart(`#${id}`);
appendChartID(id);

chart.dimension(stateDimension)
.group(stateValueSumGroup)
.width(990)
.height(600)
.keyboardAccessible(true)
.colors(['#ccc', '#e2f2ff', '#c4e4ff', '#9ed2ff', '#81c5ff', '#6bbaff', '#51aeff', '#36a2ff', '#1e96ff', '#0089ff'])
.colorDomain([0, 155])
.overlayGeoJson(geoJson.features, 'state', d => d.properties.name)
.overlayGeoJson(geoJson2.features, 'county')
.transitionDuration(0)
.title(d => `${d.key} : ${d.value ? d.value : 0}`);
});

it('internal elements are focusable by keyboard', () => {

chart.render();
chart.selectAll('path.dc-tabbable').each(function () {
const state = d3.select(this);
expect(state.attr('tabindex')).toEqual('0');
});
});

it('internal elements are clickable by pressing enter', () => {

const clickHandlerSpy = jasmine.createSpy();
chart.onClick = clickHandlerSpy;
chart.render();

const event = new Event('keydown');
event.keyCode = 13;

chart.selectAll('path.dc-tabbable').each(function (d) {
this.dispatchEvent(event);
expect(clickHandlerSpy).toHaveBeenCalledWith(d);
clickHandlerSpy.calls.reset();
});
});

});

});
32 changes: 32 additions & 0 deletions spec/heatmap-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,4 +605,36 @@ describe('dc.heatmap', () => {
});
});
});

describe('accessibility heatmap', () => {

beforeEach(() => {
chart.keyboardAccessible(true);
})

it('internal elements are focusable by keyboard', () => {

chart.render();
chart.selectAll('rect.heat-box').each(function () {
const bar = d3.select(this);
expect(bar.attr('tabindex')).toEqual('0');
});
});

it('internal elements are clickable by pressing enter', () => {

const clickHandlerSpy = jasmine.createSpy();
chart.boxOnClick = clickHandlerSpy;
chart.render();

const event = new Event('keydown');
event.keyCode = 13;

chart.selectAll('rect.heat-box').each(function (d) {
this.dispatchEvent(event);
expect(clickHandlerSpy).toHaveBeenCalledWith(d);
clickHandlerSpy.calls.reset();
});
});
});
});
25 changes: 25 additions & 0 deletions spec/legend-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,31 @@ describe('dc.legend', () => {
});
});

describe('accessible legends', () => {
beforeEach(() => {
chart.legend(new dc.Legend().keyboardAccessible(true)).render();
});

it('legend items should be focusable from keyboard', () => {

chart.select('g.dc-legend').selectAll('g.dc-legend-item text').each(function () {
const item = d3.select(this);
expect(item.attr('tabindex')).toEqual('0');
});

});

it('keyboard focus on legend item should highlight chart item', () => {

chart
.select('g.dc-legend').select('g.dc-legend-item text')
.on('focus').call(legendItem(0).nodes()[0], legendItem(0).datum());

expect(d3.select(chart.selectAll('path.line').nodes()[0]).classed('highlight')).toBeTruthy();
});

});

function legendItem (n) {
return d3.select(chart.selectAll('g.dc-legend g.dc-legend-item').nodes()[n]);
}
Expand Down
16 changes: 16 additions & 0 deletions spec/line-chart-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,22 @@ describe('dc.lineChart', () => {
});
});

describe('accessibility bar chart', () => {

beforeEach(() => {
chart.keyboardAccessible(true);
chart.brushOn(false);
})

it('internal elements are focusable by keyboard', () => {
chart.render();
chart.selectAll('circle.dot').each(function () {
const dot = d3.select(this);
expect(dot.attr('tabindex')).toEqual('0');
});
});
});

function lineLabelCount () {
expect(chart.selectAll('text.lineLabel').size()).toBe(chart.stack().length * chart.group().all().length);
}
Expand Down
35 changes: 35 additions & 0 deletions spec/number-display-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,39 @@ describe('dc.numberDisplay', () => {
});
});
});

describe('accessibility number display' , () => {
let chart;
beforeEach(() => {

const id = 'empty-div';
appendChartID(id);

chart = new dc.NumberDisplay(`#${id}`)
.transitionDuration(0)
.group(meanGroup)
.formatNumber(d3.format('.3s'))
.valueAccessor(average);
});

it('should have aria-live', () => {
chart.ariaLiveRegion(true);
chart.render();
d3.timerFlush();

expect(chart.select('span.number-display').attr('aria-live')).toEqual('polite');

});

it('should be focusable', () => {
chart.keyboardAccessible(true);
chart.render();
d3.timerFlush();

expect(chart.select('span.number-display').attr('tabindex')).toEqual('0');

});

});

});
Loading