|
1 | 1 | import { getRGBColor } from '@ui5/webcomponents-base/dist/util/ColorConversion.js'; |
2 | 2 | import type { ComponentType } from 'react'; |
| 3 | +import { useState } from 'react'; |
3 | 4 |
|
4 | 5 | export function cypressPassThroughTestsFactory(Component: ComponentType, props?: Record<string, unknown>) { |
5 | 6 | it('Pass Through HTML Standard Props', () => { |
@@ -101,6 +102,202 @@ export function testChartLegendConfig(Component, props) { |
101 | 102 | }); |
102 | 103 | } |
103 | 104 |
|
| 105 | +export function testPieSectorFocus(Component, props, { only }: { only?: boolean } = {}) { |
| 106 | + const chartConfig = { accessibilityLayer: true }; |
| 107 | + const containerSelector = '[aria-roledescription="chart"]'; |
| 108 | + const test = only ? it.only : it; |
| 109 | + |
| 110 | + test('sector focus - keyboard navigation: Tab, arrows, Enter', () => { |
| 111 | + const onDataPointClick = cy.spy().as('onDataPointClick'); |
| 112 | + cy.mount( |
| 113 | + <> |
| 114 | + <button>before</button> |
| 115 | + <Component {...props} noAnimation chartConfig={chartConfig} onDataPointClick={onDataPointClick} /> |
| 116 | + <button>after</button> |
| 117 | + </>, |
| 118 | + ); |
| 119 | + |
| 120 | + cy.findByText('before').focus(); |
| 121 | + cy.realPress('Tab'); |
| 122 | + cy.focused() |
| 123 | + .should('have.attr', 'tabindex', '0') |
| 124 | + .should('have.attr', 'role', 'application') |
| 125 | + .should('have.attr', 'aria-roledescription', 'chart'); |
| 126 | + |
| 127 | + cy.realPress('Tab'); |
| 128 | + cy.focused() |
| 129 | + .should('have.attr', 'data-sector-index', '0') |
| 130 | + .and('have.attr', 'role', 'img') |
| 131 | + .and('have.attr', 'aria-label'); |
| 132 | + |
| 133 | + cy.realPress('ArrowRight'); |
| 134 | + cy.focused().should('have.attr', 'data-sector-index', '1'); |
| 135 | + cy.realPress('ArrowLeft'); |
| 136 | + cy.focused().should('have.attr', 'data-sector-index', '0'); |
| 137 | + |
| 138 | + // Wraps from first to last |
| 139 | + cy.realPress('ArrowLeft'); |
| 140 | + cy.focused().should('have.attr', 'data-sector-index', String(props.dataset.length - 1)); |
| 141 | + |
| 142 | + cy.realPress('Enter'); |
| 143 | + cy.get('@onDataPointClick').should( |
| 144 | + 'have.been.calledWith', |
| 145 | + Cypress.sinon.match({ |
| 146 | + detail: Cypress.sinon.match({ |
| 147 | + dataIndex: props.dataset.length - 1, |
| 148 | + }), |
| 149 | + }), |
| 150 | + ); |
| 151 | + |
| 152 | + cy.realPress(['Shift', 'Tab']); |
| 153 | + cy.focused().should('have.attr', 'aria-roledescription', 'chart').and('have.attr', 'tabindex', '0'); |
| 154 | + }); |
| 155 | + |
| 156 | + test('sector focus - activeSegment with Enter and Space', () => { |
| 157 | + const onDataPointClick = cy.spy().as('onDataPointClick'); |
| 158 | + const StatefulChart = () => { |
| 159 | + const [activeSegment, setActiveSegment] = useState(3); |
| 160 | + return ( |
| 161 | + <> |
| 162 | + <button>before</button> |
| 163 | + <Component |
| 164 | + {...props} |
| 165 | + noAnimation |
| 166 | + chartConfig={{ ...chartConfig, activeSegment }} |
| 167 | + onDataPointClick={(e) => { |
| 168 | + onDataPointClick(e); |
| 169 | + setActiveSegment(e.detail.dataIndex); |
| 170 | + }} |
| 171 | + /> |
| 172 | + </> |
| 173 | + ); |
| 174 | + }; |
| 175 | + cy.mount(<StatefulChart />); |
| 176 | + cy.findByText('before').focus(); |
| 177 | + cy.realPress('Tab'); |
| 178 | + |
| 179 | + // Tab focuses the activeSegment |
| 180 | + cy.realPress('Tab'); |
| 181 | + cy.focused().should('have.attr', 'data-sector-index', '3'); |
| 182 | + |
| 183 | + cy.realPress('ArrowRight'); |
| 184 | + cy.focused().should('have.attr', 'data-sector-index', '4'); |
| 185 | + cy.realPress('Enter'); |
| 186 | + cy.get('@onDataPointClick').should( |
| 187 | + 'have.been.calledWith', |
| 188 | + Cypress.sinon.match({ |
| 189 | + detail: Cypress.sinon.match({ |
| 190 | + dataIndex: 4, |
| 191 | + }), |
| 192 | + }), |
| 193 | + ); |
| 194 | + cy.get('.recharts-active-shape').should('exist'); |
| 195 | + cy.focused().should('have.attr', 'data-sector-index', '4'); |
| 196 | + |
| 197 | + cy.realPress('ArrowRight'); |
| 198 | + cy.focused().should('have.attr', 'data-sector-index', '5'); |
| 199 | + cy.realPress('Space'); |
| 200 | + cy.get('@onDataPointClick').should( |
| 201 | + 'have.been.calledWith', |
| 202 | + Cypress.sinon.match({ |
| 203 | + detail: Cypress.sinon.match({ |
| 204 | + dataIndex: 5, |
| 205 | + }), |
| 206 | + }), |
| 207 | + ); |
| 208 | + cy.focused().should('have.attr', 'data-sector-index', '5'); |
| 209 | + }); |
| 210 | + |
| 211 | + test('sector focus - activeSegment out of bounds is clamped', () => { |
| 212 | + cy.mount( |
| 213 | + <> |
| 214 | + <button>before</button> |
| 215 | + <Component {...props} noAnimation chartConfig={{ ...chartConfig, activeSegment: 999 }} /> |
| 216 | + </>, |
| 217 | + ); |
| 218 | + cy.findByText('before').focus(); |
| 219 | + cy.realPress('Tab'); |
| 220 | + cy.realPress('Tab'); |
| 221 | + cy.focused().should('have.attr', 'data-sector-index', String(props.dataset.length - 1)); |
| 222 | + }); |
| 223 | + |
| 224 | + test('sector focus - empty dataset is non-interactive', () => { |
| 225 | + cy.mount(<Component {...props} dataset={[]} noAnimation chartConfig={chartConfig} />); |
| 226 | + cy.get(containerSelector) |
| 227 | + .should('have.attr', 'tabindex', '0') |
| 228 | + .should('have.attr', 'aria-roledescription', 'chart') |
| 229 | + .should('not.have.attr', 'role', 'application'); |
| 230 | + }); |
| 231 | + |
| 232 | + test('sector focus - dataset shrink resets keyboard state', () => { |
| 233 | + const initialDataset = props.dataset; |
| 234 | + const smallDataset = initialDataset.slice(0, 3); |
| 235 | + const baseProps = { ...props, noAnimation: true, chartConfig }; |
| 236 | + const StatefulChart = () => { |
| 237 | + const [ds, setDs] = useState(initialDataset); |
| 238 | + return ( |
| 239 | + <> |
| 240 | + <button>before</button> |
| 241 | + <button onClick={() => setDs(smallDataset)}>shrink</button> |
| 242 | + <Component {...baseProps} dataset={ds} /> |
| 243 | + </> |
| 244 | + ); |
| 245 | + }; |
| 246 | + cy.mount(<StatefulChart />); |
| 247 | + cy.findByText('before').focus(); |
| 248 | + cy.realPress('Tab'); |
| 249 | + cy.realPress('Tab'); |
| 250 | + cy.realPress('Tab'); |
| 251 | + |
| 252 | + for (let i = 0; i < 5; i++) { |
| 253 | + cy.realPress('ArrowRight'); |
| 254 | + } |
| 255 | + cy.focused().should('have.attr', 'data-sector-index', '5'); |
| 256 | + |
| 257 | + cy.findByText('shrink').click(); |
| 258 | + cy.get(containerSelector).should('have.attr', 'tabindex', '0'); |
| 259 | + |
| 260 | + cy.findByText('before').focus(); |
| 261 | + cy.realPress('Tab'); |
| 262 | + cy.realPress('Tab'); |
| 263 | + cy.realPress('Tab'); |
| 264 | + cy.focused().should('have.attr', 'data-sector-index'); |
| 265 | + }); |
| 266 | + |
| 267 | + test('sector focus - consumer event handlers are composed with internal handlers', () => { |
| 268 | + const onBlur = cy.spy().as('onBlur'); |
| 269 | + const onFocus = cy.spy().as('onFocus'); |
| 270 | + const onKeyDownCapture = cy.spy().as('onKeyDownCapture'); |
| 271 | + cy.mount( |
| 272 | + <> |
| 273 | + <button>before</button> |
| 274 | + <Component |
| 275 | + {...props} |
| 276 | + noAnimation |
| 277 | + chartConfig={chartConfig} |
| 278 | + onBlur={onBlur} |
| 279 | + onFocus={onFocus} |
| 280 | + onKeyDownCapture={onKeyDownCapture} |
| 281 | + /> |
| 282 | + <button>after</button> |
| 283 | + </>, |
| 284 | + ); |
| 285 | + |
| 286 | + cy.findByText('before').focus(); |
| 287 | + cy.realPress('Tab'); |
| 288 | + cy.get('@onFocus').should('have.been.calledOnce'); |
| 289 | + |
| 290 | + cy.realPress('Tab'); |
| 291 | + cy.get('@onKeyDownCapture').should('have.been.called'); |
| 292 | + cy.focused().should('have.attr', 'data-sector-index', '0'); |
| 293 | + |
| 294 | + cy.findByText('after').click(); |
| 295 | + cy.get('@onBlur').should('have.been.called'); |
| 296 | + // raf defers exitSectorMode, so wait for tabindex to flip back |
| 297 | + cy.get(containerSelector).should('have.attr', 'tabindex', '0'); |
| 298 | + }); |
| 299 | +} |
| 300 | + |
104 | 301 | export function testStackAggregateTotals(Component, props) { |
105 | 302 | it('showStackAggregateTotals', () => { |
106 | 303 | const { dataset, measures } = props; |
|
0 commit comments