Pagination.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. const paginationClasses = `${ CLASS_PAGINATION }--${ options.direction }`;
  86. /**
  87. * Stores all pagination items.
  88. */
  89. const items: PaginationItem[] = [];
  90. /**
  91. * The pagination element.
  92. */
  93. let pagination: HTMLUListElement | null;
  94. /**
  95. * Called when the component is mounted.
  96. */
  97. function mount(): void {
  98. destroy();
  99. on( [ EVENT_UPDATED, EVENT_REFRESH ], mount );
  100. if ( options.pagination && Slides.isEnough() ) {
  101. on( [ EVENT_MOVE, EVENT_SCROLL, EVENT_SCROLLED ], update );
  102. createPagination();
  103. emit( EVENT_PAGINATION_MOUNTED, { list: pagination, items }, getAt( Splide.index ) );
  104. update();
  105. }
  106. }
  107. /**
  108. * Destroys the component.
  109. */
  110. function destroy(): void {
  111. if ( pagination ) {
  112. destroyEvents();
  113. remove( Elements.pagination ? slice( pagination.children ) : pagination );
  114. removeClass( pagination, paginationClasses );
  115. empty( items );
  116. pagination = null;
  117. }
  118. }
  119. /**
  120. * Creates the pagination element and appends it to the slider.
  121. */
  122. function createPagination(): void {
  123. const { length } = Splide;
  124. const { classes, i18n, perPage } = options;
  125. const max = hasFocus() ? length : ceil( length / perPage );
  126. pagination = Elements.pagination || create( 'ul', classes.pagination, Elements.root );
  127. addClass( pagination, paginationClasses );
  128. setAttribute( pagination, ROLE, 'tablist' );
  129. setAttribute( pagination, ARIA_LABEL, i18n.select );
  130. setAttribute( pagination, ARIA_ORIENTATION, options.direction === TTB ? 'vertical' : '' );
  131. for ( let i = 0; i < max; i++ ) {
  132. const li = create( 'li', null, pagination );
  133. const button = create( 'button', { class: classes.page, type: 'button' }, li );
  134. const controls = Slides.getIn( i ).map( Slide => Slide.slide.id );
  135. const text = ! hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
  136. bind( button, 'click', apply( onClick, i ) );
  137. if ( options.paginationKeyboard ) {
  138. bind( button, 'keydown', apply( onKeydown, i ) );
  139. }
  140. setAttribute( li, ROLE, 'none' );
  141. setAttribute( button, ROLE, 'tab' );
  142. setAttribute( button, ARIA_CONTROLS, controls.join( ' ' ) );
  143. setAttribute( button, ARIA_LABEL, format( text, i + 1 ) );
  144. setAttribute( button, TAB_INDEX, -1 );
  145. items.push( { li, button, page: i } );
  146. }
  147. }
  148. /**
  149. * Called when the user clicks each pagination dot.
  150. * Moves the focus to the active slide for accessibility.
  151. *
  152. * @link https://www.w3.org/WAI/tutorials/carousels/functionality/
  153. *
  154. * @param page - A clicked page index.
  155. */
  156. function onClick( page: number ): void {
  157. go( `>${ page }`, true );
  158. }
  159. /**
  160. * Called when any key is pressed on the pagination.
  161. *
  162. * @link https://www.w3.org/TR/2021/NOTE-wai-aria-practices-1.2-20211129/#keyboard-interaction-21
  163. *
  164. * @param page - A page index.
  165. * @param e - A KeyboardEvent object.
  166. */
  167. function onKeydown( page: number, e: KeyboardEvent ): void {
  168. const { length } = items;
  169. const key = normalizeKey( e );
  170. let nextPage = -1;
  171. if ( key === resolve( 'ArrowRight' ) ) {
  172. nextPage = ++page % length;
  173. } else if ( key === resolve( 'ArrowLeft' ) ) {
  174. nextPage = ( --page + length ) % length;
  175. } else if ( key === 'Home' ) {
  176. nextPage = 0;
  177. } else if ( key === 'End' ) {
  178. nextPage = length - 1;
  179. }
  180. const item = items[ nextPage ];
  181. if ( item ) {
  182. focus( item.button );
  183. go( `>${ nextPage }` );
  184. prevent( e, true );
  185. }
  186. }
  187. /**
  188. * Returns the pagination item at the specified index.
  189. *
  190. * @param index - An index.
  191. *
  192. * @return A pagination item object if available, or otherwise `undefined`.
  193. */
  194. function getAt( index: number ): PaginationItem | undefined {
  195. return items[ Controller.toPage( index ) ];
  196. }
  197. /**
  198. * Updates the pagination status.
  199. */
  200. function update(): void {
  201. const prev = getAt( getIndex( true ) );
  202. const curr = getAt( getIndex() );
  203. if ( prev ) {
  204. const { button } = prev;
  205. removeClass( button, CLASS_ACTIVE );
  206. removeAttribute( button, ARIA_SELECTED );
  207. setAttribute( button, TAB_INDEX, -1 );
  208. }
  209. if ( curr ) {
  210. const { button } = curr;
  211. addClass( button, CLASS_ACTIVE );
  212. setAttribute( button, ARIA_SELECTED, true );
  213. setAttribute( button, TAB_INDEX, '' );
  214. }
  215. emit( EVENT_PAGINATION_UPDATED, { list: pagination, items }, prev, curr );
  216. }
  217. return {
  218. items,
  219. mount,
  220. destroy,
  221. getAt,
  222. update,
  223. };
  224. }