index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. /**
  2. * The component for enhancing accessibility.
  3. *
  4. * @author Naotoshi Fujita
  5. * @copyright Naotoshi Fujita. All rights reserved.
  6. */
  7. import { setAttribute, removeAttribute } from '../../utils/dom';
  8. import { sprintf } from '../../utils/utils';
  9. import { ARIA_CONTROLS, ARIA_CURRENRT, ARIA_HIDDEN, ARIA_LABEL, TAB_INDEX } from '../../constants/a11y';
  10. /**
  11. * The component for enhancing accessibility.
  12. *
  13. * @param {Splide} Splide - A Splide instance.
  14. * @param {Object} Components - An object containing components.
  15. *
  16. * @return {Object} - The component object.
  17. */
  18. export default ( Splide, Components ) => {
  19. /**
  20. * Hold a i18n object.
  21. *
  22. * @type {Object}
  23. */
  24. const i18n = Splide.i18n;
  25. /**
  26. * A11y component object.
  27. *
  28. * @type {Object}
  29. */
  30. const A11y = {
  31. /**
  32. * Required only when the accessibility option is true.
  33. *
  34. * @type {boolean}
  35. */
  36. required: Splide.options.accessibility,
  37. /**
  38. * Called when the component is mounted.
  39. */
  40. mount() {
  41. Splide
  42. .on( 'visible', Slide => { updateSlide( Slide.slide, true ) } )
  43. .on( 'hidden', Slide => { updateSlide( Slide.slide, false ) } )
  44. .on( 'arrows:mounted', initArrows )
  45. .on( 'arrows:updated', updateArrows )
  46. .on( 'pagination:mounted', initPagination )
  47. .on( 'pagination:updated', updatePagination );
  48. if ( Splide.options.isNavigation ) {
  49. Splide
  50. .on( 'navigation:mounted', initNavigation )
  51. .on( 'active', Slide => { updateNavigation( Slide, true ) } )
  52. .on( 'inactive', Slide => { updateNavigation( Slide, false ) } );
  53. }
  54. initAutoplay();
  55. },
  56. /**
  57. * Destroy.
  58. */
  59. destroy() {
  60. const Elements = Components.Elements;
  61. const arrows = Components.Arrows.arrows;
  62. Elements.slides
  63. .concat( [ arrows.prev, arrows.next, Elements.play, Elements.pause ] )
  64. .forEach( elm => {
  65. removeAttribute( elm, [ ARIA_HIDDEN, TAB_INDEX, ARIA_CONTROLS, ARIA_LABEL, ARIA_CURRENRT, 'role' ] );
  66. } );
  67. },
  68. };
  69. /**
  70. * Update slide attributes when it gets visible or hidden.
  71. *
  72. * @param {Element} slide - A slide element.
  73. * @param {Boolean} visible - True when the slide gets visible, or false when hidden.
  74. */
  75. function updateSlide( slide, visible ) {
  76. setAttribute( slide, ARIA_HIDDEN, ! visible );
  77. setAttribute( slide, TAB_INDEX, visible ? 0 : -1 );
  78. }
  79. /**
  80. * Initialize arrows if they are available.
  81. * Append screen reader elements and add aria-controls attribute.
  82. *
  83. * @param {Element} prev - Previous arrow element.
  84. * @param {Element} next - Next arrow element.
  85. */
  86. function initArrows( prev, next ) {
  87. const controls = Components.Elements.track.id;
  88. setAttribute( prev, ARIA_CONTROLS, controls );
  89. setAttribute( next, ARIA_CONTROLS, controls );
  90. }
  91. /**
  92. * Update arrow attributes.
  93. *
  94. * @param {Element} prev - Previous arrow element.
  95. * @param {Element} next - Next arrow element.
  96. * @param {number} prevIndex - Previous slide index or -1 when there is no precede slide.
  97. * @param {number} nextIndex - Next slide index or -1 when there is no next slide.
  98. */
  99. function updateArrows( prev, next, prevIndex, nextIndex ) {
  100. const index = Splide.index;
  101. const prevLabel = prevIndex > -1 && index < prevIndex ? i18n.last : i18n.prev;
  102. const nextLabel = nextIndex > -1 && index > nextIndex ? i18n.first : i18n.next;
  103. setAttribute( prev, ARIA_LABEL, prevLabel );
  104. setAttribute( next, ARIA_LABEL, nextLabel );
  105. }
  106. /**
  107. * Initialize pagination if it's available.
  108. * Append a screen reader element and add aria-controls/label attribute to each item.
  109. *
  110. * @param {Object} data - Data object containing all items.
  111. * @param {Object} activeItem - An initial active item.
  112. */
  113. function initPagination( data, activeItem ) {
  114. if ( activeItem ) {
  115. setAttribute( activeItem.button, ARIA_CURRENRT, true );
  116. }
  117. data.items.forEach( item => {
  118. const options = Splide.options;
  119. const text = options.focus === false && options.perPage > 1 ? i18n.pageX : i18n.slideX;
  120. const label = sprintf( text, item.page + 1 );
  121. const button = item.button;
  122. const controls = item.Slides.map( Slide => Slide.slide.id );
  123. setAttribute( button, ARIA_CONTROLS, controls.join( ' ' ) );
  124. setAttribute( button, ARIA_LABEL, label );
  125. } );
  126. }
  127. /**
  128. * Update pagination attributes.
  129. *
  130. * @param {Object} data - Data object containing all items.
  131. * @param {Element} prev - A previous active element.
  132. * @param {Element} curr - A current active element.
  133. */
  134. function updatePagination( data, prev, curr ) {
  135. if ( prev ) {
  136. removeAttribute( prev.button, ARIA_CURRENRT );
  137. }
  138. if ( curr ) {
  139. setAttribute( curr.button, ARIA_CURRENRT, true );
  140. }
  141. }
  142. /**
  143. * Initialize autoplay buttons.
  144. */
  145. function initAutoplay() {
  146. const Elements = Components.Elements;
  147. [ Elements.play, Elements.pause ].forEach( ( elm, index ) => {
  148. if ( elm ) {
  149. if ( ! isButton( elm ) ) {
  150. setAttribute( elm, 'role', 'button' );
  151. }
  152. setAttribute( elm, ARIA_CONTROLS, Elements.track.id );
  153. setAttribute( elm, ARIA_LABEL, i18n[ index === 0 ? 'play' : 'pause' ] );
  154. }
  155. } );
  156. }
  157. /**
  158. * Initialize navigation slider.
  159. * Add button role, aria-label, aria-controls to slide elements and append screen reader text to them.
  160. *
  161. * @param {Splide} main - A main Splide instance.
  162. */
  163. function initNavigation( main ) {
  164. const Slides = Components.Slides.getSlides( true, true );
  165. Slides.forEach( Slide => {
  166. const slide = Slide.slide;
  167. if ( ! isButton( slide ) ) {
  168. setAttribute( slide, 'role', 'button' );
  169. }
  170. const slideIndex = Slide.realIndex > -1 ? Slide.realIndex : Slide.index;
  171. const label = sprintf( i18n.slideX, slideIndex + 1 );
  172. const mainSlide = main.Components.Slides.getSlide( slideIndex );
  173. setAttribute( slide, ARIA_LABEL, label );
  174. if ( mainSlide ) {
  175. setAttribute( slide, ARIA_CONTROLS, mainSlide.slide.id );
  176. }
  177. } );
  178. }
  179. /**
  180. * Update navigation attributes.
  181. *
  182. * @param {Object} Slide - A target Slide object.
  183. * @param {boolean} active - True if the slide is active or false if inactive.
  184. */
  185. function updateNavigation( { slide }, active ) {
  186. if ( active ) {
  187. setAttribute( slide, ARIA_CURRENRT, true );
  188. } else {
  189. removeAttribute( slide, ARIA_CURRENRT );
  190. }
  191. }
  192. /**
  193. * Check if the given element is button or not.
  194. *
  195. * @param {Element} elm - An element to be checked.
  196. *
  197. * @return {boolean} - True if the given element is button.
  198. */
  199. function isButton( elm ) {
  200. return elm.tagName.toLowerCase() === 'button';
  201. }
  202. return A11y;
  203. }