Pagination.ts 6.9 KB

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