Source: accessibility/AccessibilityManager.js

accessibility/AccessibilityManager.js

  1. import * as core from '../core';
  2. //修改依赖库应对小程序环境
  3. import Device from '../dependencies/isMobile';
  4. //import Device from 'ismobilejs';
  5. import accessibleTarget from './accessibleTarget';
  6. //引入小程序补丁
  7. import { windowAlias, documentAlias, navigator } from '@ali/pixi-miniprogram-adapter';
  8. // add some extra variables to the container..
  9. core.utils.mixins.delayMixin(
  10. core.DisplayObject.prototype,
  11. accessibleTarget
  12. );
  13. const KEY_CODE_TAB = 9;
  14. const DIV_TOUCH_SIZE = 100;
  15. const DIV_TOUCH_POS_X = 0;
  16. const DIV_TOUCH_POS_Y = 0;
  17. const DIV_TOUCH_ZINDEX = 2;
  18. const DIV_HOOK_SIZE = 1;
  19. const DIV_HOOK_POS_X = -1000;
  20. const DIV_HOOK_POS_Y = -1000;
  21. const DIV_HOOK_ZINDEX = 2;
  22. /**
  23. * The Accessibility manager recreates the ability to tab and have content read by screen
  24. * readers. This is very important as it can possibly help people with disabilities access pixi
  25. * content.
  26. *
  27. * Much like interaction any DisplayObject can be made accessible. This manager will map the
  28. * events as if the mouse was being used, minimizing the effort required to implement.
  29. *
  30. * An instance of this class is automatically created by default, and can be found at renderer.plugins.accessibility
  31. *
  32. * @class
  33. * @memberof PIXI.accessibility
  34. */
  35. export default class AccessibilityManager
  36. {
  37. /**
  38. * @param {PIXI.CanvasRenderer|PIXI.WebGLRenderer} renderer - A reference to the current renderer
  39. */
  40. constructor(renderer)
  41. {
  42. if ((Device.tablet || Device.phone) && !navigator.isCocoonJS)
  43. {
  44. this.createTouchHook();
  45. }
  46. // first we create a div that will sit over the PixiJS element. This is where the div overlays will go.
  47. const div = documentAlias.createElement('div');
  48. div.style.width = `${DIV_TOUCH_SIZE}px`;
  49. div.style.height = `${DIV_TOUCH_SIZE}px`;
  50. div.style.position = 'absolute';
  51. div.style.top = `${DIV_TOUCH_POS_X}px`;
  52. div.style.left = `${DIV_TOUCH_POS_Y}px`;
  53. div.style.zIndex = DIV_TOUCH_ZINDEX;
  54. /**
  55. * This is the dom element that will sit over the PixiJS element. This is where the div overlays will go.
  56. *
  57. * @type {HTMLElement}
  58. * @private
  59. */
  60. this.div = div;
  61. /**
  62. * A simple pool for storing divs.
  63. *
  64. * @type {*}
  65. * @private
  66. */
  67. this.pool = [];
  68. /**
  69. * This is a tick used to check if an object is no longer being rendered.
  70. *
  71. * @type {Number}
  72. * @private
  73. */
  74. this.renderId = 0;
  75. /**
  76. * Setting this to true will visually show the divs.
  77. *
  78. * @type {boolean}
  79. */
  80. this.debug = false;
  81. /**
  82. * The renderer this accessibility manager works for.
  83. *
  84. * @member {PIXI.SystemRenderer}
  85. */
  86. this.renderer = renderer;
  87. /**
  88. * The array of currently active accessible items.
  89. *
  90. * @member {Array<*>}
  91. * @private
  92. */
  93. this.children = [];
  94. /**
  95. * pre-bind the functions
  96. *
  97. * @private
  98. */
  99. this._onKeyDown = this._onKeyDown.bind(this);
  100. this._onMouseMove = this._onMouseMove.bind(this);
  101. /**
  102. * stores the state of the manager. If there are no accessible objects or the mouse is moving, this will be false.
  103. *
  104. * @member {Array<*>}
  105. * @private
  106. */
  107. this.isActive = false;
  108. this.isMobileAccessabillity = false;
  109. // let listen for tab.. once pressed we can fire up and show the accessibility layer
  110. windowAlias.addEventListener('keydown', this._onKeyDown, false);
  111. }
  112. /**
  113. * Creates the touch hooks.
  114. *
  115. */
  116. createTouchHook()
  117. {
  118. const hookDiv = documentAlias.createElement('button');
  119. hookDiv.style.width = `${DIV_HOOK_SIZE}px`;
  120. hookDiv.style.height = `${DIV_HOOK_SIZE}px`;
  121. hookDiv.style.position = 'absolute';
  122. hookDiv.style.top = `${DIV_HOOK_POS_X}px`;
  123. hookDiv.style.left = `${DIV_HOOK_POS_Y}px`;
  124. hookDiv.style.zIndex = DIV_HOOK_ZINDEX;
  125. hookDiv.style.backgroundColor = '#FF0000';
  126. hookDiv.title = 'HOOK DIV';
  127. hookDiv.addEventListener('focus', () =>
  128. {
  129. this.isMobileAccessabillity = true;
  130. this.activate();
  131. documentAlias.body.removeChild(hookDiv);
  132. });
  133. documentAlias.body.appendChild(hookDiv);
  134. }
  135. /**
  136. * Activating will cause the Accessibility layer to be shown. This is called when a user
  137. * preses the tab key.
  138. *
  139. * @private
  140. */
  141. activate()
  142. {
  143. if (this.isActive)
  144. {
  145. return;
  146. }
  147. this.isActive = true;
  148. windowAlias.documentAlias.addEventListener('mousemove', this._onMouseMove, true);
  149. windowAlias.removeEventListener('keydown', this._onKeyDown, false);
  150. this.renderer.on('postrender', this.update, this);
  151. if (this.renderer.view.parentNode)
  152. {
  153. this.renderer.view.parentNode.appendChild(this.div);
  154. }
  155. }
  156. /**
  157. * Deactivating will cause the Accessibility layer to be hidden. This is called when a user moves
  158. * the mouse.
  159. *
  160. * @private
  161. */
  162. deactivate()
  163. {
  164. if (!this.isActive || this.isMobileAccessabillity)
  165. {
  166. return;
  167. }
  168. this.isActive = false;
  169. windowAlias.documentAlias.removeEventListener('mousemove', this._onMouseMove, true);
  170. windowAlias.addEventListener('keydown', this._onKeyDown, false);
  171. this.renderer.off('postrender', this.update);
  172. if (this.div.parentNode)
  173. {
  174. this.div.parentNode.removeChild(this.div);
  175. }
  176. }
  177. /**
  178. * This recursive function will run through the scene graph and add any new accessible objects to the DOM layer.
  179. *
  180. * @private
  181. * @param {PIXI.Container} displayObject - The DisplayObject to check.
  182. */
  183. updateAccessibleObjects(displayObject)
  184. {
  185. if (!displayObject.visible)
  186. {
  187. return;
  188. }
  189. if (displayObject.accessible && displayObject.interactive)
  190. {
  191. if (!displayObject._accessibleActive)
  192. {
  193. this.addChild(displayObject);
  194. }
  195. displayObject.renderId = this.renderId;
  196. }
  197. const children = displayObject.children;
  198. for (let i = 0; i < children.length; i++)
  199. {
  200. this.updateAccessibleObjects(children[i]);
  201. }
  202. }
  203. /**
  204. * Before each render this function will ensure that all divs are mapped correctly to their DisplayObjects.
  205. *
  206. * @private
  207. */
  208. update()
  209. {
  210. if (!this.renderer.renderingToScreen)
  211. {
  212. return;
  213. }
  214. // update children...
  215. this.updateAccessibleObjects(this.renderer._lastObjectRendered);
  216. const rect = this.renderer.view.getBoundingClientRect();
  217. const sx = rect.width / this.renderer.width;
  218. const sy = rect.height / this.renderer.height;
  219. let div = this.div;
  220. div.style.left = `${rect.left}px`;
  221. div.style.top = `${rect.top}px`;
  222. div.style.width = `${this.renderer.width}px`;
  223. div.style.height = `${this.renderer.height}px`;
  224. for (let i = 0; i < this.children.length; i++)
  225. {
  226. const child = this.children[i];
  227. if (child.renderId !== this.renderId)
  228. {
  229. child._accessibleActive = false;
  230. core.utils.removeItems(this.children, i, 1);
  231. this.div.removeChild(child._accessibleDiv);
  232. this.pool.push(child._accessibleDiv);
  233. child._accessibleDiv = null;
  234. i--;
  235. if (this.children.length === 0)
  236. {
  237. this.deactivate();
  238. }
  239. }
  240. else
  241. {
  242. // map div to display..
  243. div = child._accessibleDiv;
  244. let hitArea = child.hitArea;
  245. const wt = child.worldTransform;
  246. if (child.hitArea)
  247. {
  248. div.style.left = `${(wt.tx + (hitArea.x * wt.a)) * sx}px`;
  249. div.style.top = `${(wt.ty + (hitArea.y * wt.d)) * sy}px`;
  250. div.style.width = `${hitArea.width * wt.a * sx}px`;
  251. div.style.height = `${hitArea.height * wt.d * sy}px`;
  252. }
  253. else
  254. {
  255. hitArea = child.getBounds();
  256. this.capHitArea(hitArea);
  257. div.style.left = `${hitArea.x * sx}px`;
  258. div.style.top = `${hitArea.y * sy}px`;
  259. div.style.width = `${hitArea.width * sx}px`;
  260. div.style.height = `${hitArea.height * sy}px`;
  261. // update button titles and hints if they exist and they've changed
  262. if (div.title !== child.accessibleTitle && child.accessibleTitle !== null)
  263. {
  264. div.title = child.accessibleTitle;
  265. }
  266. if (div.getAttribute('aria-label') !== child.accessibleHint
  267. && child.accessibleHint !== null)
  268. {
  269. div.setAttribute('aria-label', child.accessibleHint);
  270. }
  271. }
  272. }
  273. }
  274. // increment the render id..
  275. this.renderId++;
  276. }
  277. /**
  278. * TODO: docs.
  279. *
  280. * @param {Rectangle} hitArea - TODO docs
  281. */
  282. capHitArea(hitArea)
  283. {
  284. if (hitArea.x < 0)
  285. {
  286. hitArea.width += hitArea.x;
  287. hitArea.x = 0;
  288. }
  289. if (hitArea.y < 0)
  290. {
  291. hitArea.height += hitArea.y;
  292. hitArea.y = 0;
  293. }
  294. if (hitArea.x + hitArea.width > this.renderer.width)
  295. {
  296. hitArea.width = this.renderer.width - hitArea.x;
  297. }
  298. if (hitArea.y + hitArea.height > this.renderer.height)
  299. {
  300. hitArea.height = this.renderer.height - hitArea.y;
  301. }
  302. }
  303. /**
  304. * Adds a DisplayObject to the accessibility manager
  305. *
  306. * @private
  307. * @param {DisplayObject} displayObject - The child to make accessible.
  308. */
  309. addChild(displayObject)
  310. {
  311. // this.activate();
  312. let div = this.pool.pop();
  313. if (!div)
  314. {
  315. div = documentAlias.createElement('button');
  316. div.style.width = `${DIV_TOUCH_SIZE}px`;
  317. div.style.height = `${DIV_TOUCH_SIZE}px`;
  318. div.style.backgroundColor = this.debug ? 'rgba(255,0,0,0.5)' : 'transparent';
  319. div.style.position = 'absolute';
  320. div.style.zIndex = DIV_TOUCH_ZINDEX;
  321. div.style.borderStyle = 'none';
  322. // ARIA attributes ensure that button title and hint updates are announced properly
  323. if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1)
  324. {
  325. // Chrome doesn't need aria-live to work as intended; in fact it just gets more confused.
  326. div.setAttribute('aria-live', 'off');
  327. }
  328. else
  329. {
  330. div.setAttribute('aria-live', 'polite');
  331. }
  332. if (navigator.userAgent.match(/rv:.*Gecko\//))
  333. {
  334. // FireFox needs this to announce only the new button name
  335. div.setAttribute('aria-relevant', 'additions');
  336. }
  337. else
  338. {
  339. // required by IE, other browsers don't much care
  340. div.setAttribute('aria-relevant', 'text');
  341. }
  342. div.addEventListener('click', this._onClick.bind(this));
  343. div.addEventListener('focus', this._onFocus.bind(this));
  344. div.addEventListener('focusout', this._onFocusOut.bind(this));
  345. }
  346. if (displayObject.accessibleTitle && displayObject.accessibleTitle !== null)
  347. {
  348. div.title = displayObject.accessibleTitle;
  349. }
  350. else if (!displayObject.accessibleHint
  351. || displayObject.accessibleHint === null)
  352. {
  353. div.title = `displayObject ${displayObject.tabIndex}`;
  354. }
  355. if (displayObject.accessibleHint
  356. && displayObject.accessibleHint !== null)
  357. {
  358. div.setAttribute('aria-label', displayObject.accessibleHint);
  359. }
  360. //
  361. displayObject._accessibleActive = true;
  362. displayObject._accessibleDiv = div;
  363. div.displayObject = displayObject;
  364. this.children.push(displayObject);
  365. this.div.appendChild(displayObject._accessibleDiv);
  366. displayObject._accessibleDiv.tabIndex = displayObject.tabIndex;
  367. }
  368. /**
  369. * Maps the div button press to pixi's InteractionManager (click)
  370. *
  371. * @private
  372. * @param {MouseEvent} e - The click event.
  373. */
  374. _onClick(e)
  375. {
  376. const interactionManager = this.renderer.plugins.interaction;
  377. interactionManager.dispatchEvent(e.target.displayObject, 'click', interactionManager.eventData);
  378. }
  379. /**
  380. * Maps the div focus events to pixi's InteractionManager (mouseover)
  381. *
  382. * @private
  383. * @param {FocusEvent} e - The focus event.
  384. */
  385. _onFocus(e)
  386. {
  387. if (!e.target.getAttribute('aria-live', 'off'))
  388. {
  389. e.target.setAttribute('aria-live', 'assertive');
  390. }
  391. const interactionManager = this.renderer.plugins.interaction;
  392. interactionManager.dispatchEvent(e.target.displayObject, 'mouseover', interactionManager.eventData);
  393. }
  394. /**
  395. * Maps the div focus events to pixi's InteractionManager (mouseout)
  396. *
  397. * @private
  398. * @param {FocusEvent} e - The focusout event.
  399. */
  400. _onFocusOut(e)
  401. {
  402. if (!e.target.getAttribute('aria-live', 'off'))
  403. {
  404. e.target.setAttribute('aria-live', 'polite');
  405. }
  406. const interactionManager = this.renderer.plugins.interaction;
  407. interactionManager.dispatchEvent(e.target.displayObject, 'mouseout', interactionManager.eventData);
  408. }
  409. /**
  410. * Is called when a key is pressed
  411. *
  412. * @private
  413. * @param {KeyboardEvent} e - The keydown event.
  414. */
  415. _onKeyDown(e)
  416. {
  417. if (e.keyCode !== KEY_CODE_TAB)
  418. {
  419. return;
  420. }
  421. this.activate();
  422. }
  423. /**
  424. * Is called when the mouse moves across the renderer element
  425. *
  426. * @private
  427. * @param {MouseEvent} e - The mouse event.
  428. */
  429. _onMouseMove(e)
  430. {
  431. if (e.movementX === 0 && e.movementY === 0)
  432. {
  433. return;
  434. }
  435. this.deactivate();
  436. }
  437. /**
  438. * Destroys the accessibility manager
  439. *
  440. */
  441. destroy()
  442. {
  443. this.div = null;
  444. for (let i = 0; i < this.children.length; i++)
  445. {
  446. this.children[i].div = null;
  447. }
  448. windowAlias.documentAlias.removeEventListener('mousemove', this._onMouseMove, true);
  449. windowAlias.removeEventListener('keydown', this._onKeyDown);
  450. this.pool = null;
  451. this.children = null;
  452. this.renderer = null;
  453. }
  454. }
  455. core.WebGLRenderer.registerPlugin('accessibility', AccessibilityManager);
  456. core.CanvasRenderer.registerPlugin('accessibility', AccessibilityManager);