index.js 6.0 KB

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