فهرست منبع

Update test cases.

NaotoshiFujita 3 سال پیش
والد
کامیت
d6f088f31e

+ 165 - 152
dist/js/splide.cjs.js

@@ -56,9 +56,6 @@ const STATES = {
   DESTROYED
 };
 
-const DEFAULT_EVENT_PRIORITY = 10;
-const DEFAULT_USER_EVENT_PRIORITY = 20;
-
 function empty(array) {
   array.length = 0;
 }
@@ -212,7 +209,7 @@ function setAttribute(elms, attrs, value) {
     });
   } else {
     forEach(elms, (elm) => {
-      isNull(value) ? removeAttribute(elm, attrs) : elm.setAttribute(attrs, String(value));
+      isNull(value) || value === "" ? removeAttribute(elm, attrs) : elm.setAttribute(attrs, String(value));
     });
   }
 }
@@ -267,15 +264,6 @@ function remove(nodes) {
   });
 }
 
-function measure(parent, value) {
-  if (isString(value)) {
-    const div = create("div", { style: `width: ${value}; position: absolute;` }, parent);
-    value = rect(div).width;
-    remove(div);
-  }
-  return value;
-}
-
 function parseHtml(html) {
   return child(new DOMParser().parseFromString(html, "text/html").body);
 }
@@ -293,7 +281,7 @@ function query(parent, selector) {
 }
 
 function queryAll(parent, selector) {
-  return slice(parent.querySelectorAll(selector));
+  return selector ? slice(parent.querySelectorAll(selector)) : [];
 }
 
 function removeClass(elm, classes) {
@@ -359,103 +347,85 @@ function uniqueId(prefix) {
   return `${prefix}${pad(ids[prefix] = (ids[prefix] || 0) + 1)}`;
 }
 
-function EventBus() {
-  let handlers = {};
-  function on(events, callback, key, priority = DEFAULT_EVENT_PRIORITY) {
-    forEachEvent(events, (event, namespace) => {
-      const events2 = handlers[event] = handlers[event] || [];
-      events2.push([callback, namespace, priority, key]);
-      events2.sort((handler1, handler2) => handler1[2] - handler2[2]);
-    });
-  }
-  function off(events, key) {
-    forEachEvent(events, (event, namespace) => {
-      handlers[event] = (handlers[event] || []).filter((handler) => {
-        return handler[3] ? handler[3] !== key : key || handler[1] !== namespace;
-      });
-    });
-  }
-  function offBy(key) {
-    forOwn(handlers, (eventHandlers, event) => {
-      off(event, key);
-    });
-  }
-  function emit(event) {
-    (handlers[event] || []).forEach((handler) => {
-      handler[0].apply(handler, slice(arguments, 1));
-    });
-  }
-  function destroy() {
-    handlers = {};
-  }
-  function forEachEvent(events, iteratee) {
-    toArray(events).join(" ").split(" ").forEach((eventNS) => {
-      const fragments = eventNS.split(".");
-      iteratee(fragments[0], fragments[1]);
-    });
-  }
-  return {
-    on,
-    off,
-    offBy,
-    emit,
-    destroy
-  };
-}
-
-function EventInterface(Splide2, manual) {
-  const { event } = Splide2;
-  const key = {};
+function EventBinder() {
   let listeners = [];
-  function on(events, callback, priority) {
-    event.on(events, callback, key, priority);
-  }
-  function off(events) {
-    event.off(events, key);
-  }
   function bind(targets, events, callback, options) {
-    forEachEvent(targets, events, (target, event2) => {
+    forEachEvent(targets, events, (target, event, namespace) => {
       const isEventTarget = "addEventListener" in target;
-      const remover = isEventTarget ? target.removeEventListener.bind(target, event2, callback, options) : target["removeListener"].bind(target, callback);
-      isEventTarget ? target.addEventListener(event2, callback, options) : target["addListener"](callback);
-      listeners.push([target, event2, callback, remover]);
+      const remover = isEventTarget ? target.removeEventListener.bind(target, event, callback, options) : target["removeListener"].bind(target, callback);
+      isEventTarget ? target.addEventListener(event, callback, options) : target["addListener"](callback);
+      listeners.push([target, event, namespace, callback, remover]);
     });
   }
   function unbind(targets, events, callback) {
-    forEachEvent(targets, events, (target, event2) => {
+    forEachEvent(targets, events, (target, event, namespace) => {
       listeners = listeners.filter((listener) => {
-        if (listener[0] === target && listener[1] === event2 && (!callback || listener[2] === callback)) {
-          listener[3]();
+        if (listener[0] === target && listener[1] === event && listener[2] === namespace && (!callback || listener[3] === callback)) {
+          listener[4]();
           return false;
         }
         return true;
       });
     });
   }
+  function dispatch(target, type, detail) {
+    let e;
+    const bubbles = true;
+    if (typeof CustomEvent === "function") {
+      e = new CustomEvent(type, { bubbles, detail });
+    } else {
+      e = document.createEvent("CustomEvent");
+      e.initEvent(type, bubbles, false);
+    }
+    target.dispatchEvent(e);
+    return e;
+  }
   function forEachEvent(targets, events, iteratee) {
     forEach(targets, (target) => {
-      if (target) {
-        events.split(" ").forEach(apply(iteratee, target));
-      }
+      target && forEach(events, (events2) => {
+        events2.split(" ").forEach((eventNS) => {
+          const fragment = eventNS.split(".");
+          iteratee(target, fragment[0], fragment[1]);
+        });
+      });
     });
   }
   function destroy() {
-    listeners = listeners.filter((data) => {
-      data[3]();
+    listeners.forEach((data) => {
+      data[4]();
     });
-    event.offBy(key);
+    empty(listeners);
   }
-  !manual && event.on(EVENT_DESTROY, destroy, key);
   return {
-    on,
-    off,
-    emit: event.emit,
     bind,
     unbind,
+    dispatch,
     destroy
   };
 }
 
+function EventInterface(Splide2) {
+  const bus = Splide2 ? Splide2.event.bus : document.createDocumentFragment();
+  const binder = EventBinder();
+  function on(events, callback) {
+    binder.bind(bus, toArray(events).join(" "), (e) => {
+      callback.apply(callback, isArray(e.detail) ? e.detail : []);
+    });
+  }
+  function emit(event) {
+    binder.dispatch(bus, event, slice(arguments, 1));
+  }
+  if (Splide2) {
+    Splide2.event.on(EVENT_DESTROY, binder.destroy);
+  }
+  return assign(binder, {
+    bus,
+    on,
+    off: apply(binder.unbind, bus),
+    emit
+  });
+}
+
 function RequestInterval(interval, onInterval, onUpdate, limit) {
   const { now } = Date;
   let startTime;
@@ -549,13 +519,13 @@ function Throttle(func, duration) {
 }
 
 function Media(Splide2, Components2, options) {
-  const event = EventInterface(Splide2, true);
+  const binder = EventBinder();
   const breakpoints = options.breakpoints || {};
-  const userOptions = merge({}, options);
+  const initialOptions = merge({}, options);
   const queries = [];
   function setup() {
     const isMin = options.mediaQuery === "min";
-    register(Object.keys(breakpoints).sort((n, m) => isMin ? +m - +n : +n - +m).map((key) => [breakpoints[key], `(${isMin ? "min" : "max"}-width:${key}px)`]).concat([[userOptions]]));
+    register(Object.keys(breakpoints).sort((n, m) => isMin ? +m - +n : +n - +m).map((key) => [breakpoints[key], `(${isMin ? "min" : "max"}-width:${key}px)`]));
     register([[{
       speed: 0,
       autoplay: "pause"
@@ -564,13 +534,13 @@ function Media(Splide2, Components2, options) {
   }
   function destroy(completely) {
     if (completely) {
-      event.destroy();
+      binder.destroy();
     }
   }
   function register(entries) {
     queries.push(entries.map((entry) => {
-      const query = entry[1] && matchMedia(entry[1]);
-      query && event.bind(query, "change", update);
+      const query = matchMedia(entry[1]);
+      binder.bind(query, "change", update);
       return [entry[0], query];
     }));
   }
@@ -578,7 +548,7 @@ function Media(Splide2, Components2, options) {
     const options2 = accumulate();
     const { destroy: _destroy } = options2;
     if (_destroy) {
-      Splide2.options = userOptions;
+      Splide2.options = initialOptions;
       Splide2.destroy(_destroy === "completely");
     } else if (Splide2.state.is(DESTROYED)) {
       destroy(true);
@@ -589,10 +559,10 @@ function Media(Splide2, Components2, options) {
   }
   function accumulate() {
     return queries.reduce((merged, entries) => {
-      const entry = find(entries, (entry2) => !entry2[1] || entry2[1].matches) || [];
-      entry[1] && event.emit(EVENT_MEDIA, entry[1]);
+      const entry = find(entries, (entry2) => entry2[1].matches) || [];
+      entry[1] && Splide2.emit(EVENT_MEDIA, entry[1]);
       return merge(merged, entry[0] || {});
-    }, merge({}, userOptions));
+    }, merge({}, initialOptions));
   }
   return {
     setup,
@@ -640,6 +610,7 @@ const DISABLED = "disabled";
 const ARIA_PREFIX = "aria-";
 const ARIA_CONTROLS = `${ARIA_PREFIX}controls`;
 const ARIA_CURRENT = `${ARIA_PREFIX}current`;
+const ARIA_SELECTED = `${ARIA_PREFIX}selected`;
 const ARIA_LABEL = `${ARIA_PREFIX}label`;
 const ARIA_HIDDEN = `${ARIA_PREFIX}hidden`;
 const ARIA_ORIENTATION = `${ARIA_PREFIX}orientation`;
@@ -679,7 +650,6 @@ const CLASS_AUTOPLAY = `${PROJECT_CODE}__autoplay`;
 const CLASS_PLAY = `${PROJECT_CODE}__play`;
 const CLASS_PAUSE = `${PROJECT_CODE}__pause`;
 const CLASS_SPINNER = `${PROJECT_CODE}__spinner`;
-const CLASS_SR = `${PROJECT_CODE}__sr`;
 const CLASS_INITIALIZED = "is-initialized";
 const CLASS_ACTIVE = "is-active";
 const CLASS_PREV = "is-prev";
@@ -757,7 +727,7 @@ function Elements(Splide2, Components2, options) {
     track.id = track.id || `${id}-track`;
     list.id = list.id || `${id}-list`;
     setAttribute(root, ARIA_ROLEDESCRIPTION, i18n.carousel);
-    setAttribute(root, ROLE, root.tagName !== "SECTION" && options.role || null);
+    setAttribute(root, ROLE, root.tagName !== "SECTION" && options.role || "");
     setAttribute(list, ROLE, "none");
   }
   function find(selector) {
@@ -786,18 +756,17 @@ const FADE = "fade";
 function Slide$1(Splide2, index, slideIndex, slide) {
   const { on, emit, bind, destroy: destroyEvents } = EventInterface(Splide2);
   const { Components, root, options } = Splide2;
-  const { isNavigation, updateOnMove, i18n } = options;
+  const { isNavigation, updateOnMove, i18n, pagination } = options;
   const { resolve } = Components.Direction;
   const styles = getAttribute(slide, "style");
   const isClone = slideIndex > -1;
   const container = child(slide, `.${CLASS_CONTAINER}`);
-  const focusableNodes = options.focusableNodes && queryAll(slide, options.focusableNodes);
   let destroyed;
   function mount() {
     if (!isClone) {
       slide.id = `${root.id}-slide${pad(index + 1)}`;
-      setAttribute(slide, ROLE, "group");
-      setAttribute(slide, ARIA_ROLEDESCRIPTION, i18n.slide);
+      setAttribute(slide, ROLE, pagination ? "tabpanel" : "group");
+      setAttribute(slide, ARIA_ROLEDESCRIPTION, pagination ? "" : i18n.slide);
       setAttribute(slide, ARIA_LABEL, format(i18n.slideLabel, [index + 1, Splide2.length]));
     }
     listen();
@@ -820,12 +789,12 @@ function Slide$1(Splide2, index, slideIndex, slide) {
     setAttribute(slide, "style", styles);
   }
   function initNavigation() {
-    const idx = isClone ? slideIndex : index;
-    const label = format(i18n.slideX, idx + 1);
-    const controls = Splide2.splides.map((target) => target.splide.root.id).join(" ");
-    setAttribute(slide, ARIA_LABEL, label);
+    const controls = Splide2.splides.map((target) => {
+      const Slide2 = target.splide.Components.Slides.getAt(index);
+      return Slide2 ? Slide2.slide.id : "";
+    }).join(" ");
+    setAttribute(slide, ARIA_LABEL, format(i18n.slideX, (isClone ? slideIndex : index) + 1));
     setAttribute(slide, ARIA_CONTROLS, controls);
-    setAttribute(slide, ROLE, "menuitem");
     updateActivity(isActive());
   }
   function onMove() {
@@ -835,32 +804,39 @@ function Slide$1(Splide2, index, slideIndex, slide) {
   }
   function update() {
     if (!destroyed) {
-      const { index: currIndex } = Splide2;
+      const { index: curr } = Splide2;
       updateActivity(isActive());
       updateVisibility(isVisible());
-      toggleClass(slide, CLASS_PREV, index === currIndex - 1);
-      toggleClass(slide, CLASS_NEXT, index === currIndex + 1);
+      toggleClass(slide, CLASS_PREV, index === curr - 1);
+      toggleClass(slide, CLASS_NEXT, index === curr + 1);
     }
   }
   function updateActivity(active) {
     if (active !== hasClass(slide, CLASS_ACTIVE)) {
       toggleClass(slide, CLASS_ACTIVE, active);
-      if (isNavigation) {
-        setAttribute(slide, ARIA_CURRENT, active || null);
-      }
+      setAttribute(slide, ARIA_CURRENT, isNavigation && active || "");
       emit(active ? EVENT_ACTIVE : EVENT_INACTIVE, self);
     }
   }
   function updateVisibility(visible) {
     const hidden = !visible && (!isActive() || isClone);
-    setAttribute(slide, ARIA_HIDDEN, hidden || null);
-    setAttribute(slide, TAB_INDEX, !hidden && options.slideFocus ? 0 : null);
-    setAttribute(focusableNodes || [], TAB_INDEX, hidden ? -1 : null);
+    if (document.activeElement === slide && hidden) {
+      nextTick(forwardFocus);
+    }
+    setAttribute(slide, ARIA_HIDDEN, hidden || "");
+    setAttribute(slide, TAB_INDEX, !hidden && options.slideFocus ? 0 : "");
+    setAttribute(queryAll(slide, options.focusableNodes || ""), TAB_INDEX, hidden ? -1 : "");
     if (visible !== hasClass(slide, CLASS_VISIBLE)) {
       toggleClass(slide, CLASS_VISIBLE, visible);
       emit(visible ? EVENT_VISIBLE : EVENT_HIDDEN, self);
     }
   }
+  function forwardFocus() {
+    const Slide2 = Components.Slides.getAt(Splide2.index);
+    if (Slide2) {
+      focus(Slide2.slide);
+    }
+  }
   function style$1(prop, value, useContainer) {
     style(useContainer && container || slide, prop, value);
   }
@@ -1163,10 +1139,9 @@ function Clones(Splide2, Components2, options) {
     if (!Splide2.is(LOOP)) {
       clones2 = 0;
     } else if (!clones2) {
-      const fixedSize = measure(Elements.list, options[resolve("fixedWidth")]);
+      const fixedSize = options[resolve("fixedWidth")] && Components2.Layout.slideSize(0);
       const fixedCount = fixedSize && ceil(rect(Elements.track)[resolve("width")] / fixedSize);
-      const baseCount = fixedCount || options[resolve("autoWidth")] && Splide2.length || options.perPage;
-      clones2 = baseCount * (options.drag ? (options.flickMaxPages || 1) + 1 : 2);
+      clones2 = fixedCount || options[resolve("autoWidth")] && Splide2.length || options.perPage;
     }
     return clones2;
   }
@@ -1898,7 +1873,18 @@ function Drag(Splide2, Components2, options) {
   };
 }
 
-const IE_ARROW_KEYS = ["Left", "Right", "Up", "Down"];
+const NORMALIZATION_MAP = {
+  Spacebar: " ",
+  Right: "ArrowRight",
+  Left: "ArrowLeft",
+  Up: "ArrowUp",
+  Down: "ArrowDown"
+};
+function normalizeKey(key) {
+  key = isString(key) ? key : key.key;
+  return NORMALIZATION_MAP[key] || key;
+}
+
 const KEYBOARD_EVENT = "keydown";
 function Keyboard(Splide2, Components2, options) {
   const { on, bind, unbind } = EventInterface(Splide2);
@@ -1908,7 +1894,8 @@ function Keyboard(Splide2, Components2, options) {
   let disabled;
   function mount() {
     init();
-    on(EVENT_UPDATED, onUpdated);
+    on(EVENT_UPDATED, destroy);
+    on(EVENT_UPDATED, init);
     on(EVENT_MOVE, onMove);
   }
   function init() {
@@ -1936,17 +1923,12 @@ function Keyboard(Splide2, Components2, options) {
       disabled = _disabled;
     });
   }
-  function onUpdated() {
-    destroy();
-    init();
-  }
   function onKeydown(e) {
     if (!disabled) {
-      const { key } = e;
-      const normalizedKey = includes(IE_ARROW_KEYS, key) ? `Arrow${key}` : key;
-      if (normalizedKey === resolve("ArrowLeft")) {
+      const key = normalizeKey(e);
+      if (key === resolve("ArrowLeft")) {
         Splide2.go("<");
-      } else if (normalizedKey === resolve("ArrowRight")) {
+      } else if (key === resolve("ArrowRight")) {
         Splide2.go(">");
       }
     }
@@ -2052,7 +2034,8 @@ function LazyLoad(Splide2, Components2, options) {
 function Pagination(Splide2, Components2, options) {
   const { on, emit, bind, unbind } = EventInterface(Splide2);
   const { Slides, Elements, Controller } = Components2;
-  const { hasFocus, getIndex } = Controller;
+  const { hasFocus, getIndex, go } = Controller;
+  const { resolve } = Components2.Direction;
   const items = [];
   let list;
   function mount() {
@@ -2072,7 +2055,7 @@ function Pagination(Splide2, Components2, options) {
     if (list) {
       remove(list);
       items.forEach((item) => {
-        unbind(item.button, "click");
+        unbind(item.button, "click keydown focus");
       });
       empty(items);
       list = null;
@@ -2084,21 +2067,46 @@ function Pagination(Splide2, Components2, options) {
     const parent = options.pagination === "slider" && Elements.slider || Elements.root;
     const max = hasFocus() ? length : ceil(length / perPage);
     list = create("ul", classes.pagination, parent);
+    setAttribute(list, ROLE, "tablist");
+    setAttribute(list, ARIA_LABEL, i18n.select);
+    setAttribute(list, ARIA_ORIENTATION, options.direction === TTB ? "vertical" : "");
     for (let i = 0; i < max; i++) {
       const li = create("li", null, list);
       const button = create("button", { class: classes.page, type: "button" }, li);
+      const controls = Slides.getIn(i).map((Slide) => Slide.slide.id);
       const text = !hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
       bind(button, "click", apply(onClick, i));
-      setAttribute(button, ARIA_CONTROLS, Components2.Elements.list.id);
+      bind(button, "keydown", apply(onKeydown, i));
+      setAttribute(li, ROLE, "none");
+      setAttribute(button, ROLE, "tab");
+      setAttribute(button, ARIA_CONTROLS, controls.join(" "));
       setAttribute(button, ARIA_LABEL, format(text, i + 1));
+      setAttribute(button, TAB_INDEX, -1);
       items.push({ li, button, page: i });
     }
   }
   function onClick(page) {
-    Controller.go(`>${page}`, true, () => {
-      const Slide = Slides.getAt(Controller.toIndex(page));
-      Slide && focus(Slide.slide);
-    });
+    go(`>${page}`, true);
+  }
+  function onKeydown(page, e) {
+    const { length } = items;
+    const key = normalizeKey(e);
+    let nextPage = -1;
+    if (key === resolve("ArrowRight")) {
+      nextPage = ++page % length;
+    } else if (key === resolve("ArrowLeft")) {
+      nextPage = (--page + length) % length;
+    } else if (key === "Home") {
+      nextPage = 0;
+    } else if (key === "End") {
+      nextPage = length - 1;
+    }
+    const item = items[nextPage];
+    if (item) {
+      focus(item.button);
+      go(`>${nextPage}`);
+      prevent(e, true);
+    }
   }
   function getAt(index) {
     return items[Controller.toPage(index)];
@@ -2107,12 +2115,16 @@ function Pagination(Splide2, Components2, options) {
     const prev = getAt(getIndex(true));
     const curr = getAt(getIndex());
     if (prev) {
-      removeClass(prev.button, CLASS_ACTIVE);
-      removeAttribute(prev.button, ARIA_CURRENT);
+      const { button } = prev;
+      removeClass(button, CLASS_ACTIVE);
+      removeAttribute(button, ARIA_SELECTED);
+      setAttribute(button, TAB_INDEX, -1);
     }
     if (curr) {
-      addClass(curr.button, CLASS_ACTIVE);
-      setAttribute(curr.button, ARIA_CURRENT, true);
+      const { button } = curr;
+      addClass(button, CLASS_ACTIVE);
+      setAttribute(button, ARIA_SELECTED, true);
+      setAttribute(button, TAB_INDEX, "");
     }
     emit(EVENT_PAGINATION_UPDATED, { list, items }, prev, curr);
   }
@@ -2125,7 +2137,7 @@ function Pagination(Splide2, Components2, options) {
   };
 }
 
-const TRIGGER_KEYS = [" ", "Enter", "Spacebar"];
+const TRIGGER_KEYS = [" ", "Enter"];
 function Sync(Splide2, Components2, options) {
   const { list } = Components2.Elements;
   const events = [];
@@ -2138,7 +2150,6 @@ function Sync(Splide2, Components2, options) {
     }
   }
   function destroy() {
-    removeAttribute(list, ALL_ATTRIBUTES);
     events.forEach((event) => {
       event.destroy();
     });
@@ -2164,18 +2175,17 @@ function Sync(Splide2, Components2, options) {
     on(EVENT_CLICK, onClick);
     on(EVENT_SLIDE_KEYDOWN, onKeydown);
     on([EVENT_MOUNTED, EVENT_UPDATED], update);
-    setAttribute(list, ROLE, "menu");
     events.push(event);
     event.emit(EVENT_NAVIGATION_MOUNTED, Splide2.splides);
   }
   function update() {
-    setAttribute(list, ARIA_ORIENTATION, options.direction !== TTB ? "horizontal" : null);
+    setAttribute(list, ARIA_ORIENTATION, options.direction === TTB ? "vertical" : "");
   }
   function onClick(Slide) {
     Splide2.go(Slide.index);
   }
   function onKeydown(Slide, e) {
-    if (includes(TRIGGER_KEYS, e.key)) {
+    if (includes(TRIGGER_KEYS, normalizeKey(e))) {
       onClick(Slide);
       prevent(e);
     }
@@ -2221,8 +2231,9 @@ function Live(Splide2, Components2, options) {
   const { on } = EventInterface(Splide2);
   const { list } = Components2.Elements;
   const { live } = options;
+  const enabled = live && !options.isNavigation;
   function mount() {
-    if (live) {
+    if (enabled) {
       setAttribute(list, ARIA_ATOMIC, false);
       disable(!Components2.Autoplay.isPaused());
       on(EVENT_AUTOPLAY_PLAY, apply(disable, true));
@@ -2230,7 +2241,7 @@ function Live(Splide2, Components2, options) {
     }
   }
   function disable(disabled) {
-    if (live) {
+    if (enabled) {
       setAttribute(list, ARIA_LIVE, disabled ? "off" : "polite");
     }
   }
@@ -2274,6 +2285,7 @@ const I18N = {
   pause: "Pause autoplay",
   carousel: "carousel",
   slide: "slide",
+  select: "Select slide to show",
   slideLabel: "%s of %s"
 };
 
@@ -2297,6 +2309,7 @@ const DEFAULTS = {
   slideFocus: true,
   trimSpace: true,
   focusableNodes: "a, button, textarea, input, select, iframe",
+  live: true,
   classes: CLASSES,
   i18n: I18N
 };
@@ -2377,7 +2390,7 @@ function Slide(Splide2, Components2, options) {
 
 const _Splide = class {
   constructor(target, options) {
-    this.event = EventBus();
+    this.event = EventInterface();
     this.Components = {};
     this.state = State(CREATED);
     this.splides = [];
@@ -2386,12 +2399,13 @@ const _Splide = class {
     const root = isString(target) ? query(document, target) : target;
     assert(root, `${root} is invalid.`);
     this.root = root;
-    merge(this._options, DEFAULTS, _Splide.defaults, options || {});
+    options = merge({}, DEFAULTS, _Splide.defaults, options || {});
     try {
-      merge(this._options, JSON.parse(getAttribute(root, DATA_ATTRIBUTE)));
+      merge(options, JSON.parse(getAttribute(root, DATA_ATTRIBUTE)));
     } catch (e) {
       assert(false, "Invalid JSON");
     }
+    this._options = options;
   }
   mount(Extensions, Transition) {
     const { state, Components: Components2 } = this;
@@ -2429,7 +2443,7 @@ const _Splide = class {
     return this;
   }
   on(events, callback) {
-    this.event.on(events, callback, null, DEFAULT_USER_EVENT_PRIORITY);
+    this.event.on(events, callback);
     return this;
   }
   off(events) {
@@ -2458,7 +2472,7 @@ const _Splide = class {
   destroy(completely = true) {
     const { event, state } = this;
     if (state.is(CREATED)) {
-      event.on(EVENT_READY, this.destroy.bind(this, completely), this);
+      EventInterface(this).on(EVENT_READY, this.destroy.bind(this, completely));
     } else {
       forOwn(this._Components, (component) => {
         component.destroy && component.destroy(completely);
@@ -2882,7 +2896,6 @@ exports.CLASS_ROOT = CLASS_ROOT;
 exports.CLASS_SLIDE = CLASS_SLIDE;
 exports.CLASS_SLIDER = CLASS_SLIDER;
 exports.CLASS_SPINNER = CLASS_SPINNER;
-exports.CLASS_SR = CLASS_SR;
 exports.CLASS_TRACK = CLASS_TRACK;
 exports.CLASS_VISIBLE = CLASS_VISIBLE;
 exports.EVENT_ACTIVE = EVENT_ACTIVE;
@@ -2917,7 +2930,7 @@ exports.EVENT_SHIFTED = EVENT_SHIFTED;
 exports.EVENT_SLIDE_KEYDOWN = EVENT_SLIDE_KEYDOWN;
 exports.EVENT_UPDATED = EVENT_UPDATED;
 exports.EVENT_VISIBLE = EVENT_VISIBLE;
-exports.EventBus = EventBus;
+exports.EventBinder = EventBinder;
 exports.EventInterface = EventInterface;
 exports.RequestInterval = RequestInterval;
 exports.STATUS_CLASSES = STATUS_CLASSES;

+ 165 - 151
dist/js/splide.esm.js

@@ -52,9 +52,6 @@ const STATES = {
   DESTROYED
 };
 
-const DEFAULT_EVENT_PRIORITY = 10;
-const DEFAULT_USER_EVENT_PRIORITY = 20;
-
 function empty(array) {
   array.length = 0;
 }
@@ -208,7 +205,7 @@ function setAttribute(elms, attrs, value) {
     });
   } else {
     forEach(elms, (elm) => {
-      isNull(value) ? removeAttribute(elm, attrs) : elm.setAttribute(attrs, String(value));
+      isNull(value) || value === "" ? removeAttribute(elm, attrs) : elm.setAttribute(attrs, String(value));
     });
   }
 }
@@ -263,15 +260,6 @@ function remove(nodes) {
   });
 }
 
-function measure(parent, value) {
-  if (isString(value)) {
-    const div = create("div", { style: `width: ${value}; position: absolute;` }, parent);
-    value = rect(div).width;
-    remove(div);
-  }
-  return value;
-}
-
 function parseHtml(html) {
   return child(new DOMParser().parseFromString(html, "text/html").body);
 }
@@ -289,7 +277,7 @@ function query(parent, selector) {
 }
 
 function queryAll(parent, selector) {
-  return slice(parent.querySelectorAll(selector));
+  return selector ? slice(parent.querySelectorAll(selector)) : [];
 }
 
 function removeClass(elm, classes) {
@@ -355,103 +343,85 @@ function uniqueId(prefix) {
   return `${prefix}${pad(ids[prefix] = (ids[prefix] || 0) + 1)}`;
 }
 
-function EventBus() {
-  let handlers = {};
-  function on(events, callback, key, priority = DEFAULT_EVENT_PRIORITY) {
-    forEachEvent(events, (event, namespace) => {
-      const events2 = handlers[event] = handlers[event] || [];
-      events2.push([callback, namespace, priority, key]);
-      events2.sort((handler1, handler2) => handler1[2] - handler2[2]);
-    });
-  }
-  function off(events, key) {
-    forEachEvent(events, (event, namespace) => {
-      handlers[event] = (handlers[event] || []).filter((handler) => {
-        return handler[3] ? handler[3] !== key : key || handler[1] !== namespace;
-      });
-    });
-  }
-  function offBy(key) {
-    forOwn(handlers, (eventHandlers, event) => {
-      off(event, key);
-    });
-  }
-  function emit(event) {
-    (handlers[event] || []).forEach((handler) => {
-      handler[0].apply(handler, slice(arguments, 1));
-    });
-  }
-  function destroy() {
-    handlers = {};
-  }
-  function forEachEvent(events, iteratee) {
-    toArray(events).join(" ").split(" ").forEach((eventNS) => {
-      const fragments = eventNS.split(".");
-      iteratee(fragments[0], fragments[1]);
-    });
-  }
-  return {
-    on,
-    off,
-    offBy,
-    emit,
-    destroy
-  };
-}
-
-function EventInterface(Splide2, manual) {
-  const { event } = Splide2;
-  const key = {};
+function EventBinder() {
   let listeners = [];
-  function on(events, callback, priority) {
-    event.on(events, callback, key, priority);
-  }
-  function off(events) {
-    event.off(events, key);
-  }
   function bind(targets, events, callback, options) {
-    forEachEvent(targets, events, (target, event2) => {
+    forEachEvent(targets, events, (target, event, namespace) => {
       const isEventTarget = "addEventListener" in target;
-      const remover = isEventTarget ? target.removeEventListener.bind(target, event2, callback, options) : target["removeListener"].bind(target, callback);
-      isEventTarget ? target.addEventListener(event2, callback, options) : target["addListener"](callback);
-      listeners.push([target, event2, callback, remover]);
+      const remover = isEventTarget ? target.removeEventListener.bind(target, event, callback, options) : target["removeListener"].bind(target, callback);
+      isEventTarget ? target.addEventListener(event, callback, options) : target["addListener"](callback);
+      listeners.push([target, event, namespace, callback, remover]);
     });
   }
   function unbind(targets, events, callback) {
-    forEachEvent(targets, events, (target, event2) => {
+    forEachEvent(targets, events, (target, event, namespace) => {
       listeners = listeners.filter((listener) => {
-        if (listener[0] === target && listener[1] === event2 && (!callback || listener[2] === callback)) {
-          listener[3]();
+        if (listener[0] === target && listener[1] === event && listener[2] === namespace && (!callback || listener[3] === callback)) {
+          listener[4]();
           return false;
         }
         return true;
       });
     });
   }
+  function dispatch(target, type, detail) {
+    let e;
+    const bubbles = true;
+    if (typeof CustomEvent === "function") {
+      e = new CustomEvent(type, { bubbles, detail });
+    } else {
+      e = document.createEvent("CustomEvent");
+      e.initEvent(type, bubbles, false);
+    }
+    target.dispatchEvent(e);
+    return e;
+  }
   function forEachEvent(targets, events, iteratee) {
     forEach(targets, (target) => {
-      if (target) {
-        events.split(" ").forEach(apply(iteratee, target));
-      }
+      target && forEach(events, (events2) => {
+        events2.split(" ").forEach((eventNS) => {
+          const fragment = eventNS.split(".");
+          iteratee(target, fragment[0], fragment[1]);
+        });
+      });
     });
   }
   function destroy() {
-    listeners = listeners.filter((data) => {
-      data[3]();
+    listeners.forEach((data) => {
+      data[4]();
     });
-    event.offBy(key);
+    empty(listeners);
   }
-  !manual && event.on(EVENT_DESTROY, destroy, key);
   return {
-    on,
-    off,
-    emit: event.emit,
     bind,
     unbind,
+    dispatch,
     destroy
   };
 }
 
+function EventInterface(Splide2) {
+  const bus = Splide2 ? Splide2.event.bus : document.createDocumentFragment();
+  const binder = EventBinder();
+  function on(events, callback) {
+    binder.bind(bus, toArray(events).join(" "), (e) => {
+      callback.apply(callback, isArray(e.detail) ? e.detail : []);
+    });
+  }
+  function emit(event) {
+    binder.dispatch(bus, event, slice(arguments, 1));
+  }
+  if (Splide2) {
+    Splide2.event.on(EVENT_DESTROY, binder.destroy);
+  }
+  return assign(binder, {
+    bus,
+    on,
+    off: apply(binder.unbind, bus),
+    emit
+  });
+}
+
 function RequestInterval(interval, onInterval, onUpdate, limit) {
   const { now } = Date;
   let startTime;
@@ -545,13 +515,13 @@ function Throttle(func, duration) {
 }
 
 function Media(Splide2, Components2, options) {
-  const event = EventInterface(Splide2, true);
+  const binder = EventBinder();
   const breakpoints = options.breakpoints || {};
-  const userOptions = merge({}, options);
+  const initialOptions = merge({}, options);
   const queries = [];
   function setup() {
     const isMin = options.mediaQuery === "min";
-    register(Object.keys(breakpoints).sort((n, m) => isMin ? +m - +n : +n - +m).map((key) => [breakpoints[key], `(${isMin ? "min" : "max"}-width:${key}px)`]).concat([[userOptions]]));
+    register(Object.keys(breakpoints).sort((n, m) => isMin ? +m - +n : +n - +m).map((key) => [breakpoints[key], `(${isMin ? "min" : "max"}-width:${key}px)`]));
     register([[{
       speed: 0,
       autoplay: "pause"
@@ -560,13 +530,13 @@ function Media(Splide2, Components2, options) {
   }
   function destroy(completely) {
     if (completely) {
-      event.destroy();
+      binder.destroy();
     }
   }
   function register(entries) {
     queries.push(entries.map((entry) => {
-      const query = entry[1] && matchMedia(entry[1]);
-      query && event.bind(query, "change", update);
+      const query = matchMedia(entry[1]);
+      binder.bind(query, "change", update);
       return [entry[0], query];
     }));
   }
@@ -574,7 +544,7 @@ function Media(Splide2, Components2, options) {
     const options2 = accumulate();
     const { destroy: _destroy } = options2;
     if (_destroy) {
-      Splide2.options = userOptions;
+      Splide2.options = initialOptions;
       Splide2.destroy(_destroy === "completely");
     } else if (Splide2.state.is(DESTROYED)) {
       destroy(true);
@@ -585,10 +555,10 @@ function Media(Splide2, Components2, options) {
   }
   function accumulate() {
     return queries.reduce((merged, entries) => {
-      const entry = find(entries, (entry2) => !entry2[1] || entry2[1].matches) || [];
-      entry[1] && event.emit(EVENT_MEDIA, entry[1]);
+      const entry = find(entries, (entry2) => entry2[1].matches) || [];
+      entry[1] && Splide2.emit(EVENT_MEDIA, entry[1]);
       return merge(merged, entry[0] || {});
-    }, merge({}, userOptions));
+    }, merge({}, initialOptions));
   }
   return {
     setup,
@@ -636,6 +606,7 @@ const DISABLED = "disabled";
 const ARIA_PREFIX = "aria-";
 const ARIA_CONTROLS = `${ARIA_PREFIX}controls`;
 const ARIA_CURRENT = `${ARIA_PREFIX}current`;
+const ARIA_SELECTED = `${ARIA_PREFIX}selected`;
 const ARIA_LABEL = `${ARIA_PREFIX}label`;
 const ARIA_HIDDEN = `${ARIA_PREFIX}hidden`;
 const ARIA_ORIENTATION = `${ARIA_PREFIX}orientation`;
@@ -675,7 +646,6 @@ const CLASS_AUTOPLAY = `${PROJECT_CODE}__autoplay`;
 const CLASS_PLAY = `${PROJECT_CODE}__play`;
 const CLASS_PAUSE = `${PROJECT_CODE}__pause`;
 const CLASS_SPINNER = `${PROJECT_CODE}__spinner`;
-const CLASS_SR = `${PROJECT_CODE}__sr`;
 const CLASS_INITIALIZED = "is-initialized";
 const CLASS_ACTIVE = "is-active";
 const CLASS_PREV = "is-prev";
@@ -753,7 +723,7 @@ function Elements(Splide2, Components2, options) {
     track.id = track.id || `${id}-track`;
     list.id = list.id || `${id}-list`;
     setAttribute(root, ARIA_ROLEDESCRIPTION, i18n.carousel);
-    setAttribute(root, ROLE, root.tagName !== "SECTION" && options.role || null);
+    setAttribute(root, ROLE, root.tagName !== "SECTION" && options.role || "");
     setAttribute(list, ROLE, "none");
   }
   function find(selector) {
@@ -782,18 +752,17 @@ const FADE = "fade";
 function Slide$1(Splide2, index, slideIndex, slide) {
   const { on, emit, bind, destroy: destroyEvents } = EventInterface(Splide2);
   const { Components, root, options } = Splide2;
-  const { isNavigation, updateOnMove, i18n } = options;
+  const { isNavigation, updateOnMove, i18n, pagination } = options;
   const { resolve } = Components.Direction;
   const styles = getAttribute(slide, "style");
   const isClone = slideIndex > -1;
   const container = child(slide, `.${CLASS_CONTAINER}`);
-  const focusableNodes = options.focusableNodes && queryAll(slide, options.focusableNodes);
   let destroyed;
   function mount() {
     if (!isClone) {
       slide.id = `${root.id}-slide${pad(index + 1)}`;
-      setAttribute(slide, ROLE, "group");
-      setAttribute(slide, ARIA_ROLEDESCRIPTION, i18n.slide);
+      setAttribute(slide, ROLE, pagination ? "tabpanel" : "group");
+      setAttribute(slide, ARIA_ROLEDESCRIPTION, pagination ? "" : i18n.slide);
       setAttribute(slide, ARIA_LABEL, format(i18n.slideLabel, [index + 1, Splide2.length]));
     }
     listen();
@@ -816,12 +785,12 @@ function Slide$1(Splide2, index, slideIndex, slide) {
     setAttribute(slide, "style", styles);
   }
   function initNavigation() {
-    const idx = isClone ? slideIndex : index;
-    const label = format(i18n.slideX, idx + 1);
-    const controls = Splide2.splides.map((target) => target.splide.root.id).join(" ");
-    setAttribute(slide, ARIA_LABEL, label);
+    const controls = Splide2.splides.map((target) => {
+      const Slide2 = target.splide.Components.Slides.getAt(index);
+      return Slide2 ? Slide2.slide.id : "";
+    }).join(" ");
+    setAttribute(slide, ARIA_LABEL, format(i18n.slideX, (isClone ? slideIndex : index) + 1));
     setAttribute(slide, ARIA_CONTROLS, controls);
-    setAttribute(slide, ROLE, "menuitem");
     updateActivity(isActive());
   }
   function onMove() {
@@ -831,32 +800,39 @@ function Slide$1(Splide2, index, slideIndex, slide) {
   }
   function update() {
     if (!destroyed) {
-      const { index: currIndex } = Splide2;
+      const { index: curr } = Splide2;
       updateActivity(isActive());
       updateVisibility(isVisible());
-      toggleClass(slide, CLASS_PREV, index === currIndex - 1);
-      toggleClass(slide, CLASS_NEXT, index === currIndex + 1);
+      toggleClass(slide, CLASS_PREV, index === curr - 1);
+      toggleClass(slide, CLASS_NEXT, index === curr + 1);
     }
   }
   function updateActivity(active) {
     if (active !== hasClass(slide, CLASS_ACTIVE)) {
       toggleClass(slide, CLASS_ACTIVE, active);
-      if (isNavigation) {
-        setAttribute(slide, ARIA_CURRENT, active || null);
-      }
+      setAttribute(slide, ARIA_CURRENT, isNavigation && active || "");
       emit(active ? EVENT_ACTIVE : EVENT_INACTIVE, self);
     }
   }
   function updateVisibility(visible) {
     const hidden = !visible && (!isActive() || isClone);
-    setAttribute(slide, ARIA_HIDDEN, hidden || null);
-    setAttribute(slide, TAB_INDEX, !hidden && options.slideFocus ? 0 : null);
-    setAttribute(focusableNodes || [], TAB_INDEX, hidden ? -1 : null);
+    if (document.activeElement === slide && hidden) {
+      nextTick(forwardFocus);
+    }
+    setAttribute(slide, ARIA_HIDDEN, hidden || "");
+    setAttribute(slide, TAB_INDEX, !hidden && options.slideFocus ? 0 : "");
+    setAttribute(queryAll(slide, options.focusableNodes || ""), TAB_INDEX, hidden ? -1 : "");
     if (visible !== hasClass(slide, CLASS_VISIBLE)) {
       toggleClass(slide, CLASS_VISIBLE, visible);
       emit(visible ? EVENT_VISIBLE : EVENT_HIDDEN, self);
     }
   }
+  function forwardFocus() {
+    const Slide2 = Components.Slides.getAt(Splide2.index);
+    if (Slide2) {
+      focus(Slide2.slide);
+    }
+  }
   function style$1(prop, value, useContainer) {
     style(useContainer && container || slide, prop, value);
   }
@@ -1159,10 +1135,9 @@ function Clones(Splide2, Components2, options) {
     if (!Splide2.is(LOOP)) {
       clones2 = 0;
     } else if (!clones2) {
-      const fixedSize = measure(Elements.list, options[resolve("fixedWidth")]);
+      const fixedSize = options[resolve("fixedWidth")] && Components2.Layout.slideSize(0);
       const fixedCount = fixedSize && ceil(rect(Elements.track)[resolve("width")] / fixedSize);
-      const baseCount = fixedCount || options[resolve("autoWidth")] && Splide2.length || options.perPage;
-      clones2 = baseCount * (options.drag ? (options.flickMaxPages || 1) + 1 : 2);
+      clones2 = fixedCount || options[resolve("autoWidth")] && Splide2.length || options.perPage;
     }
     return clones2;
   }
@@ -1894,7 +1869,18 @@ function Drag(Splide2, Components2, options) {
   };
 }
 
-const IE_ARROW_KEYS = ["Left", "Right", "Up", "Down"];
+const NORMALIZATION_MAP = {
+  Spacebar: " ",
+  Right: "ArrowRight",
+  Left: "ArrowLeft",
+  Up: "ArrowUp",
+  Down: "ArrowDown"
+};
+function normalizeKey(key) {
+  key = isString(key) ? key : key.key;
+  return NORMALIZATION_MAP[key] || key;
+}
+
 const KEYBOARD_EVENT = "keydown";
 function Keyboard(Splide2, Components2, options) {
   const { on, bind, unbind } = EventInterface(Splide2);
@@ -1904,7 +1890,8 @@ function Keyboard(Splide2, Components2, options) {
   let disabled;
   function mount() {
     init();
-    on(EVENT_UPDATED, onUpdated);
+    on(EVENT_UPDATED, destroy);
+    on(EVENT_UPDATED, init);
     on(EVENT_MOVE, onMove);
   }
   function init() {
@@ -1932,17 +1919,12 @@ function Keyboard(Splide2, Components2, options) {
       disabled = _disabled;
     });
   }
-  function onUpdated() {
-    destroy();
-    init();
-  }
   function onKeydown(e) {
     if (!disabled) {
-      const { key } = e;
-      const normalizedKey = includes(IE_ARROW_KEYS, key) ? `Arrow${key}` : key;
-      if (normalizedKey === resolve("ArrowLeft")) {
+      const key = normalizeKey(e);
+      if (key === resolve("ArrowLeft")) {
         Splide2.go("<");
-      } else if (normalizedKey === resolve("ArrowRight")) {
+      } else if (key === resolve("ArrowRight")) {
         Splide2.go(">");
       }
     }
@@ -2048,7 +2030,8 @@ function LazyLoad(Splide2, Components2, options) {
 function Pagination(Splide2, Components2, options) {
   const { on, emit, bind, unbind } = EventInterface(Splide2);
   const { Slides, Elements, Controller } = Components2;
-  const { hasFocus, getIndex } = Controller;
+  const { hasFocus, getIndex, go } = Controller;
+  const { resolve } = Components2.Direction;
   const items = [];
   let list;
   function mount() {
@@ -2068,7 +2051,7 @@ function Pagination(Splide2, Components2, options) {
     if (list) {
       remove(list);
       items.forEach((item) => {
-        unbind(item.button, "click");
+        unbind(item.button, "click keydown focus");
       });
       empty(items);
       list = null;
@@ -2080,21 +2063,46 @@ function Pagination(Splide2, Components2, options) {
     const parent = options.pagination === "slider" && Elements.slider || Elements.root;
     const max = hasFocus() ? length : ceil(length / perPage);
     list = create("ul", classes.pagination, parent);
+    setAttribute(list, ROLE, "tablist");
+    setAttribute(list, ARIA_LABEL, i18n.select);
+    setAttribute(list, ARIA_ORIENTATION, options.direction === TTB ? "vertical" : "");
     for (let i = 0; i < max; i++) {
       const li = create("li", null, list);
       const button = create("button", { class: classes.page, type: "button" }, li);
+      const controls = Slides.getIn(i).map((Slide) => Slide.slide.id);
       const text = !hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX;
       bind(button, "click", apply(onClick, i));
-      setAttribute(button, ARIA_CONTROLS, Components2.Elements.list.id);
+      bind(button, "keydown", apply(onKeydown, i));
+      setAttribute(li, ROLE, "none");
+      setAttribute(button, ROLE, "tab");
+      setAttribute(button, ARIA_CONTROLS, controls.join(" "));
       setAttribute(button, ARIA_LABEL, format(text, i + 1));
+      setAttribute(button, TAB_INDEX, -1);
       items.push({ li, button, page: i });
     }
   }
   function onClick(page) {
-    Controller.go(`>${page}`, true, () => {
-      const Slide = Slides.getAt(Controller.toIndex(page));
-      Slide && focus(Slide.slide);
-    });
+    go(`>${page}`, true);
+  }
+  function onKeydown(page, e) {
+    const { length } = items;
+    const key = normalizeKey(e);
+    let nextPage = -1;
+    if (key === resolve("ArrowRight")) {
+      nextPage = ++page % length;
+    } else if (key === resolve("ArrowLeft")) {
+      nextPage = (--page + length) % length;
+    } else if (key === "Home") {
+      nextPage = 0;
+    } else if (key === "End") {
+      nextPage = length - 1;
+    }
+    const item = items[nextPage];
+    if (item) {
+      focus(item.button);
+      go(`>${nextPage}`);
+      prevent(e, true);
+    }
   }
   function getAt(index) {
     return items[Controller.toPage(index)];
@@ -2103,12 +2111,16 @@ function Pagination(Splide2, Components2, options) {
     const prev = getAt(getIndex(true));
     const curr = getAt(getIndex());
     if (prev) {
-      removeClass(prev.button, CLASS_ACTIVE);
-      removeAttribute(prev.button, ARIA_CURRENT);
+      const { button } = prev;
+      removeClass(button, CLASS_ACTIVE);
+      removeAttribute(button, ARIA_SELECTED);
+      setAttribute(button, TAB_INDEX, -1);
     }
     if (curr) {
-      addClass(curr.button, CLASS_ACTIVE);
-      setAttribute(curr.button, ARIA_CURRENT, true);
+      const { button } = curr;
+      addClass(button, CLASS_ACTIVE);
+      setAttribute(button, ARIA_SELECTED, true);
+      setAttribute(button, TAB_INDEX, "");
     }
     emit(EVENT_PAGINATION_UPDATED, { list, items }, prev, curr);
   }
@@ -2121,7 +2133,7 @@ function Pagination(Splide2, Components2, options) {
   };
 }
 
-const TRIGGER_KEYS = [" ", "Enter", "Spacebar"];
+const TRIGGER_KEYS = [" ", "Enter"];
 function Sync(Splide2, Components2, options) {
   const { list } = Components2.Elements;
   const events = [];
@@ -2134,7 +2146,6 @@ function Sync(Splide2, Components2, options) {
     }
   }
   function destroy() {
-    removeAttribute(list, ALL_ATTRIBUTES);
     events.forEach((event) => {
       event.destroy();
     });
@@ -2160,18 +2171,17 @@ function Sync(Splide2, Components2, options) {
     on(EVENT_CLICK, onClick);
     on(EVENT_SLIDE_KEYDOWN, onKeydown);
     on([EVENT_MOUNTED, EVENT_UPDATED], update);
-    setAttribute(list, ROLE, "menu");
     events.push(event);
     event.emit(EVENT_NAVIGATION_MOUNTED, Splide2.splides);
   }
   function update() {
-    setAttribute(list, ARIA_ORIENTATION, options.direction !== TTB ? "horizontal" : null);
+    setAttribute(list, ARIA_ORIENTATION, options.direction === TTB ? "vertical" : "");
   }
   function onClick(Slide) {
     Splide2.go(Slide.index);
   }
   function onKeydown(Slide, e) {
-    if (includes(TRIGGER_KEYS, e.key)) {
+    if (includes(TRIGGER_KEYS, normalizeKey(e))) {
       onClick(Slide);
       prevent(e);
     }
@@ -2217,8 +2227,9 @@ function Live(Splide2, Components2, options) {
   const { on } = EventInterface(Splide2);
   const { list } = Components2.Elements;
   const { live } = options;
+  const enabled = live && !options.isNavigation;
   function mount() {
-    if (live) {
+    if (enabled) {
       setAttribute(list, ARIA_ATOMIC, false);
       disable(!Components2.Autoplay.isPaused());
       on(EVENT_AUTOPLAY_PLAY, apply(disable, true));
@@ -2226,7 +2237,7 @@ function Live(Splide2, Components2, options) {
     }
   }
   function disable(disabled) {
-    if (live) {
+    if (enabled) {
       setAttribute(list, ARIA_LIVE, disabled ? "off" : "polite");
     }
   }
@@ -2270,6 +2281,7 @@ const I18N = {
   pause: "Pause autoplay",
   carousel: "carousel",
   slide: "slide",
+  select: "Select slide to show",
   slideLabel: "%s of %s"
 };
 
@@ -2293,6 +2305,7 @@ const DEFAULTS = {
   slideFocus: true,
   trimSpace: true,
   focusableNodes: "a, button, textarea, input, select, iframe",
+  live: true,
   classes: CLASSES,
   i18n: I18N
 };
@@ -2373,7 +2386,7 @@ function Slide(Splide2, Components2, options) {
 
 const _Splide = class {
   constructor(target, options) {
-    this.event = EventBus();
+    this.event = EventInterface();
     this.Components = {};
     this.state = State(CREATED);
     this.splides = [];
@@ -2382,12 +2395,13 @@ const _Splide = class {
     const root = isString(target) ? query(document, target) : target;
     assert(root, `${root} is invalid.`);
     this.root = root;
-    merge(this._options, DEFAULTS, _Splide.defaults, options || {});
+    options = merge({}, DEFAULTS, _Splide.defaults, options || {});
     try {
-      merge(this._options, JSON.parse(getAttribute(root, DATA_ATTRIBUTE)));
+      merge(options, JSON.parse(getAttribute(root, DATA_ATTRIBUTE)));
     } catch (e) {
       assert(false, "Invalid JSON");
     }
+    this._options = options;
   }
   mount(Extensions, Transition) {
     const { state, Components: Components2 } = this;
@@ -2425,7 +2439,7 @@ const _Splide = class {
     return this;
   }
   on(events, callback) {
-    this.event.on(events, callback, null, DEFAULT_USER_EVENT_PRIORITY);
+    this.event.on(events, callback);
     return this;
   }
   off(events) {
@@ -2454,7 +2468,7 @@ const _Splide = class {
   destroy(completely = true) {
     const { event, state } = this;
     if (state.is(CREATED)) {
-      event.on(EVENT_READY, this.destroy.bind(this, completely), this);
+      EventInterface(this).on(EVENT_READY, this.destroy.bind(this, completely));
     } else {
       forOwn(this._Components, (component) => {
         component.destroy && component.destroy(completely);
@@ -2854,4 +2868,4 @@ class SplideRenderer {
   }
 }
 
-export { CLASSES, CLASS_ACTIVE, CLASS_ARROW, CLASS_ARROWS, CLASS_ARROW_NEXT, CLASS_ARROW_PREV, CLASS_AUTOPLAY, CLASS_CLONE, CLASS_CONTAINER, CLASS_INITIALIZED, CLASS_LIST, CLASS_LOADING, CLASS_NEXT, CLASS_PAGINATION, CLASS_PAGINATION_PAGE, CLASS_PAUSE, CLASS_PLAY, CLASS_PREV, CLASS_PROGRESS, CLASS_PROGRESS_BAR, CLASS_ROOT, CLASS_SLIDE, CLASS_SLIDER, CLASS_SPINNER, CLASS_SR, CLASS_TRACK, CLASS_VISIBLE, EVENT_ACTIVE, EVENT_ARROWS_MOUNTED, EVENT_ARROWS_UPDATED, EVENT_AUTOPLAY_PAUSE, EVENT_AUTOPLAY_PLAY, EVENT_AUTOPLAY_PLAYING, EVENT_CLICK, EVENT_DESTROY, EVENT_DRAG, EVENT_DRAGGED, EVENT_DRAGGING, EVENT_HIDDEN, EVENT_INACTIVE, EVENT_LAZYLOAD_LOADED, EVENT_MEDIA, EVENT_MOUNTED, EVENT_MOVE, EVENT_MOVED, EVENT_NAVIGATION_MOUNTED, EVENT_PAGINATION_MOUNTED, EVENT_PAGINATION_UPDATED, EVENT_READY, EVENT_REFRESH, EVENT_REPOSITIONED, EVENT_RESIZE, EVENT_RESIZED, EVENT_SCROLL, EVENT_SCROLLED, EVENT_SHIFTED, EVENT_SLIDE_KEYDOWN, EVENT_UPDATED, EVENT_VISIBLE, EventBus, EventInterface, RequestInterval, STATUS_CLASSES, Splide, SplideRenderer, State, Throttle, Splide as default };
+export { CLASSES, CLASS_ACTIVE, CLASS_ARROW, CLASS_ARROWS, CLASS_ARROW_NEXT, CLASS_ARROW_PREV, CLASS_AUTOPLAY, CLASS_CLONE, CLASS_CONTAINER, CLASS_INITIALIZED, CLASS_LIST, CLASS_LOADING, CLASS_NEXT, CLASS_PAGINATION, CLASS_PAGINATION_PAGE, CLASS_PAUSE, CLASS_PLAY, CLASS_PREV, CLASS_PROGRESS, CLASS_PROGRESS_BAR, CLASS_ROOT, CLASS_SLIDE, CLASS_SLIDER, CLASS_SPINNER, CLASS_TRACK, CLASS_VISIBLE, EVENT_ACTIVE, EVENT_ARROWS_MOUNTED, EVENT_ARROWS_UPDATED, EVENT_AUTOPLAY_PAUSE, EVENT_AUTOPLAY_PLAY, EVENT_AUTOPLAY_PLAYING, EVENT_CLICK, EVENT_DESTROY, EVENT_DRAG, EVENT_DRAGGED, EVENT_DRAGGING, EVENT_HIDDEN, EVENT_INACTIVE, EVENT_LAZYLOAD_LOADED, EVENT_MEDIA, EVENT_MOUNTED, EVENT_MOVE, EVENT_MOVED, EVENT_NAVIGATION_MOUNTED, EVENT_PAGINATION_MOUNTED, EVENT_PAGINATION_UPDATED, EVENT_READY, EVENT_REFRESH, EVENT_REPOSITIONED, EVENT_RESIZE, EVENT_RESIZED, EVENT_SCROLL, EVENT_SCROLLED, EVENT_SHIFTED, EVENT_SLIDE_KEYDOWN, EVENT_UPDATED, EVENT_VISIBLE, EventBinder, EventInterface, RequestInterval, STATUS_CLASSES, Splide, SplideRenderer, State, Throttle, Splide as default };

+ 8 - 7
dist/js/splide.js

@@ -734,8 +734,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
 
   function Elements(Splide2, Components2, options) {
     var _EventInterface = EventInterface(Splide2),
-        on = _EventInterface.on,
-        bind = _EventInterface.bind;
+        on = _EventInterface.on;
 
     var root = Splide2.root;
     var i18n = options.i18n;
@@ -834,7 +833,8 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
         options = Splide2.options;
     var isNavigation = options.isNavigation,
         updateOnMove = options.updateOnMove,
-        i18n = options.i18n;
+        i18n = options.i18n,
+        pagination = options.pagination;
     var resolve = Components.Direction.resolve;
     var styles = getAttribute(slide, "style");
     var isClone = slideIndex > -1;
@@ -844,8 +844,8 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
     function mount() {
       if (!isClone) {
         slide.id = root.id + "-slide" + pad(index + 1);
-        setAttribute(slide, ROLE, options.pagination ? "tabpanel" : "group");
-        setAttribute(slide, ARIA_ROLEDESCRIPTION, i18n.slide);
+        setAttribute(slide, ROLE, pagination ? "tabpanel" : "group");
+        setAttribute(slide, ARIA_ROLEDESCRIPTION, pagination ? "" : i18n.slide);
         setAttribute(slide, ARIA_LABEL, format(i18n.slideLabel, [index + 1, Splide2.length]));
       }
 
@@ -2237,7 +2237,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
   }
 
   var NORMALIZATION_MAP = {
-    spacebar: " ",
+    Spacebar: " ",
     Right: "ArrowRight",
     Left: "ArrowLeft",
     Up: "ArrowUp",
@@ -2496,6 +2496,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
       list = create("ul", classes.pagination, parent);
       setAttribute(list, ROLE, "tablist");
       setAttribute(list, ARIA_LABEL, i18n.select);
+      setAttribute(list, ARIA_ORIENTATION, options.direction === TTB ? "vertical" : "");
 
       for (var i = 0; i < max; i++) {
         var li = create("li", null, list);
@@ -2545,7 +2546,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
 
       if (item) {
         focus(item.button);
-        go(">" + page);
+        go(">" + nextPage);
         prevent(e, true);
       }
     }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/js/splide.min.js


BIN
dist/js/splide.min.js.gz


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
dist/js/splide.min.js.map


+ 27 - 40
dist/types/index.d.ts

@@ -261,6 +261,7 @@ declare const I18N: {
     pause: string;
     carousel: string;
     slide: string;
+    select: string;
     slideLabel: string;
 };
 
@@ -786,70 +787,57 @@ declare type SlidesPredicate = (Slide: SlideComponent, index: number, Slides: Sl
 declare type SlideMatcher = number | number[] | string | SlidesPredicate;
 
 /**
- * The interface for the EventBus instance.
- *
- * @since 3.0.0
- */
-interface EventBusObject {
-    on(events: string | string[], callback: EventBusCallback, key?: object, priority?: number): void;
-    off(events: string | string[], key?: object): void;
-    offBy(key: object): void;
-    emit(event: string, ...args: any[]): void;
-    destroy(): void;
-}
-/**
- * The interface for each event handler object.
+ * The type for an EventTarget or an array with EventTarget objects.
  *
- * @since 3.0.0
+ * @since 3.7.0
  */
-declare type EventHandler = [AnyFunction, string, number, object?];
+declare type EventTargets = EventTarget | EventTarget[];
 /**
- * The type for a callback function of the EventBus.
+ * The interface for the EventBinder object.
  *
  * @since 3.0.0
  */
-declare type EventBusCallback = AnyFunction;
+interface EventBinderObject {
+    bind(target: EventTargets, events: string | string[], callback: AnyFunction, options?: AddEventListenerOptions): void;
+    unbind(target: EventTarget | EventTarget[], events: string | string[], callback?: AnyFunction): void;
+    dispatch<T>(target: EventTarget, event: string, detail?: T): void;
+    destroy(): void;
+}
 /**
- * The constructor to provided a simple event system.
+ * The constructor function to provide methods to subscribe native events.
  *
- * @since 3.0.0
+ * @since 3.7.0
+ * @constructor
  *
- * @return An EventBus object.
+ * @return An EventBinder object.
  */
-declare function EventBus(): EventBusObject;
+declare function EventBinder(): EventBinderObject;
 
-/**
- * The type for an EventTarget or an array with EventTarget objects.
- *
- * @since 3.7.0
- */
-declare type EventTargets = EventTarget | EventTarget[];
 /**
  * The interface for the EventInterface object.
  *
  * @since 3.0.0
  */
-interface EventInterfaceObject {
-    on<K extends keyof EventMap>(event: K, callback: EventMap[K], priority?: number): void;
-    on(events: string | string[], callback: EventBusCallback, priority?: number): void;
+interface EventInterfaceObject extends EventBinderObject {
+    on<K extends keyof EventMap>(event: K, callback: EventMap[K]): void;
+    on(events: string | string[], callback: AnyFunction): void;
     off<K extends keyof EventMap>(events: K | K[] | string | string[]): void;
     emit<K extends keyof EventMap>(event: K, ...args: Parameters<EventMap[K]>): void;
     emit(event: string, ...args: any[]): void;
-    bind(target: EventTargets, events: string, callback: AnyFunction, options?: AddEventListenerOptions): void;
-    unbind(target: EventTarget | EventTarget[], events: string, callback?: AnyFunction): void;
-    destroy(): void;
+    /** @internal */
+    bus: DocumentFragment;
 }
 /**
- * The function that provides interface for internal and native events.
+ * The constructor function that provides interface for internal and native events.
  *
  * @since 3.0.0
+ * @constructor
  *
  * @param Splide - A Splide instance.
- * @param manual - Optional. Whether to destroy the interface manually or not.
  *
  * @return A collection of interface functions.
  */
-declare function EventInterface(Splide: Splide, manual?: boolean): EventInterfaceObject;
+declare function EventInterface(Splide?: Splide): EventInterfaceObject;
 
 /**
  * The interface for the returning value of the RequestInterval.
@@ -938,7 +926,7 @@ declare class Splide {
     /**
      * The EventBusObject object.
      */
-    readonly event: EventBusObject;
+    readonly event: EventInterfaceObject;
     /**
      * The collection of all component objects.
      */
@@ -1062,7 +1050,7 @@ declare class Splide {
      * @return `this`
      */
     on<K extends keyof EventMap>(events: K, callback: EventMap[K]): this;
-    on(events: string | string[], callback: EventBusCallback): this;
+    on(events: string | string[], callback: AnyFunction): this;
     /**
      * Removes the registered all handlers for the specified event or events.
      * If you want to only remove a particular handler, use namespace to identify it.
@@ -1586,7 +1574,6 @@ declare const CLASS_AUTOPLAY: string;
 declare const CLASS_PLAY: string;
 declare const CLASS_PAUSE: string;
 declare const CLASS_SPINNER: string;
-declare const CLASS_SR: string;
 declare const CLASS_INITIALIZED = "is-initialized";
 declare const CLASS_ACTIVE = "is-active";
 declare const CLASS_PREV = "is-prev";
@@ -1616,4 +1603,4 @@ declare const CLASSES: {
     spinner: string;
 };
 
-export { AnyFunction, ArrowsComponent, AutoplayComponent, BaseComponent, CLASSES, CLASS_ACTIVE, CLASS_ARROW, CLASS_ARROWS, CLASS_ARROW_NEXT, CLASS_ARROW_PREV, CLASS_AUTOPLAY, CLASS_CLONE, CLASS_CONTAINER, CLASS_INITIALIZED, CLASS_LIST, CLASS_LOADING, CLASS_NEXT, CLASS_PAGINATION, CLASS_PAGINATION_PAGE, CLASS_PAUSE, CLASS_PLAY, CLASS_PREV, CLASS_PROGRESS, CLASS_PROGRESS_BAR, CLASS_ROOT, CLASS_SLIDE, CLASS_SLIDER, CLASS_SPINNER, CLASS_SR, CLASS_TRACK, CLASS_VISIBLE, Cast, ClonesComponent, ComponentConstructor, Components, ControllerComponent, CoverComponent, DirectionComponent, DragComponent, EVENT_ACTIVE, EVENT_ARROWS_MOUNTED, EVENT_ARROWS_UPDATED, EVENT_AUTOPLAY_PAUSE, EVENT_AUTOPLAY_PLAY, EVENT_AUTOPLAY_PLAYING, EVENT_CLICK, EVENT_DESTROY, EVENT_DRAG, EVENT_DRAGGED, EVENT_DRAGGING, EVENT_HIDDEN, EVENT_INACTIVE, EVENT_LAZYLOAD_LOADED, EVENT_MEDIA, EVENT_MOUNTED, EVENT_MOVE, EVENT_MOVED, EVENT_NAVIGATION_MOUNTED, EVENT_PAGINATION_MOUNTED, EVENT_PAGINATION_UPDATED, EVENT_READY, EVENT_REFRESH, EVENT_REPOSITIONED, EVENT_RESIZE, EVENT_RESIZED, EVENT_SCROLL, EVENT_SCROLLED, EVENT_SHIFTED, EVENT_SLIDE_KEYDOWN, EVENT_UPDATED, EVENT_VISIBLE, ElementsComponent, EventBus, EventBusCallback, EventBusObject, EventHandler, EventInterface, EventInterfaceObject, EventMap, Head, KeyboardComponent, LayoutComponent, LazyLoadComponent, LiveComponent, MediaComponent, MoveComponent, Options, PaginationComponent, PaginationData, PaginationItem, Push, RequestInterval, RequestIntervalInterface, Resolve, ResponsiveOptions, STATUS_CLASSES, ScrollComponent, Shift, ShiftN, SlideComponent, SlidesComponent, Splide, SplideRenderer, State, StateObject, SyncComponent, SyncTarget, Throttle, ThrottleInstance, TransitionComponent, WheelComponent, Splide as default };
+export { AnyFunction, ArrowsComponent, AutoplayComponent, BaseComponent, CLASSES, CLASS_ACTIVE, CLASS_ARROW, CLASS_ARROWS, CLASS_ARROW_NEXT, CLASS_ARROW_PREV, CLASS_AUTOPLAY, CLASS_CLONE, CLASS_CONTAINER, CLASS_INITIALIZED, CLASS_LIST, CLASS_LOADING, CLASS_NEXT, CLASS_PAGINATION, CLASS_PAGINATION_PAGE, CLASS_PAUSE, CLASS_PLAY, CLASS_PREV, CLASS_PROGRESS, CLASS_PROGRESS_BAR, CLASS_ROOT, CLASS_SLIDE, CLASS_SLIDER, CLASS_SPINNER, CLASS_TRACK, CLASS_VISIBLE, Cast, ClonesComponent, ComponentConstructor, Components, ControllerComponent, CoverComponent, DirectionComponent, DragComponent, EVENT_ACTIVE, EVENT_ARROWS_MOUNTED, EVENT_ARROWS_UPDATED, EVENT_AUTOPLAY_PAUSE, EVENT_AUTOPLAY_PLAY, EVENT_AUTOPLAY_PLAYING, EVENT_CLICK, EVENT_DESTROY, EVENT_DRAG, EVENT_DRAGGED, EVENT_DRAGGING, EVENT_HIDDEN, EVENT_INACTIVE, EVENT_LAZYLOAD_LOADED, EVENT_MEDIA, EVENT_MOUNTED, EVENT_MOVE, EVENT_MOVED, EVENT_NAVIGATION_MOUNTED, EVENT_PAGINATION_MOUNTED, EVENT_PAGINATION_UPDATED, EVENT_READY, EVENT_REFRESH, EVENT_REPOSITIONED, EVENT_RESIZE, EVENT_RESIZED, EVENT_SCROLL, EVENT_SCROLLED, EVENT_SHIFTED, EVENT_SLIDE_KEYDOWN, EVENT_UPDATED, EVENT_VISIBLE, ElementsComponent, EventBinder, EventBinderObject, EventInterface, EventInterfaceObject, EventMap, Head, KeyboardComponent, LayoutComponent, LazyLoadComponent, LiveComponent, MediaComponent, MoveComponent, Options, PaginationComponent, PaginationData, PaginationItem, Push, RequestInterval, RequestIntervalInterface, Resolve, ResponsiveOptions, STATUS_CLASSES, ScrollComponent, Shift, ShiftN, SlideComponent, SlidesComponent, Splide, SplideRenderer, State, StateObject, SyncComponent, SyncTarget, Throttle, ThrottleInstance, TransitionComponent, WheelComponent, Splide as default };

+ 1 - 1
src/css/template/default/foundation/colors.scss

@@ -5,4 +5,4 @@ $sub02: #999 !default;
 $background: #ccc !default;
 $background-active: #fff !default;
 
-$focus: #00bbff !default;
+$focus: #0bf !default;

+ 1 - 1
src/css/template/default/foundation/mixins.scss

@@ -12,4 +12,4 @@ $outline-offset-slide: -3px !default;
 @mixin focus-outline-slide {
   @include focus-outline;
   outline-offset: $outline-offset-slide;
-}
+}

+ 6 - 14
src/js/components/Clones/test/general.test.ts

@@ -1,12 +1,11 @@
 import { CLASS_CLONE } from '../../../constants/classes';
 import { init } from '../../../test';
-import { SLIDER_WIDTH } from '../../../test/fixtures/constants';
 
 
 describe( 'Clones', () => {
   // This test must be the first because of uniqueId().
   test( 'can generate clones with unique IDs.', () => {
-    const splide = init( { type: 'loop' } );
+    const splide = init( { type: 'loop', perPage: 3 } );
     const clones = splide.root.getElementsByClassName( CLASS_CLONE );
 
     expect( clones[ 0 ].id ).toBe( 'splide01-clone01' );
@@ -15,28 +14,21 @@ describe( 'Clones', () => {
   } );
 
   test( 'can generate clones according to the flickMaxPages option.', () => {
-    const splide = init( { type: 'loop', flickMaxPages: 2 } );
+    const splide = init( { type: 'loop', perPage: 3 } );
     const clones = splide.root.getElementsByClassName( CLASS_CLONE );
     const Slides = splide.Components.Slides.get( true );
 
-    expect( clones.length ).toBe( ( splide.options.flickMaxPages + 1 ) * 2 );
+    expect( clones.length ).toBe( splide.options.perPage * 2 );
     expect( clones[ 2 ].nextElementSibling ).toBe( Slides[ 0 ].slide );
     expect( clones[ 3 ].previousElementSibling ).toBe( Slides[ Slides.length - 1 ].slide );
   } );
 
-  test( 'can generate clones according to the flickMaxPages and perPage options.', () => {
+  test( 'can generate clones according to the perPage option.', () => {
     const splide = init( { type: 'loop', flickMaxPages: 1, perPage: 3 } );
     const clones = splide.root.getElementsByClassName( CLASS_CLONE );
-    const { flickMaxPages, perPage } = splide.options;
+    const { perPage } = splide.options;
 
-    expect( clones.length ).toBe( perPage * ( flickMaxPages + 1 ) * 2 );
-  } );
-
-  test( 'can generate clones according to the fixedWidth option.', () => {
-    const splide = init( { type: 'loop', flickMaxPages: 1, fixedWidth: 100 } );
-    const clones = splide.root.getElementsByClassName( CLASS_CLONE );
-
-    expect( clones.length ).toBe( Math.ceil( SLIDER_WIDTH / 100 ) * ( splide.options.flickMaxPages + 1 ) * 2 );
+    expect( clones.length ).toBe( perPage * 2 );
   } );
 
   test( 'should register clones to Slides component.', () => {

+ 1 - 1
src/js/components/Elements/Elements.ts

@@ -77,7 +77,7 @@ export interface ElementsComponent extends BaseComponent, ElementCollection {
  * @return An Elements component object.
  */
 export function Elements( Splide: Splide, Components: Components, options: Options ): ElementsComponent {
-  const { on, bind } = EventInterface( Splide );
+  const { on } = EventInterface( Splide );
   const { root } = Splide;
   const { i18n } = options;
   const elements: ElementCollection = {} as ElementCollection;

+ 1 - 1
src/js/components/Keyboard/Keyboard.ts

@@ -3,7 +3,7 @@ import { EVENT_MOVE, EVENT_UPDATED } from '../../constants/events';
 import { EventInterface } from '../../constructors';
 import { Splide } from '../../core/Splide/Splide';
 import { BaseComponent, Components, Options } from '../../types';
-import { includes, nextTick, setAttribute } from '../../utils';
+import { nextTick, setAttribute } from '../../utils';
 import { normalizeKey } from '../../utils/dom/normalizeKey/normalizeKey';
 
 

+ 3 - 3
src/js/components/LazyLoad/test/nearby.test.ts

@@ -122,17 +122,17 @@ describe( 'LazyLoad in the `nearby` mode', () => {
   } );
 
   test( 'can start loading images of previous slides in the loop mode.', () => {
-    const splide = init( { type: 'loop', lazyLoad: true }, { src: false, dataSrc: true } );
+    const splide = init( { type: 'loop', lazyLoad: true, perPage: 3 }, { src: false, dataSrc: true } );
     const prev1  = splide.Components.Slides.getAt( -1 );
     const prev2  = splide.Components.Slides.getAt( -2 );
     const last1  = splide.Components.Slides.getAt( splide.length - 1 );
     const last2  = splide.Components.Slides.getAt( splide.length - 2 );
 
     expect( prev1.slide.querySelector( 'img' ).src ).toBe( `${ URL }/${ splide.length - 1 }.jpg` );
-    expect( prev2.slide.querySelector( 'img' ).src ).toBe( '' );
+    expect( prev2.slide.querySelector( 'img' ).src ).toBe( `${ URL }/${ splide.length - 2 }.jpg` );
 
     expect( last1.slide.querySelector( 'img' ).src ).toBe( `${ URL }/${ splide.length - 1 }.jpg` );
-    expect( last2.slide.querySelector( 'img' ).src ).toBe( '' );
+    expect( last2.slide.querySelector( 'img' ).src ).toBe( `${ URL }/${ splide.length - 2 }.jpg` );
   } );
 
   test( 'should not start loading an image if the slide is not close to the current location.', () => {

+ 12 - 3
src/js/components/Pagination/Pagination.ts

@@ -1,5 +1,13 @@
-import { ARIA_CONTROLS, ARIA_LABEL, ARIA_SELECTED, ROLE, TAB_INDEX } from '../../constants/attributes';
+import {
+  ARIA_CONTROLS,
+  ARIA_LABEL,
+  ARIA_ORIENTATION,
+  ARIA_SELECTED,
+  ROLE,
+  TAB_INDEX,
+} from '../../constants/attributes';
 import { CLASS_ACTIVE } from '../../constants/classes';
+import { TTB } from '../../constants/directions';
 import {
   EVENT_MOVE,
   EVENT_PAGINATION_MOUNTED,
@@ -135,6 +143,7 @@ export function Pagination( Splide: Splide, Components: Components, options: Opt
 
     setAttribute( list, ROLE, 'tablist' );
     setAttribute( list, ARIA_LABEL, i18n.select );
+    setAttribute( list, ARIA_ORIENTATION, options.direction === TTB ? 'vertical' : '' );
 
     for ( let i = 0; i < max; i++ ) {
       const li       = create( 'li', null, list );
@@ -180,7 +189,7 @@ export function Pagination( Splide: Splide, Components: Components, options: Opt
     const { length } = items;
     const key = normalizeKey( e );
 
-    let nextPage = -1
+    let nextPage = -1;
 
     if ( key === resolve( 'ArrowRight' ) ) {
       nextPage = ++page % length;
@@ -196,7 +205,7 @@ export function Pagination( Splide: Splide, Components: Components, options: Opt
 
     if ( item ) {
       focus( item.button );
-      go( `>${ page }` );
+      go( `>${ nextPage }` );
       prevent( e, true );
     }
   }

+ 5 - 16
src/js/components/Pagination/test/general.test.ts

@@ -43,14 +43,14 @@ describe( 'Pagination', () => {
     const items  = document.getElementsByClassName( CLASS_PAGINATION_PAGE );
 
     expect( items[ 0 ].classList.contains( CLASS_ACTIVE ) ).toBe( true );
-    expect( items[ 0 ].getAttribute( 'aria-current' ) ).toBe( 'true' );
+    expect( items[ 0 ].getAttribute( 'aria-selected' ) ).toBe( 'true' );
 
     splide.go( 2 );
 
     expect( items[ 0 ].classList.contains( CLASS_ACTIVE ) ).toBe( false );
-    expect( items[ 0 ].getAttribute( 'aria-current' ) ).toBeNull();
+    expect( items[ 0 ].getAttribute( 'aria-selected' ) ).toBeNull();
     expect( items[ 2 ].classList.contains( CLASS_ACTIVE ) ).toBe( true );
-    expect( items[ 2 ].getAttribute( 'aria-current' ) ).toBe( 'true' );
+    expect( items[ 2 ].getAttribute( 'aria-selected' ) ).toBe( 'true' );
   } );
 
   test( 'can update status classes by the page.', () => {
@@ -60,23 +60,12 @@ describe( 'Pagination', () => {
     splide.go( 4 ); // page: 1
 
     expect( items[ 1 ].classList.contains( CLASS_ACTIVE ) ).toBe( true );
-    expect( items[ 1 ].getAttribute( 'aria-current' ) ).toBe( 'true' );
+    expect( items[ 1 ].getAttribute( 'aria-selected' ) ).toBe( 'true' );
 
     splide.go( 7 ); // end page
 
     expect( items[ 3 ].classList.contains( CLASS_ACTIVE ) ).toBe( true );
-    expect( items[ 3 ].getAttribute( 'aria-current' ) ).toBe( 'true' );
-  } );
-
-  test( 'can set focus to the selected slide.', () => {
-    const splide = init( { speed: 0 } );
-    const items  = document.getElementsByClassName( CLASS_PAGINATION_PAGE );
-
-    fire( items[ 0 ], 'click' );
-    expect( splide.Components.Slides.getAt( 0 ).slide ).toBe( document.activeElement );
-
-    fire( items[ 1 ], 'click' );
-    expect( splide.Components.Slides.getAt( 1 ).slide ).toBe( document.activeElement );
+    expect( items[ 3 ].getAttribute( 'aria-selected' ) ).toBe( 'true' );
   } );
 
   test( 'should not create pagination if slides are not enough to the perPage option.', () => {

+ 114 - 0
src/js/components/Pagination/test/tab.test.ts

@@ -0,0 +1,114 @@
+import { CLASS_PAGINATION, CLASS_PAGINATION_PAGE } from '../../../constants/classes';
+import { fire, init } from '../../../test';
+import { Options } from '../../../types';
+
+
+/**
+ * - `aria-selected` is tested on general.test.
+ * - `aria-labelledby` is not necessary since each tabpanel has its own `aria-label`.
+ */
+describe( 'Pagination', () => {
+  test( 'can set the `tablist` role to the pagination root.', () => {
+    init();
+    const pagination = document.querySelector( `.${ CLASS_PAGINATION }` );
+    expect( pagination.getAttribute( 'role' ) ).toBe( 'tablist' );
+  } );
+
+  test( 'can set the `tab` role to each item in pagination.', () => {
+    init();
+    const items = Array.from( document.getElementsByClassName( CLASS_PAGINATION_PAGE ) );
+
+    items.forEach( item => {
+      expect( item.getAttribute( 'role' ) ).toBe( 'tab' );
+    } );
+
+    expect( items.length ).toBeGreaterThan( 0 );
+  } );
+
+  test( 'can set the `aria-label` role to each item in pagination.', () => {
+    const items = Array.from( document.getElementsByClassName( CLASS_PAGINATION_PAGE ) );
+
+    items.forEach( ( item, index ) => {
+      expect( item.getAttribute( 'aria-label' ) ).toBe( `Go to slide ${ index + 1 }` );
+    } );
+  } );
+
+  test( 'can set `aria-controls="target slide ID"` to each item in pagination.', () => {
+    const splide = init();
+    const items  = Array.from( document.getElementsByClassName( CLASS_PAGINATION_PAGE ) );
+    const Slides = splide.Components.Slides;
+
+    items.forEach( ( item, index ) => {
+      const Slide = Slides.getAt( index );
+
+      if ( Slide ) {
+        expect( item.getAttribute( 'aria-controls' ) ).toBe( Slide.slide.id );
+      } else {
+        fail();
+      }
+    } );
+  } );
+
+  describe.each( [
+    [ 'ltr', 'ArrowRight', 'ArrowLeft' ],
+    [ 'rtl', 'ArrowLeft', 'ArrowRight' ],
+    [ 'ttb', 'ArrowDown', 'ArrowUp' ],
+  ] )( 'in %s mode.', (
+    direction,
+    nextKey,
+    prevKey
+  ) => {
+    test( 'can move focus by arrow keys and activate the corresponded slide', () => {
+      const splide = init( { speed: 0, direction: direction as Options[ 'direction' ], height: 300 } );
+      const items  = document.getElementsByClassName( CLASS_PAGINATION_PAGE );
+
+      fire( items[ 0 ], 'keydown', { key: nextKey } );
+      expect( items[ 1 ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( 1 );
+
+      fire( items[ 1 ], 'keydown', { key: nextKey } );
+      expect( items[ 2 ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( 2 );
+
+      fire( items[ 2 ], 'keydown', { key: prevKey } );
+      expect( items[ 1 ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( 1 );
+    } );
+
+    test( 'can loop focus by arrow keys.', () => {
+      const splide = init( { speed: 0, direction: direction as Options[ 'direction' ], height: 300 } );
+      const items  = document.getElementsByClassName( CLASS_PAGINATION_PAGE );
+      const end    = splide.length - 1;
+
+      fire( items[ 0 ], 'keydown', { key: prevKey } );
+      expect( items[ end ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( end );
+
+      fire( items[ end ], 'keydown', { key: nextKey } );
+      expect( items[ 0 ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( 0 );
+    } );
+
+    test( 'can focus the first slide by  and the last slide by End.', () => {
+      const splide = init( { speed: 0, direction: direction as Options[ 'direction' ], height: 300 } );
+      const items  = document.getElementsByClassName( CLASS_PAGINATION_PAGE );
+      const end    = splide.length - 1;
+
+      fire( items[ 0 ], 'keydown', { key: 'End' } );
+      expect( items[ end ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( end );
+
+      fire( items[ end ], 'keydown', { key: 'Home' } );
+      expect( items[ 0 ] === document.activeElement ).toBe( true );
+      expect( splide.index ).toBe( 0 );
+    } );
+
+    test( 'can set proper orientation according to the direction.', () => {
+      init( { speed: 0, direction: direction as Options[ 'direction' ], height: 300 } );
+      const pagination = document.querySelector( `.${ CLASS_PAGINATION }` );
+
+      expect( pagination.getAttribute( 'aria-orientation' ) )
+        .toBe( direction === 'ttb' ? 'vertical' : null );
+    } );
+  } );
+} );

+ 3 - 3
src/js/components/Slides/Slide.ts

@@ -88,7 +88,7 @@ export interface  SlideComponent extends BaseComponent {
 export function Slide( Splide: Splide, index: number, slideIndex: number, slide: HTMLElement ): SlideComponent {
   const { on, emit, bind, destroy: destroyEvents } = EventInterface( Splide );
   const { Components, root, options } = Splide;
-  const { isNavigation, updateOnMove, i18n } = options;
+  const { isNavigation, updateOnMove, i18n, pagination } = options;
   const { resolve } = Components.Direction;
   const styles    = getAttribute( slide, 'style' );
   const isClone   = slideIndex > -1;
@@ -105,8 +105,8 @@ export function Slide( Splide: Splide, index: number, slideIndex: number, slide:
   function mount( this: SlideComponent ): void {
     if ( ! isClone ) {
       slide.id = `${ root.id }-slide${ pad( index + 1 ) }`;
-      setAttribute( slide, ROLE, options.pagination ? 'tabpanel' : 'group' );
-      setAttribute( slide, ARIA_ROLEDESCRIPTION, i18n.slide );
+      setAttribute( slide, ROLE, pagination ? 'tabpanel' : 'group' );
+      setAttribute( slide, ARIA_ROLEDESCRIPTION, pagination ? '' : i18n.slide );
       setAttribute( slide, ARIA_LABEL, format( i18n.slideLabel, [ index + 1, Splide.length ] ) );
     }
 

+ 28 - 4
src/js/components/Slides/test/slide.test.ts

@@ -205,17 +205,41 @@ describe( 'Slide', () => {
     expect( Clone.isWithin( 0, 1 ) ).toBe( true );
   } );
 
-  test( 'can assign the role and aria attributes.', () => {
-    const splide = init( { speed: 0, isNavigation: true } );
+
+  test( 'should assign the tabpanel role without `aria-roledescription` if the pagination option is enabled.', () => {
+    const splide = init( { pagination: true } );
+
+    splide.Components.Slides.forEach( ( { slide } ) => {
+      expect( slide.getAttribute( 'role' ) ).toBe( 'tabpanel' );
+      expect( slide.getAttribute( 'aria-roledescription' ) ).toBeNull();
+    } );
+  } );
+
+  test( 'should assign assign group tab role with `aria-roledescription` if the pagination option is disabled.', () => {
+    const splide = init( { pagination: false } );
+
+    splide.Components.Slides.forEach( ( { slide } ) => {
+      expect( slide.getAttribute( 'role' ) ).toBe( 'group' );
+      expect( slide.getAttribute( 'aria-roledescription' ) ).toBe( splide.options.i18n.slide );
+    } );
+  } );
+
+  test( 'can assign and update role/aria attributes for navigation.', () => {
+    const splide = init( { speed: 0, isNavigation: true, pagination: false } );
     const { Slides } = splide.Components;
     const Slide0 = Slides.getAt( 0 );
     const Slide1 = Slides.getAt( 1 );
 
-    expect( Slide0.slide.getAttribute( 'role' ) ).toBe( 'menuitem' );
+    expect( Slide0.slide.getAttribute( 'aria-current' ) ).toBe( 'true' );
     expect( Slide0.slide.getAttribute( 'aria-label' ) ).toBe( format( splide.options.i18n.slideX, 1 ) );
 
-    expect( Slide1.slide.getAttribute( 'role' ) ).toBe( 'menuitem' );
+    expect( Slide1.slide.getAttribute( 'aria-current' ) ).toBeNull();
     expect( Slide1.slide.getAttribute( 'aria-label' ) ).toBe( format( splide.options.i18n.slideX, 2 ) );
+
+    splide.go( 1 );
+
+    expect( Slide0.slide.getAttribute( 'aria-current' ) ).toBeNull();
+    expect( Slide1.slide.getAttribute( 'aria-current' ) ).toBe( 'true' );
   } );
 
   test( 'can emit the `click` event on click.', done => {

+ 1 - 1
src/js/utils/dom/normalizeKey/normalizeKey.ts

@@ -7,7 +7,7 @@ import { isString } from '../../type/type';
  * @since 3.7.0
  */
 export const NORMALIZATION_MAP = {
-  spacebar: ' ',
+  Spacebar: ' ',
   Right   : 'ArrowRight',
   Left    : 'ArrowLeft',
   Up      : 'ArrowUp',

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است