Pagination.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {
  2. ARIA_CONTROLS,
  3. ARIA_LABEL,
  4. ARIA_ORIENTATION,
  5. ARIA_SELECTED,
  6. ROLE,
  7. TAB_INDEX,
  8. } from '../../constants/attributes';
  9. import { CLASS_ACTIVE, CLASS_PAGINATION } from '../../constants/classes';
  10. import { TTB } from '../../constants/directions';
  11. import {
  12. EVENT_MOVE,
  13. EVENT_PAGINATION_MOUNTED,
  14. EVENT_PAGINATION_UPDATED,
  15. EVENT_REFRESH,
  16. EVENT_SCROLL,
  17. EVENT_SCROLLED,
  18. EVENT_UPDATED,
  19. } from '../../constants/events';
  20. import { EventInterface } from '../../constructors';
  21. import { Splide } from '../../core/Splide/Splide';
  22. import { BaseComponent, Components, Options } from '../../types';
  23. import {
  24. addClass,
  25. apply,
  26. ceil,
  27. create,
  28. empty,
  29. focus,
  30. format,
  31. prevent,
  32. remove,
  33. removeAttribute,
  34. removeClass,
  35. setAttribute,
  36. slice,
  37. } from '../../utils';
  38. import { normalizeKey } from '../../utils/dom/normalizeKey/normalizeKey';
  39. /**
  40. * The interface for the Pagination component.
  41. *
  42. * @since 3.0.0
  43. */
  44. export interface PaginationComponent extends BaseComponent {
  45. items: PaginationItem[];
  46. getAt( index: number ): PaginationItem;
  47. update(): void;
  48. }
  49. /**
  50. * The interface for data of the pagination.
  51. *
  52. * @since 3.0.0
  53. */
  54. export interface PaginationData {
  55. list: HTMLUListElement;
  56. items: PaginationItem[];
  57. }
  58. /**
  59. * The interface for each pagination item.
  60. *
  61. * @since 3.0.0
  62. */
  63. export interface PaginationItem {
  64. li: HTMLLIElement;
  65. button: HTMLButtonElement;
  66. page: number;
  67. }
  68. /**
  69. * The component for the pagination UI (a slide picker).
  70. *
  71. * @link https://www.w3.org/TR/2021/NOTE-wai-aria-practices-1.2-20211129/#grouped-carousel-elements
  72. * @since 3.0.0
  73. *
  74. * @param Splide - A Splide instance.
  75. * @param Components - A collection of components.
  76. * @param options - Options.
  77. *
  78. * @return A Pagination component object.
  79. */
  80. export function Pagination( Splide: Splide, Components: Components, options: Options ): PaginationComponent {
  81. const { on, emit, bind, destroy: destroyEvents } = EventInterface( Splide );
  82. const { Slides, Elements, Controller } = Components;
  83. const { hasFocus, getIndex, go } = Controller;
  84. const { resolve } = Components.Direction;
  85. /**
  86. * Stores all pagination items.
  87. */
  88. const items: PaginationItem[] = [];
  89. /**
  90. * The pagination element.
  91. */
  92. let list: HTMLUListElement | null;
  93. /**
  94. * Holds modifier classes.
  95. */
  96. let paginationClasses: string;
  97. /**
  98. * Called when the component is mounted.
  99. */
  100. function mount(): void {
  101. destroy();
  102. on( [ EVENT_UPDATED, EVENT_REFRESH ], mount );
  103. if ( options.pagination && Slides.isEnough() ) {
  104. on( [ EVENT_MOVE, EVENT_SCROLL, EVENT_SCROLLED ], update );
  105. createPagination();
  106. update();
  107. emit( EVENT_PAGINATION_MOUNTED, { list, items }, getAt( Splide.index ) );
  108. }
  109. }
  110. /**
  111. * Destroys the component.
  112. */
  113. function destroy(): void {
  114. if ( list ) {
  115. destroyEvents();
  116. remove( Elements.pagination ? slice( list.children ) : list );
  117. removeClass( list, paginationClasses );
  118. empty( items );
  119. list = null;
  120. }
  121. }
  122. /**
  123. * Creates the pagination element and appends it to the slider.
  124. */
  125. function createPagination(): void {
  126. const { length } = Splide;
  127. const { classes, i18n, perPage } = options;
  128. const max = hasFocus() ? length : ceil( length / perPage );
  129. list = Elements.pagination || create( 'ul', classes.pagination, Elements.root );
  130. addClass( list, ( paginationClasses = `${ CLASS_PAGINATION }--${ getDirection() }` ) );
  131. setAttribute( list, ROLE, 'tablist' );
  132. setAttribute( list, ARIA_LABEL, i18n.select );
  133. setAttribute( list, ARIA_ORIENTATION, getDirection() === TTB ? 'vertical' : '' );
  134. for ( let i = 0; i < max; i++ ) {
  135. const li = create( 'li', null, list );
  136. const button = create( 'button', { class: classes.page, type: 'button' }, li );
  137. const controls = Slides.getIn( i ).map( Slide => Slide.slide.id );
  138. const text = ! hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
  139. bind( button, 'click', apply( onClick, i ) );
  140. if ( options.paginationKeyboard ) {
  141. bind( button, 'keydown', apply( onKeydown, i ) );
  142. }
  143. setAttribute( li, ROLE, 'none' );
  144. setAttribute( button, ROLE, 'tab' );
  145. setAttribute( button, ARIA_CONTROLS, controls.join( ' ' ) );
  146. setAttribute( button, ARIA_LABEL, format( text, i + 1 ) );
  147. setAttribute( button, TAB_INDEX, -1 );
  148. items.push( { li, button, page: i } );
  149. }
  150. }
  151. /**
  152. * Called when the user clicks each pagination dot.
  153. * Moves the focus to the active slide for accessibility.
  154. *
  155. * @link https://www.w3.org/WAI/tutorials/carousels/functionality/
  156. *
  157. * @param page - A clicked page index.
  158. */
  159. function onClick( page: number ): void {
  160. go( `>${ page }`, true );
  161. }
  162. /**
  163. * Called when any key is pressed on the pagination.
  164. *
  165. * @link https://www.w3.org/TR/2021/NOTE-wai-aria-practices-1.2-20211129/#keyboard-interaction-21
  166. *
  167. * @param page - A page index.
  168. * @param e - A KeyboardEvent object.
  169. */
  170. function onKeydown( page: number, e: KeyboardEvent ): void {
  171. const { length } = items;
  172. const key = normalizeKey( e );
  173. const dir = getDirection();
  174. let nextPage = -1;
  175. if ( key === resolve( 'ArrowRight', false, dir ) ) {
  176. nextPage = ++page % length;
  177. } else if ( key === resolve( 'ArrowLeft', false, dir ) ) {
  178. nextPage = ( --page + length ) % length;
  179. } else if ( key === 'Home' ) {
  180. nextPage = 0;
  181. } else if ( key === 'End' ) {
  182. nextPage = length - 1;
  183. }
  184. const item = items[ nextPage ];
  185. if ( item ) {
  186. focus( item.button );
  187. go( `>${ nextPage }` );
  188. prevent( e, true );
  189. }
  190. }
  191. /**
  192. * Returns the latest direction for pagination.
  193. */
  194. function getDirection(): Options['direction'] {
  195. return options.paginationDirection || options.direction;
  196. }
  197. /**
  198. * Returns the pagination item at the specified index.
  199. *
  200. * @param index - An index.
  201. *
  202. * @return A pagination item object if available, or otherwise `undefined`.
  203. */
  204. function getAt( index: number ): PaginationItem | undefined {
  205. return items[ Controller.toPage( index ) ];
  206. }
  207. /**
  208. * Updates the pagination status.
  209. */
  210. function update(): void {
  211. const prev = getAt( getIndex( true ) );
  212. const curr = getAt( getIndex() );
  213. if ( prev ) {
  214. const { button } = prev;
  215. removeClass( button, CLASS_ACTIVE );
  216. removeAttribute( button, ARIA_SELECTED );
  217. setAttribute( button, TAB_INDEX, -1 );
  218. }
  219. if ( curr ) {
  220. const { button } = curr;
  221. addClass( button, CLASS_ACTIVE );
  222. setAttribute( button, ARIA_SELECTED, true );
  223. setAttribute( button, TAB_INDEX, '' );
  224. }
  225. emit( EVENT_PAGINATION_UPDATED, { list, items }, prev, curr );
  226. }
  227. return {
  228. items,
  229. mount,
  230. destroy,
  231. getAt,
  232. update,
  233. };
  234. }