mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
feat: support screen reader in gridless sheet (#1756)
* feat: support screen reader on sheet * feat: support screen reader * fix: add hidden screen reader * fix: failed test case * fix: change to currentId * fix: remove unrelated change * fix: change to cell id * fix: comments * fix: add no title * fix: failed test cases * refactor: comments * fix: remove ?
This commit is contained in:
@@ -234,5 +234,145 @@
|
||||
"Visualization.Invalid.Measure": {
|
||||
"value": "Invalid measure",
|
||||
"comment": "Status text shown when a measure is not valid"
|
||||
},
|
||||
"Object.AutoChart": {
|
||||
"value": "Autochart",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.BarChart": {
|
||||
"value": "Bar chart",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.ComboChart": {
|
||||
"value": "Combo chart",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Container": {
|
||||
"value": "Container",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.DistributionPlot": {
|
||||
"value": "Distribution plot",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.BoxPlot": {
|
||||
"value": "Box plot",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.FilterPane": {
|
||||
"value": "Filter pane",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Gauge": {
|
||||
"value": "Gauge",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Histogram": {
|
||||
"value": "Histogram",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Kpi": {
|
||||
"value": "KPI",
|
||||
"comment": "Do not use more than a total of 19 characters when you translate this string."
|
||||
},
|
||||
"Object.LineChart": {
|
||||
"value": "Line chart",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Listbox": {
|
||||
"value": "List box",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.PieChart": {
|
||||
"value": "Pie chart",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.PivotTable": {
|
||||
"value": "Pivot table",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Map": {
|
||||
"value": "Map",
|
||||
"comment": "Visualization for geographical data"
|
||||
},
|
||||
"Object.ScatterPlot": {
|
||||
"value": "Scatter plot",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.StraightTable": {
|
||||
"value": "Straight table",
|
||||
"comment": "Visualization in the library."
|
||||
},
|
||||
"Object.TextImage": {
|
||||
"value": "Text & image",
|
||||
"comment": "Visualization in the library. Renamed from Utility object to Text & image"
|
||||
},
|
||||
"Object.Treemap": {
|
||||
"value": "Treemap",
|
||||
"comment": ""
|
||||
},
|
||||
"Object.WaterfallChart":{
|
||||
"value": "Waterfall chart",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.MekkoChart": {
|
||||
"value": "Mekko chart",
|
||||
"comment": "Visualization for?"
|
||||
},
|
||||
"Object.ActionButton": {
|
||||
"value": "Button",
|
||||
"comment": "Button for selections and navigation"
|
||||
},
|
||||
"Object.NavMenu": {
|
||||
"value": "Navigation menu",
|
||||
"comment": "A navigation menu for navigating sheets."
|
||||
},
|
||||
"Object.BulletChart": {
|
||||
"value": "Bullet chart",
|
||||
"comment": "Visualization for values using bars and ranges"
|
||||
},
|
||||
"Object.NlgChart": {
|
||||
"value": "NL insights",
|
||||
"comment": "Visualization in the library."
|
||||
},
|
||||
"Object.TabContainer": {
|
||||
"value": "Tab container",
|
||||
"comment": "Visualization in the library."
|
||||
},
|
||||
"Object.Table": {
|
||||
"value": "Table",
|
||||
"comment": "Visualization in the library"
|
||||
},
|
||||
"Object.Table.Deprecated": {
|
||||
"value": "Table ",
|
||||
"comment": "Visualization in the library that will be deprecated."
|
||||
},
|
||||
"Object.Text": {
|
||||
"value": "Text",
|
||||
"comment": "A chart used to display text, measures, add tables with rows and columns, etc."
|
||||
},
|
||||
"Object.LayoutContainer": {
|
||||
"value": "Layout container",
|
||||
"comment": "Visualization in the library."
|
||||
},
|
||||
"Object.GridChart": {
|
||||
"value": "Grid chart",
|
||||
"comment": "A chart used to present data in a matrix."
|
||||
},
|
||||
"Object.FunnelChart": {
|
||||
"value": "Funnel chart",
|
||||
"comment": "A chart often used to represent stages of a process."
|
||||
},
|
||||
"Object.SankeyChart": {
|
||||
"value": "Sankey chart",
|
||||
"comment": "A chart that shows the flow from a set of values to another."
|
||||
},
|
||||
"Object.RadarChart": {
|
||||
"value": "Radar chart",
|
||||
"comment": "A chart that displays multivariate data stacked at an axis with the same central point."
|
||||
},
|
||||
"Accessibility.Object.NoTitle": {
|
||||
"value": "No title",
|
||||
"comment": "Screen reader text for objects without a title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import eventmixin from '../selections/event-mixin';
|
||||
import useStyling from '../hooks/useStyling';
|
||||
import RenderError from '../utils/render-error';
|
||||
import getPadding from '../utils/cell-padding';
|
||||
import translationKeys from '../utils/extension-translation-keys';
|
||||
import hiddenScreenReaderText from '../utils/style/screen-reader';
|
||||
|
||||
/**
|
||||
* @interface
|
||||
@@ -583,6 +585,7 @@ const Cell = forwardRef(
|
||||
snPlugins={snPlugins}
|
||||
layout={layout}
|
||||
appLayout={appLayout}
|
||||
cellId={currentId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -607,7 +610,8 @@ const Cell = forwardRef(
|
||||
translator,
|
||||
});
|
||||
}
|
||||
|
||||
const translationKey = translationKeys.get(layout?.visualization);
|
||||
const translation = translator.get(translationKey);
|
||||
return (
|
||||
<Paper
|
||||
style={{
|
||||
@@ -644,7 +648,15 @@ const Cell = forwardRef(
|
||||
...(useOldCellPadding ? { padding: theme.spacing(1) } : {}),
|
||||
...(state.longRunningQuery ? { opacity: '0.3' } : {}),
|
||||
}}
|
||||
aria-labelledby={`${currentId}_title ${currentId}_type ${currentId}_content`}
|
||||
>
|
||||
{layout && (
|
||||
<div
|
||||
id={`${currentId}_type`}
|
||||
style={hiddenScreenReaderText}
|
||||
aria-label={translation ?? layout.visualization}
|
||||
/>
|
||||
)}
|
||||
{cellNode && layout && state.sn && (
|
||||
<Header
|
||||
layout={layout}
|
||||
@@ -654,6 +666,8 @@ const Cell = forwardRef(
|
||||
focusHandler={focusHandler.current}
|
||||
titleStyles={titleStyles}
|
||||
isRtl={isRtl}
|
||||
id={currentId}
|
||||
translator={translator}
|
||||
>
|
||||
|
||||
</Header>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { styled } from '@mui/material/styles';
|
||||
|
||||
import { Grid, Tooltip, Typography } from '@mui/material';
|
||||
import ActionsToolbar from './ActionsToolbar';
|
||||
import hiddenScreenReaderText from '../utils/style/screen-reader';
|
||||
|
||||
const PREFIX = 'Header';
|
||||
|
||||
@@ -42,7 +43,7 @@ const CellSubTitle = {
|
||||
className: 'njs-cell-sub-title',
|
||||
};
|
||||
|
||||
function Header({ layout, sn, anchorEl, hovering, focusHandler, titleStyles = {}, isRtl }) {
|
||||
function Header({ id, layout, sn, anchorEl, hovering, focusHandler, titleStyles = {}, isRtl, translator }) {
|
||||
const showTitle = layout.showTitles && !!layout.title;
|
||||
const showSubtitle = layout.showTitles && !!layout.subtitle;
|
||||
const showInSelectionActions = layout.qSelectionInfo && layout.qSelectionInfo.qInSelections;
|
||||
@@ -76,17 +77,28 @@ function Header({ layout, sn, anchorEl, hovering, focusHandler, titleStyles = {}
|
||||
isRtl={isRtl}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledGrid item container wrap="nowrap" className={cls.join(' ')}>
|
||||
<Grid item zeroMinWidth xs dir={isRtl ? 'rtl' : 'ltr'}>
|
||||
<Grid container wrap="nowrap" direction="column">
|
||||
{showTitle && (
|
||||
{showTitle ? (
|
||||
<Tooltip title={layout.title}>
|
||||
<Typography variant="h6" noWrap className={CellTitle.className} style={titleStyles.main}>
|
||||
<Typography
|
||||
id={`${id}_title`}
|
||||
variant="h6"
|
||||
noWrap
|
||||
className={CellTitle.className}
|
||||
style={titleStyles.main}
|
||||
>
|
||||
{layout.title}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div
|
||||
id={`${id}_title`}
|
||||
style={hiddenScreenReaderText}
|
||||
aria-label={translator.get('Accessibility.Object.NoTitle')}
|
||||
/>
|
||||
)}
|
||||
{showSubtitle && (
|
||||
<Tooltip title={layout.subtitle}>
|
||||
|
||||
@@ -19,7 +19,7 @@ const VizElement = {
|
||||
className: 'njs-viz',
|
||||
};
|
||||
|
||||
function Supernova({ sn, snOptions: options, snPlugins: plugins, layout, appLayout, halo }) {
|
||||
function Supernova({ sn, snOptions: options, snPlugins: plugins, layout, appLayout, halo, cellId }) {
|
||||
const { component } = sn;
|
||||
|
||||
const { theme: themeName, language, constraints, interactions, keyboardNavigation } = useContext(InstanceContext);
|
||||
@@ -135,7 +135,7 @@ function Supernova({ sn, snOptions: options, snPlugins: plugins, layout, appLayo
|
||||
}}
|
||||
className={VizElement.className}
|
||||
>
|
||||
<div ref={snRef} style={{ position: 'absolute', width: '100%', height: '100%' }} />
|
||||
<div ref={snRef} id={`${cellId}_content`} style={{ position: 'absolute', width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('<Cell />', () => {
|
||||
Header = jest.fn().mockImplementation(() => 'Header');
|
||||
InstanceContext = React.createContext();
|
||||
appLayout = { foo: 'app-layout' };
|
||||
layout = { qSelectionInfo: {}, visualization: '' };
|
||||
layout = { qSelectionInfo: {}, visualization: '', qInfo: { qId: 'id' } };
|
||||
layoutState = { validating: true, canCancel: false, canRetry: false };
|
||||
longrunning = { cancel: jest.fn(), retry: jest.fn() };
|
||||
useLayout = jest.fn().mockReturnValue([layout, layoutState, longrunning]);
|
||||
|
||||
@@ -23,9 +23,14 @@ describe('<Header />', () => {
|
||||
jest.spyOn(useRectModule, 'default').mockImplementation(() => [() => {}, rect]);
|
||||
ActionsToolbarModule.default = ActionsToolbar;
|
||||
|
||||
render = async (layout = {}, sn = { component: {}, selectionToolbar: {} }, focusHandler = {}) => {
|
||||
render = async (
|
||||
layout = {},
|
||||
sn = { component: {}, selectionToolbar: {} },
|
||||
focusHandler = {},
|
||||
translator = { get: (s) => s }
|
||||
) => {
|
||||
await act(async () => {
|
||||
renderer = create(<Header layout={layout} sn={sn} focusHandler={focusHandler} />);
|
||||
renderer = create(<Header layout={layout} sn={sn} focusHandler={focusHandler} translator={translator} />);
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -40,7 +45,7 @@ describe('<Header />', () => {
|
||||
expect(types).toHaveLength(0);
|
||||
});
|
||||
test('should render title', async () => {
|
||||
await render({ showTitles: true, title: 'foo' });
|
||||
await render({ showTitles: true, title: 'foo', qInfo: { qId: 'id' } });
|
||||
const types = renderer.root.findAllByType(Typography);
|
||||
expect(types).toHaveLength(1);
|
||||
expect(types[0].props.children).toBe('foo');
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('<Supernova />', () => {
|
||||
sn = { component: {} },
|
||||
snOptions = {},
|
||||
snPlugins = [],
|
||||
layout = {},
|
||||
layout = { qInfo: { qId: 'id' } },
|
||||
appLayout = {},
|
||||
halo = {},
|
||||
rendererOptions,
|
||||
@@ -30,6 +30,7 @@ describe('<Supernova />', () => {
|
||||
layout={layout}
|
||||
appLayout={appLayout}
|
||||
halo={halo}
|
||||
cellId="id"
|
||||
/>,
|
||||
rendererOptions || null
|
||||
);
|
||||
@@ -47,9 +48,10 @@ describe('<Supernova />', () => {
|
||||
},
|
||||
snOptions: {},
|
||||
snPlugins: [],
|
||||
layout: {},
|
||||
layout: { qInfo: { qId: 'id' } },
|
||||
appLayout: {},
|
||||
halo: {},
|
||||
cellId: 'id',
|
||||
});
|
||||
});
|
||||
test('should mount', async () => {
|
||||
@@ -65,6 +67,7 @@ describe('<Supernova />', () => {
|
||||
logicalSize,
|
||||
component,
|
||||
},
|
||||
layout: { qInfo: { qId: 'id' } },
|
||||
rendererOptions: {
|
||||
createNodeMock: () => ({
|
||||
style: {},
|
||||
@@ -100,7 +103,7 @@ describe('<Supernova />', () => {
|
||||
},
|
||||
snOptions,
|
||||
snPlugins: [],
|
||||
layout: 'layout',
|
||||
layout: { qInfo: { qId: 'id' } },
|
||||
appLayout: { qLocaleInfo: 'loc' },
|
||||
halo: { public: { theme: 'theme', nebbie: 'embedAPI' }, app: { session: {} } },
|
||||
rendererOptions: {
|
||||
@@ -115,7 +118,7 @@ describe('<Supernova />', () => {
|
||||
expect(await initialRender).toBe(true);
|
||||
expect(component.render).toHaveBeenCalledTimes(1);
|
||||
expect(component.render.mock.calls[0][0]).toEqual({
|
||||
layout: 'layout',
|
||||
layout: { qInfo: { qId: 'id' } },
|
||||
options: snOptions,
|
||||
plugins: [],
|
||||
embed: 'embedAPI',
|
||||
|
||||
@@ -39,7 +39,10 @@ const model = {
|
||||
function getDefaultProps() {
|
||||
const containerRef = React.createRef();
|
||||
const defaultProps = {
|
||||
layout: { title: 'The title', qListObject: { qDimensionInfo: { qLocked: false, qGrouping: 'N' } } },
|
||||
layout: {
|
||||
title: 'The title',
|
||||
qListObject: { qDimensionInfo: { qLocked: false, qGrouping: 'N' } },
|
||||
},
|
||||
translator,
|
||||
styles,
|
||||
isRtl: false,
|
||||
|
||||
40
apis/nucleus/src/utils/extension-translation-keys.js
Normal file
40
apis/nucleus/src/utils/extension-translation-keys.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const translationKeys = new Map();
|
||||
const extensions = [
|
||||
['auto-chart', 'Object.AutoChart'],
|
||||
['dummy-chart', 'Dummy'],
|
||||
['barchart', 'Object.BarChart'],
|
||||
['combochart', 'Object.ComboChart'],
|
||||
['container', 'Object.Container'],
|
||||
['distributionplot', 'Object.DistributionPlot'],
|
||||
['boxplot', 'Object.BoxPlot'],
|
||||
['filterpane', 'Object.FilterPane'],
|
||||
['gauge', 'Object.Gauge'],
|
||||
['histogram', 'Object.Histogram'],
|
||||
['kpi', 'Object.Kpi'],
|
||||
['linechart', 'Object.LineChart'],
|
||||
['listbox', 'Object.Listbox'],
|
||||
['piechart', 'Object.PieChart'],
|
||||
['pivot-table', 'Object.PivotTable'],
|
||||
['map', 'Object.Map'],
|
||||
['scatterplot', 'Object.ScatterPlot'],
|
||||
['sn-table', 'Object.StraightTable'],
|
||||
['text-image', 'Object.TextImage'],
|
||||
['treemap', 'Object.Treemap'],
|
||||
['waterfallchart', 'Object.WaterfallChart'],
|
||||
['mekkochart', 'Object.MekkoChart'],
|
||||
['action-button', 'Object.ActionButton'],
|
||||
['sn-nav-menu', 'Object.NavMenu'],
|
||||
['bulletchart', 'Object.BulletChart'],
|
||||
['sn-nlg-chart', 'Object.NlgChart'],
|
||||
['sn-analysis-autochart', 'Common.AnalysisTypes'],
|
||||
['sn-tabbed-container', 'Object.TabContainer'],
|
||||
['qlik-sankey-chart-ext', 'Object.SankeyChart'],
|
||||
['qlik-radar-chart', 'Object.RadarChart'],
|
||||
['qlik-funnel-chart-ext', 'Object.FunnelChart'],
|
||||
['sn-grid-chart', 'Object.GridChart'],
|
||||
['sn-layout-container', 'Object.LayoutContainer'],
|
||||
];
|
||||
extensions.forEach(([key, value]) => {
|
||||
translationKeys.set(key, value);
|
||||
});
|
||||
export default translationKeys;
|
||||
5
apis/nucleus/src/utils/style/screen-reader.js
Normal file
5
apis/nucleus/src/utils/style/screen-reader.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const hiddenScreenReaderText = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
export default hiddenScreenReaderText;
|
||||
Reference in New Issue
Block a user