accessibility.js 22 KB

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