accessibility.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /*!
  2. * @pixi/accessibility - v6.5.10
  3. * Compiled Thu, 06 Jul 2023 15:25:11 UTC
  4. *
  5. * @pixi/accessibility is licensed under the MIT License.
  6. * http://www.opensource.org/licenses/mit-license
  7. */
  8. 'use strict';
  9. Object.defineProperty(exports, '__esModule', { value: true });
  10. var display = require('@pixi/display');
  11. var utils = require('@pixi/utils');
  12. var core = require('@pixi/core');
  13. /**
  14. * Default property values of accessible objects
  15. * used by {@link PIXI.AccessibilityManager}.
  16. * @private
  17. * @function accessibleTarget
  18. * @memberof PIXI
  19. * @type {object}
  20. * @example
  21. * function MyObject() {}
  22. *
  23. * Object.assign(
  24. * MyObject.prototype,
  25. * PIXI.accessibleTarget
  26. * );
  27. */
  28. var accessibleTarget = {
  29. /**
  30. * Flag for if the object is accessible. If true AccessibilityManager will overlay a
  31. * shadow div with attributes set
  32. * @member {boolean}
  33. * @memberof PIXI.DisplayObject#
  34. */
  35. accessible: false,
  36. /**
  37. * Sets the title attribute of the shadow div
  38. * If accessibleTitle AND accessibleHint has not been this will default to 'displayObject [tabIndex]'
  39. * @member {?string}
  40. * @memberof PIXI.DisplayObject#
  41. */
  42. accessibleTitle: null,
  43. /**
  44. * Sets the aria-label attribute of the shadow div
  45. * @member {string}
  46. * @memberof PIXI.DisplayObject#
  47. */
  48. accessibleHint: null,
  49. /**
  50. * @member {number}
  51. * @memberof PIXI.DisplayObject#
  52. * @private
  53. * @todo Needs docs.
  54. */
  55. tabIndex: 0,
  56. /**
  57. * @member {boolean}
  58. * @memberof PIXI.DisplayObject#
  59. * @todo Needs docs.
  60. */
  61. _accessibleActive: false,
  62. /**
  63. * @member {boolean}
  64. * @memberof PIXI.DisplayObject#
  65. * @todo Needs docs.
  66. */
  67. _accessibleDiv: null,
  68. /**
  69. * Specify the type of div the accessible layer is. Screen readers treat the element differently
  70. * depending on this type. Defaults to button.
  71. * @member {string}
  72. * @memberof PIXI.DisplayObject#
  73. * @default 'button'
  74. */
  75. accessibleType: 'button',
  76. /**
  77. * Specify the pointer-events the accessible div will use
  78. * Defaults to auto.
  79. * @member {string}
  80. * @memberof PIXI.DisplayObject#
  81. * @default 'auto'
  82. */
  83. accessiblePointerEvents: 'auto',
  84. /**
  85. * Setting to false will prevent any children inside this container to
  86. * be accessible. Defaults to true.
  87. * @member {boolean}
  88. * @memberof PIXI.DisplayObject#
  89. * @default true
  90. */
  91. accessibleChildren: true,
  92. renderId: -1,
  93. };
  94. // add some extra variables to the container..
  95. display.DisplayObject.mixin(accessibleTarget);
  96. var KEY_CODE_TAB = 9;
  97. var DIV_TOUCH_SIZE = 100;
  98. var DIV_TOUCH_POS_X = 0;
  99. var DIV_TOUCH_POS_Y = 0;
  100. var DIV_TOUCH_ZINDEX = 2;
  101. var DIV_HOOK_SIZE = 1;
  102. var DIV_HOOK_POS_X = -1000;
  103. var DIV_HOOK_POS_Y = -1000;
  104. var DIV_HOOK_ZINDEX = 2;
  105. /**
  106. * The Accessibility manager recreates the ability to tab and have content read by screen readers.
  107. * This is very important as it can possibly help people with disabilities access PixiJS content.
  108. *
  109. * A DisplayObject can be made accessible just like it can be made interactive. This manager will map the
  110. * events as if the mouse was being used, minimizing the effort required to implement.
  111. *
  112. * An instance of this class is automatically created by default, and can be found at `renderer.plugins.accessibility`
  113. * @class
  114. * @memberof PIXI
  115. */
  116. var AccessibilityManager = /** @class */ (function () {
  117. /**
  118. * @param {PIXI.CanvasRenderer|PIXI.Renderer} renderer - A reference to the current renderer
  119. */
  120. function AccessibilityManager(renderer) {
  121. /** Setting this to true will visually show the divs. */
  122. this.debug = false;
  123. /** Internal variable, see isActive getter. */
  124. this._isActive = false;
  125. /** Internal variable, see isMobileAccessibility getter. */
  126. this._isMobileAccessibility = false;
  127. /** A simple pool for storing divs. */
  128. this.pool = [];
  129. /** This is a tick used to check if an object is no longer being rendered. */
  130. this.renderId = 0;
  131. /** The array of currently active accessible items. */
  132. this.children = [];
  133. /** Count to throttle div updates on android devices. */
  134. this.androidUpdateCount = 0;
  135. /** The frequency to update the div elements. */
  136. this.androidUpdateFrequency = 500; // 2fps
  137. this._hookDiv = null;
  138. if (utils.isMobile.tablet || utils.isMobile.phone) {
  139. this.createTouchHook();
  140. }
  141. // first we create a div that will sit over the PixiJS element. This is where the div overlays will go.
  142. var div = document.createElement('div');
  143. div.style.width = DIV_TOUCH_SIZE + "px";
  144. div.style.height = DIV_TOUCH_SIZE + "px";
  145. div.style.position = 'absolute';
  146. div.style.top = DIV_TOUCH_POS_X + "px";
  147. div.style.left = DIV_TOUCH_POS_Y + "px";
  148. div.style.zIndex = DIV_TOUCH_ZINDEX.toString();
  149. this.div = div;
  150. this.renderer = renderer;
  151. /**
  152. * pre-bind the functions
  153. * @type {Function}
  154. * @private
  155. */
  156. this._onKeyDown = this._onKeyDown.bind(this);
  157. /**
  158. * pre-bind the functions
  159. * @type {Function}
  160. * @private
  161. */
  162. this._onMouseMove = this._onMouseMove.bind(this);
  163. // let listen for tab.. once pressed we can fire up and show the accessibility layer
  164. globalThis.addEventListener('keydown', this._onKeyDown, false);
  165. }
  166. Object.defineProperty(AccessibilityManager.prototype, "isActive", {
  167. /**
  168. * Value of `true` if accessibility is currently active and accessibility layers are showing.
  169. * @member {boolean}
  170. * @readonly
  171. */
  172. get: function () {
  173. return this._isActive;
  174. },
  175. enumerable: false,
  176. configurable: true
  177. });
  178. Object.defineProperty(AccessibilityManager.prototype, "isMobileAccessibility", {
  179. /**
  180. * Value of `true` if accessibility is enabled for touch devices.
  181. * @member {boolean}
  182. * @readonly
  183. */
  184. get: function () {
  185. return this._isMobileAccessibility;
  186. },
  187. enumerable: false,
  188. configurable: true
  189. });
  190. /**
  191. * Creates the touch hooks.
  192. * @private
  193. */
  194. AccessibilityManager.prototype.createTouchHook = function () {
  195. var _this = this;
  196. var hookDiv = document.createElement('button');
  197. hookDiv.style.width = DIV_HOOK_SIZE + "px";
  198. hookDiv.style.height = DIV_HOOK_SIZE + "px";
  199. hookDiv.style.position = 'absolute';
  200. hookDiv.style.top = DIV_HOOK_POS_X + "px";
  201. hookDiv.style.left = DIV_HOOK_POS_Y + "px";
  202. hookDiv.style.zIndex = DIV_HOOK_ZINDEX.toString();
  203. hookDiv.style.backgroundColor = '#FF0000';
  204. hookDiv.title = 'select to enable accessibility for this content';
  205. hookDiv.addEventListener('focus', function () {
  206. _this._isMobileAccessibility = true;
  207. _this.activate();
  208. _this.destroyTouchHook();
  209. });
  210. document.body.appendChild(hookDiv);
  211. this._hookDiv = hookDiv;
  212. };
  213. /**
  214. * Destroys the touch hooks.
  215. * @private
  216. */
  217. AccessibilityManager.prototype.destroyTouchHook = function () {
  218. if (!this._hookDiv) {
  219. return;
  220. }
  221. document.body.removeChild(this._hookDiv);
  222. this._hookDiv = null;
  223. };
  224. /**
  225. * Activating will cause the Accessibility layer to be shown.
  226. * This is called when a user presses the tab key.
  227. * @private
  228. */
  229. AccessibilityManager.prototype.activate = function () {
  230. var _a;
  231. if (this._isActive) {
  232. return;
  233. }
  234. this._isActive = true;
  235. globalThis.document.addEventListener('mousemove', this._onMouseMove, true);
  236. globalThis.removeEventListener('keydown', this._onKeyDown, false);
  237. this.renderer.on('postrender', this.update, this);
  238. (_a = this.renderer.view.parentNode) === null || _a === void 0 ? void 0 : _a.appendChild(this.div);
  239. };
  240. /**
  241. * Deactivating will cause the Accessibility layer to be hidden.
  242. * This is called when a user moves the mouse.
  243. * @private
  244. */
  245. AccessibilityManager.prototype.deactivate = function () {
  246. var _a;
  247. if (!this._isActive || this._isMobileAccessibility) {
  248. return;
  249. }
  250. this._isActive = false;
  251. globalThis.document.removeEventListener('mousemove', this._onMouseMove, true);
  252. globalThis.addEventListener('keydown', this._onKeyDown, false);
  253. this.renderer.off('postrender', this.update);
  254. (_a = this.div.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(this.div);
  255. };
  256. /**
  257. * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer.
  258. * @private
  259. * @param {PIXI.Container} displayObject - The DisplayObject to check.
  260. */
  261. AccessibilityManager.prototype.updateAccessibleObjects = function (displayObject) {
  262. if (!displayObject.visible || !displayObject.accessibleChildren) {
  263. return;
  264. }
  265. if (displayObject.accessible && displayObject.interactive) {
  266. if (!displayObject._accessibleActive) {
  267. this.addChild(displayObject);
  268. }
  269. displayObject.renderId = this.renderId;
  270. }
  271. var children = displayObject.children;
  272. if (children) {
  273. for (var i = 0; i < children.length; i++) {
  274. this.updateAccessibleObjects(children[i]);
  275. }
  276. }
  277. };
  278. /**
  279. * Before each render this function will ensure that all divs are mapped correctly to their DisplayObjects.
  280. * @private
  281. */
  282. AccessibilityManager.prototype.update = function () {
  283. /* On Android default web browser, tab order seems to be calculated by position rather than tabIndex,
  284. * moving buttons can cause focus to flicker between two buttons making it hard/impossible to navigate,
  285. * so I am just running update every half a second, seems to fix it.
  286. */
  287. var now = performance.now();
  288. if (utils.isMobile.android.device && now < this.androidUpdateCount) {
  289. return;
  290. }
  291. this.androidUpdateCount = now + this.androidUpdateFrequency;
  292. if (!this.renderer.renderingToScreen) {
  293. return;
  294. }
  295. // update children...
  296. if (this.renderer._lastObjectRendered) {
  297. this.updateAccessibleObjects(this.renderer._lastObjectRendered);
  298. }
  299. var _a = this.renderer.view.getBoundingClientRect(), left = _a.left, top = _a.top, width = _a.width, height = _a.height;
  300. var _b = this.renderer, viewWidth = _b.width, viewHeight = _b.height, resolution = _b.resolution;
  301. var sx = (width / viewWidth) * resolution;
  302. var sy = (height / viewHeight) * resolution;
  303. var div = this.div;
  304. div.style.left = left + "px";
  305. div.style.top = top + "px";
  306. div.style.width = viewWidth + "px";
  307. div.style.height = viewHeight + "px";
  308. for (var i = 0; i < this.children.length; i++) {
  309. var child = this.children[i];
  310. if (child.renderId !== this.renderId) {
  311. child._accessibleActive = false;
  312. utils.removeItems(this.children, i, 1);
  313. this.div.removeChild(child._accessibleDiv);
  314. this.pool.push(child._accessibleDiv);
  315. child._accessibleDiv = null;
  316. i--;
  317. }
  318. else {
  319. // map div to display..
  320. div = child._accessibleDiv;
  321. var hitArea = child.hitArea;
  322. var wt = child.worldTransform;
  323. if (child.hitArea) {
  324. div.style.left = (wt.tx + (hitArea.x * wt.a)) * sx + "px";
  325. div.style.top = (wt.ty + (hitArea.y * wt.d)) * sy + "px";
  326. div.style.width = hitArea.width * wt.a * sx + "px";
  327. div.style.height = hitArea.height * wt.d * sy + "px";
  328. }
  329. else {
  330. hitArea = child.getBounds();
  331. this.capHitArea(hitArea);
  332. div.style.left = hitArea.x * sx + "px";
  333. div.style.top = hitArea.y * sy + "px";
  334. div.style.width = hitArea.width * sx + "px";
  335. div.style.height = hitArea.height * sy + "px";
  336. // update button titles and hints if they exist and they've changed
  337. if (div.title !== child.accessibleTitle && child.accessibleTitle !== null) {
  338. div.title = child.accessibleTitle;
  339. }
  340. if (div.getAttribute('aria-label') !== child.accessibleHint
  341. && child.accessibleHint !== null) {
  342. div.setAttribute('aria-label', child.accessibleHint);
  343. }
  344. }
  345. // the title or index may have changed, if so lets update it!
  346. if (child.accessibleTitle !== div.title || child.tabIndex !== div.tabIndex) {
  347. div.title = child.accessibleTitle;
  348. div.tabIndex = child.tabIndex;
  349. if (this.debug)
  350. { this.updateDebugHTML(div); }
  351. }
  352. }
  353. }
  354. // increment the render id..
  355. this.renderId++;
  356. };
  357. /**
  358. * private function that will visually add the information to the
  359. * accessability div
  360. * @param {HTMLElement} div -
  361. */
  362. AccessibilityManager.prototype.updateDebugHTML = function (div) {
  363. div.innerHTML = "type: " + div.type + "</br> title : " + div.title + "</br> tabIndex: " + div.tabIndex;
  364. };
  365. /**
  366. * Adjust the hit area based on the bounds of a display object
  367. * @param {PIXI.Rectangle} hitArea - Bounds of the child
  368. */
  369. AccessibilityManager.prototype.capHitArea = function (hitArea) {
  370. if (hitArea.x < 0) {
  371. hitArea.width += hitArea.x;
  372. hitArea.x = 0;
  373. }
  374. if (hitArea.y < 0) {
  375. hitArea.height += hitArea.y;
  376. hitArea.y = 0;
  377. }
  378. var _a = this.renderer, viewWidth = _a.width, viewHeight = _a.height;
  379. if (hitArea.x + hitArea.width > viewWidth) {
  380. hitArea.width = viewWidth - hitArea.x;
  381. }
  382. if (hitArea.y + hitArea.height > viewHeight) {
  383. hitArea.height = viewHeight - hitArea.y;
  384. }
  385. };
  386. /**
  387. * Adds a DisplayObject to the accessibility manager
  388. * @private
  389. * @param {PIXI.DisplayObject} displayObject - The child to make accessible.
  390. */
  391. AccessibilityManager.prototype.addChild = function (displayObject) {
  392. // this.activate();
  393. var div = this.pool.pop();
  394. if (!div) {
  395. div = document.createElement('button');
  396. div.style.width = DIV_TOUCH_SIZE + "px";
  397. div.style.height = DIV_TOUCH_SIZE + "px";
  398. div.style.backgroundColor = this.debug ? 'rgba(255,255,255,0.5)' : 'transparent';
  399. div.style.position = 'absolute';
  400. div.style.zIndex = DIV_TOUCH_ZINDEX.toString();
  401. div.style.borderStyle = 'none';
  402. // ARIA attributes ensure that button title and hint updates are announced properly
  403. if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
  404. // Chrome doesn't need aria-live to work as intended; in fact it just gets more confused.
  405. div.setAttribute('aria-live', 'off');
  406. }
  407. else {
  408. div.setAttribute('aria-live', 'polite');
  409. }
  410. if (navigator.userAgent.match(/rv:.*Gecko\//)) {
  411. // FireFox needs this to announce only the new button name
  412. div.setAttribute('aria-relevant', 'additions');
  413. }
  414. else {
  415. // required by IE, other browsers don't much care
  416. div.setAttribute('aria-relevant', 'text');
  417. }
  418. div.addEventListener('click', this._onClick.bind(this));
  419. div.addEventListener('focus', this._onFocus.bind(this));
  420. div.addEventListener('focusout', this._onFocusOut.bind(this));
  421. }
  422. // set pointer events
  423. div.style.pointerEvents = displayObject.accessiblePointerEvents;
  424. // set the type, this defaults to button!
  425. div.type = displayObject.accessibleType;
  426. if (displayObject.accessibleTitle && displayObject.accessibleTitle !== null) {
  427. div.title = displayObject.accessibleTitle;
  428. }
  429. else if (!displayObject.accessibleHint
  430. || displayObject.accessibleHint === null) {
  431. div.title = "displayObject " + displayObject.tabIndex;
  432. }
  433. if (displayObject.accessibleHint
  434. && displayObject.accessibleHint !== null) {
  435. div.setAttribute('aria-label', displayObject.accessibleHint);
  436. }
  437. if (this.debug)
  438. { this.updateDebugHTML(div); }
  439. displayObject._accessibleActive = true;
  440. displayObject._accessibleDiv = div;
  441. div.displayObject = displayObject;
  442. this.children.push(displayObject);
  443. this.div.appendChild(displayObject._accessibleDiv);
  444. displayObject._accessibleDiv.tabIndex = displayObject.tabIndex;
  445. };
  446. /**
  447. * Maps the div button press to pixi's InteractionManager (click)
  448. * @private
  449. * @param {MouseEvent} e - The click event.
  450. */
  451. AccessibilityManager.prototype._onClick = function (e) {
  452. var interactionManager = this.renderer.plugins.interaction;
  453. var displayObject = e.target.displayObject;
  454. var eventData = interactionManager.eventData;
  455. interactionManager.dispatchEvent(displayObject, 'click', eventData);
  456. interactionManager.dispatchEvent(displayObject, 'pointertap', eventData);
  457. interactionManager.dispatchEvent(displayObject, 'tap', eventData);
  458. };
  459. /**
  460. * Maps the div focus events to pixi's InteractionManager (mouseover)
  461. * @private
  462. * @param {FocusEvent} e - The focus event.
  463. */
  464. AccessibilityManager.prototype._onFocus = function (e) {
  465. if (!e.target.getAttribute('aria-live')) {
  466. e.target.setAttribute('aria-live', 'assertive');
  467. }
  468. var interactionManager = this.renderer.plugins.interaction;
  469. var displayObject = e.target.displayObject;
  470. var eventData = interactionManager.eventData;
  471. interactionManager.dispatchEvent(displayObject, 'mouseover', eventData);
  472. };
  473. /**
  474. * Maps the div focus events to pixi's InteractionManager (mouseout)
  475. * @private
  476. * @param {FocusEvent} e - The focusout event.
  477. */
  478. AccessibilityManager.prototype._onFocusOut = function (e) {
  479. if (!e.target.getAttribute('aria-live')) {
  480. e.target.setAttribute('aria-live', 'polite');
  481. }
  482. var interactionManager = this.renderer.plugins.interaction;
  483. var displayObject = e.target.displayObject;
  484. var eventData = interactionManager.eventData;
  485. interactionManager.dispatchEvent(displayObject, 'mouseout', eventData);
  486. };
  487. /**
  488. * Is called when a key is pressed
  489. * @private
  490. * @param {KeyboardEvent} e - The keydown event.
  491. */
  492. AccessibilityManager.prototype._onKeyDown = function (e) {
  493. if (e.keyCode !== KEY_CODE_TAB) {
  494. return;
  495. }
  496. this.activate();
  497. };
  498. /**
  499. * Is called when the mouse moves across the renderer element
  500. * @private
  501. * @param {MouseEvent} e - The mouse event.
  502. */
  503. AccessibilityManager.prototype._onMouseMove = function (e) {
  504. if (e.movementX === 0 && e.movementY === 0) {
  505. return;
  506. }
  507. this.deactivate();
  508. };
  509. /** Destroys the accessibility manager */
  510. AccessibilityManager.prototype.destroy = function () {
  511. this.destroyTouchHook();
  512. this.div = null;
  513. globalThis.document.removeEventListener('mousemove', this._onMouseMove, true);
  514. globalThis.removeEventListener('keydown', this._onKeyDown);
  515. this.pool = null;
  516. this.children = null;
  517. this.renderer = null;
  518. };
  519. /** @ignore */
  520. AccessibilityManager.extension = {
  521. name: 'accessibility',
  522. type: [
  523. core.ExtensionType.RendererPlugin,
  524. core.ExtensionType.CanvasRendererPlugin ],
  525. };
  526. return AccessibilityManager;
  527. }());
  528. exports.AccessibilityManager = AccessibilityManager;
  529. exports.accessibleTarget = accessibleTarget;
  530. //# sourceMappingURL=accessibility.js.map