vidply 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vidply.js CHANGED
@@ -1232,6 +1232,42 @@ var VidPly = (() => {
1232
1232
  document.addEventListener("keydown", handleEscape);
1233
1233
  }, 100);
1234
1234
  }
1235
+ // Helper method to add keyboard navigation to menus (arrow keys)
1236
+ attachMenuKeyboardNavigation(menu) {
1237
+ const menuItems = Array.from(menu.querySelectorAll(`.${this.player.options.classPrefix}-menu-item`));
1238
+ if (menuItems.length === 0) return;
1239
+ const handleKeyDown = (e) => {
1240
+ const currentIndex = menuItems.indexOf(document.activeElement);
1241
+ switch (e.key) {
1242
+ case "ArrowDown":
1243
+ e.preventDefault();
1244
+ const nextIndex = (currentIndex + 1) % menuItems.length;
1245
+ menuItems[nextIndex].focus();
1246
+ break;
1247
+ case "ArrowUp":
1248
+ e.preventDefault();
1249
+ const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
1250
+ menuItems[prevIndex].focus();
1251
+ break;
1252
+ case "Home":
1253
+ e.preventDefault();
1254
+ menuItems[0].focus();
1255
+ break;
1256
+ case "End":
1257
+ e.preventDefault();
1258
+ menuItems[menuItems.length - 1].focus();
1259
+ break;
1260
+ case "Enter":
1261
+ case " ":
1262
+ e.preventDefault();
1263
+ if (document.activeElement && menuItems.includes(document.activeElement)) {
1264
+ document.activeElement.click();
1265
+ }
1266
+ break;
1267
+ }
1268
+ };
1269
+ menu.addEventListener("keydown", handleKeyDown);
1270
+ }
1235
1271
  createElement() {
1236
1272
  this.element = DOMUtils.createElement("div", {
1237
1273
  className: `${this.player.options.classPrefix}-controls`,
@@ -1635,12 +1671,26 @@ var VidPly = (() => {
1635
1671
  }
1636
1672
  createTimeDisplay() {
1637
1673
  const container = DOMUtils.createElement("div", {
1638
- className: `${this.player.options.classPrefix}-time`
1674
+ className: `${this.player.options.classPrefix}-time`,
1675
+ attributes: {
1676
+ "role": "group",
1677
+ "aria-label": "Time display"
1678
+ }
1639
1679
  });
1640
1680
  this.controls.currentTimeDisplay = DOMUtils.createElement("span", {
1641
1681
  className: `${this.player.options.classPrefix}-current-time`,
1642
- textContent: "00:00"
1682
+ attributes: {
1683
+ "aria-label": "0 seconds"
1684
+ }
1685
+ });
1686
+ const currentTimeVisual = DOMUtils.createElement("span", {
1687
+ textContent: "00:00",
1688
+ attributes: {
1689
+ "aria-hidden": "true"
1690
+ }
1643
1691
  });
1692
+ this.controls.currentTimeDisplay.appendChild(currentTimeVisual);
1693
+ this.controls.currentTimeVisual = currentTimeVisual;
1644
1694
  const separator = DOMUtils.createElement("span", {
1645
1695
  textContent: " / ",
1646
1696
  attributes: {
@@ -1649,8 +1699,18 @@ var VidPly = (() => {
1649
1699
  });
1650
1700
  this.controls.durationDisplay = DOMUtils.createElement("span", {
1651
1701
  className: `${this.player.options.classPrefix}-duration`,
1652
- textContent: "00:00"
1702
+ attributes: {
1703
+ "aria-label": "Duration: 0 seconds"
1704
+ }
1705
+ });
1706
+ const durationVisual = DOMUtils.createElement("span", {
1707
+ textContent: "00:00",
1708
+ attributes: {
1709
+ "aria-hidden": "true"
1710
+ }
1653
1711
  });
1712
+ this.controls.durationDisplay.appendChild(durationVisual);
1713
+ this.controls.durationVisual = durationVisual;
1654
1714
  container.appendChild(this.controls.currentTimeDisplay);
1655
1715
  container.appendChild(separator);
1656
1716
  container.appendChild(this.controls.durationDisplay);
@@ -1726,7 +1786,8 @@ var VidPly = (() => {
1726
1786
  className: `${this.player.options.classPrefix}-menu-item`,
1727
1787
  attributes: {
1728
1788
  "type": "button",
1729
- "role": "menuitem"
1789
+ "role": "menuitem",
1790
+ "tabindex": "-1"
1730
1791
  }
1731
1792
  });
1732
1793
  const timeLabel = DOMUtils.createElement("span", {
@@ -1746,6 +1807,13 @@ var VidPly = (() => {
1746
1807
  });
1747
1808
  menu.appendChild(item);
1748
1809
  }
1810
+ this.attachMenuKeyboardNavigation(menu);
1811
+ setTimeout(() => {
1812
+ const firstItem = menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
1813
+ if (firstItem) {
1814
+ firstItem.focus();
1815
+ }
1816
+ }, 0);
1749
1817
  }
1750
1818
  }
1751
1819
  button.appendChild(menu);
@@ -1799,19 +1867,22 @@ var VidPly = (() => {
1799
1867
  });
1800
1868
  menu.appendChild(noQualityItem);
1801
1869
  } else {
1870
+ let activeItem = null;
1802
1871
  if (isHLS) {
1803
1872
  const autoItem = DOMUtils.createElement("button", {
1804
1873
  className: `${this.player.options.classPrefix}-menu-item`,
1805
1874
  textContent: i18n.t("player.auto"),
1806
1875
  attributes: {
1807
1876
  "type": "button",
1808
- "role": "menuitem"
1877
+ "role": "menuitem",
1878
+ "tabindex": "-1"
1809
1879
  }
1810
1880
  });
1811
1881
  const isAuto = this.player.renderer.hls && this.player.renderer.hls.currentLevel === -1;
1812
1882
  if (isAuto) {
1813
1883
  autoItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1814
1884
  autoItem.appendChild(createIconElement("check"));
1885
+ activeItem = autoItem;
1815
1886
  }
1816
1887
  autoItem.addEventListener("click", () => {
1817
1888
  if (this.player.renderer.switchQuality) {
@@ -1827,12 +1898,14 @@ var VidPly = (() => {
1827
1898
  textContent: quality.name || `${quality.height}p`,
1828
1899
  attributes: {
1829
1900
  "type": "button",
1830
- "role": "menuitem"
1901
+ "role": "menuitem",
1902
+ "tabindex": "-1"
1831
1903
  }
1832
1904
  });
1833
1905
  if (quality.index === currentQuality) {
1834
1906
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1835
1907
  item.appendChild(createIconElement("check"));
1908
+ activeItem = item;
1836
1909
  }
1837
1910
  item.addEventListener("click", () => {
1838
1911
  if (this.player.renderer.switchQuality) {
@@ -1842,6 +1915,13 @@ var VidPly = (() => {
1842
1915
  });
1843
1916
  menu.appendChild(item);
1844
1917
  });
1918
+ this.attachMenuKeyboardNavigation(menu);
1919
+ setTimeout(() => {
1920
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
1921
+ if (focusTarget) {
1922
+ focusTarget.focus();
1923
+ }
1924
+ }, 0);
1845
1925
  }
1846
1926
  } else {
1847
1927
  const noSupportItem = DOMUtils.createElement("div", {
@@ -1935,6 +2015,12 @@ var VidPly = (() => {
1935
2015
  menu.style.minWidth = "220px";
1936
2016
  button.appendChild(menu);
1937
2017
  this.attachMenuCloseHandler(menu, button, true);
2018
+ setTimeout(() => {
2019
+ const firstSelect = menu.querySelector("select");
2020
+ if (firstSelect) {
2021
+ firstSelect.focus();
2022
+ }
2023
+ }, 0);
1938
2024
  }
1939
2025
  createStyleControl(label, property, options) {
1940
2026
  const group = DOMUtils.createElement("div", {
@@ -2147,18 +2233,21 @@ var VidPly = (() => {
2147
2233
  }
2148
2234
  });
2149
2235
  const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
2236
+ let activeItem = null;
2150
2237
  speeds.forEach((speed) => {
2151
2238
  const item = DOMUtils.createElement("button", {
2152
2239
  className: `${this.player.options.classPrefix}-menu-item`,
2153
2240
  textContent: this.formatSpeedLabel(speed),
2154
2241
  attributes: {
2155
2242
  "type": "button",
2156
- "role": "menuitem"
2243
+ "role": "menuitem",
2244
+ "tabindex": "-1"
2157
2245
  }
2158
2246
  });
2159
2247
  if (speed === this.player.state.playbackSpeed) {
2160
2248
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
2161
2249
  item.appendChild(createIconElement("check"));
2250
+ activeItem = item;
2162
2251
  }
2163
2252
  item.addEventListener("click", () => {
2164
2253
  this.player.setPlaybackSpeed(speed);
@@ -2167,7 +2256,14 @@ var VidPly = (() => {
2167
2256
  menu.appendChild(item);
2168
2257
  });
2169
2258
  button.appendChild(menu);
2259
+ this.attachMenuKeyboardNavigation(menu);
2170
2260
  this.attachMenuCloseHandler(menu, button);
2261
+ setTimeout(() => {
2262
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
2263
+ if (focusTarget) {
2264
+ focusTarget.focus();
2265
+ }
2266
+ }, 0);
2171
2267
  }
2172
2268
  createCaptionsButton() {
2173
2269
  const button = DOMUtils.createElement("button", {
@@ -2210,17 +2306,20 @@ var VidPly = (() => {
2210
2306
  this.attachMenuCloseHandler(menu, button);
2211
2307
  return;
2212
2308
  }
2309
+ let activeItem = null;
2213
2310
  const offItem = DOMUtils.createElement("button", {
2214
2311
  className: `${this.player.options.classPrefix}-menu-item`,
2215
2312
  textContent: i18n.t("captions.off"),
2216
2313
  attributes: {
2217
2314
  "type": "button",
2218
- "role": "menuitem"
2315
+ "role": "menuitem",
2316
+ "tabindex": "-1"
2219
2317
  }
2220
2318
  });
2221
2319
  if (!this.player.state.captionsEnabled) {
2222
2320
  offItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
2223
2321
  offItem.appendChild(createIconElement("check"));
2322
+ activeItem = offItem;
2224
2323
  }
2225
2324
  offItem.addEventListener("click", () => {
2226
2325
  this.player.disableCaptions();
@@ -2236,12 +2335,14 @@ var VidPly = (() => {
2236
2335
  attributes: {
2237
2336
  "type": "button",
2238
2337
  "role": "menuitem",
2239
- "lang": track.language
2338
+ "lang": track.language,
2339
+ "tabindex": "-1"
2240
2340
  }
2241
2341
  });
2242
2342
  if (this.player.state.captionsEnabled && this.player.captionManager.currentTrack === this.player.captionManager.tracks[track.index]) {
2243
2343
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
2244
2344
  item.appendChild(createIconElement("check"));
2345
+ activeItem = item;
2245
2346
  }
2246
2347
  item.addEventListener("click", () => {
2247
2348
  this.player.captionManager.switchTrack(track.index);
@@ -2251,7 +2352,14 @@ var VidPly = (() => {
2251
2352
  menu.appendChild(item);
2252
2353
  });
2253
2354
  button.appendChild(menu);
2355
+ this.attachMenuKeyboardNavigation(menu);
2254
2356
  this.attachMenuCloseHandler(menu, button);
2357
+ setTimeout(() => {
2358
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
2359
+ if (focusTarget) {
2360
+ focusTarget.focus();
2361
+ }
2362
+ }, 0);
2255
2363
  }
2256
2364
  updateCaptionsButton() {
2257
2365
  if (!this.controls.captions) return;
@@ -2426,36 +2534,43 @@ var VidPly = (() => {
2426
2534
  const percent = this.player.state.currentTime / this.player.state.duration * 100;
2427
2535
  this.controls.played.style.width = `${percent}%`;
2428
2536
  this.controls.progress.setAttribute("aria-valuenow", String(Math.round(percent)));
2429
- if (this.controls.currentTimeDisplay) {
2430
- this.controls.currentTimeDisplay.textContent = TimeUtils.formatTime(this.player.state.currentTime);
2537
+ if (this.controls.currentTimeVisual) {
2538
+ const currentTime = this.player.state.currentTime;
2539
+ this.controls.currentTimeVisual.textContent = TimeUtils.formatTime(currentTime);
2540
+ this.controls.currentTimeDisplay.setAttribute("aria-label", TimeUtils.formatDuration(currentTime));
2431
2541
  }
2432
2542
  }
2433
2543
  updateDuration() {
2434
- if (this.controls.durationDisplay) {
2435
- this.controls.durationDisplay.textContent = TimeUtils.formatTime(this.player.state.duration);
2544
+ if (this.controls.durationVisual) {
2545
+ const duration = this.player.state.duration;
2546
+ this.controls.durationVisual.textContent = TimeUtils.formatTime(duration);
2547
+ this.controls.durationDisplay.setAttribute("aria-label", "Duration: " + TimeUtils.formatDuration(duration));
2436
2548
  }
2437
2549
  }
2438
2550
  updateVolumeDisplay() {
2439
- if (!this.controls.volumeFill) return;
2440
2551
  const percent = this.player.state.volume * 100;
2441
- this.controls.volumeFill.style.height = `${percent}%`;
2552
+ if (this.controls.volumeFill) {
2553
+ this.controls.volumeFill.style.height = `${percent}%`;
2554
+ }
2442
2555
  if (this.controls.mute) {
2443
2556
  const icon = this.controls.mute.querySelector(".vidply-icon");
2444
- let iconName;
2445
- if (this.player.state.muted || this.player.state.volume === 0) {
2446
- iconName = "volumeMuted";
2447
- } else if (this.player.state.volume < 0.3) {
2448
- iconName = "volumeLow";
2449
- } else if (this.player.state.volume < 0.7) {
2450
- iconName = "volumeMedium";
2451
- } else {
2452
- iconName = "volumeHigh";
2557
+ if (icon) {
2558
+ let iconName;
2559
+ if (this.player.state.muted || this.player.state.volume === 0) {
2560
+ iconName = "volumeMuted";
2561
+ } else if (this.player.state.volume < 0.3) {
2562
+ iconName = "volumeLow";
2563
+ } else if (this.player.state.volume < 0.7) {
2564
+ iconName = "volumeMedium";
2565
+ } else {
2566
+ iconName = "volumeHigh";
2567
+ }
2568
+ icon.innerHTML = createIconElement(iconName).innerHTML;
2569
+ this.controls.mute.setAttribute(
2570
+ "aria-label",
2571
+ this.player.state.muted ? i18n.t("player.unmute") : i18n.t("player.mute")
2572
+ );
2453
2573
  }
2454
- icon.innerHTML = createIconElement(iconName).innerHTML;
2455
- this.controls.mute.setAttribute(
2456
- "aria-label",
2457
- this.player.state.muted ? i18n.t("player.unmute") : i18n.t("player.mute")
2458
- );
2459
2574
  }
2460
2575
  if (this.controls.volumeSlider) {
2461
2576
  this.controls.volumeSlider.setAttribute("aria-valuenow", String(Math.round(percent)));
@@ -2640,7 +2755,6 @@ var VidPly = (() => {
2640
2755
  this.updateCaptions();
2641
2756
  };
2642
2757
  selectedTrack.track.addEventListener("cuechange", this.cueChangeHandler);
2643
- this.element.style.display = "block";
2644
2758
  this.player.emit("captionsenabled", selectedTrack);
2645
2759
  }
2646
2760
  }
@@ -2783,6 +2897,9 @@ var VidPly = (() => {
2783
2897
  }
2784
2898
  }
2785
2899
  }
2900
+ if (!handled && this.player.options.debug) {
2901
+ console.log("[VidPly] Unhandled key:", e.key, "code:", e.code, "shiftKey:", e.shiftKey);
2902
+ }
2786
2903
  }
2787
2904
  executeAction(action, event) {
2788
2905
  switch (action) {
@@ -2801,12 +2918,6 @@ var VidPly = (() => {
2801
2918
  case "seek-backward":
2802
2919
  this.player.seekBackward();
2803
2920
  return true;
2804
- case "seek-forward-large":
2805
- this.player.seekForward(this.player.options.seekIntervalLarge);
2806
- return true;
2807
- case "seek-backward-large":
2808
- this.player.seekBackward(this.player.options.seekIntervalLarge);
2809
- return true;
2810
2921
  case "mute":
2811
2922
  this.player.toggleMute();
2812
2923
  return true;
@@ -2815,14 +2926,22 @@ var VidPly = (() => {
2815
2926
  return true;
2816
2927
  case "captions":
2817
2928
  if (this.player.captionManager && this.player.captionManager.tracks.length > 1) {
2818
- const captionsButton = document.querySelector(".vidply-captions");
2819
- if (captionsButton && this.player.controlBar) {
2929
+ const captionsButton = this.player.controlBar && this.player.controlBar.controls.captions;
2930
+ if (captionsButton) {
2820
2931
  this.player.controlBar.showCaptionsMenu(captionsButton);
2932
+ } else {
2933
+ this.player.toggleCaptions();
2821
2934
  }
2822
2935
  } else {
2823
2936
  this.player.toggleCaptions();
2824
2937
  }
2825
2938
  return true;
2939
+ case "caption-style-menu":
2940
+ if (this.player.controlBar && this.player.controlBar.controls.captionStyle) {
2941
+ this.player.controlBar.showCaptionStyleMenu(this.player.controlBar.controls.captionStyle);
2942
+ return true;
2943
+ }
2944
+ return false;
2826
2945
  case "speed-up":
2827
2946
  this.player.setPlaybackSpeed(
2828
2947
  Math.min(2, this.player.state.playbackSpeed + 0.25)
@@ -2833,9 +2952,30 @@ var VidPly = (() => {
2833
2952
  Math.max(0.25, this.player.state.playbackSpeed - 0.25)
2834
2953
  );
2835
2954
  return true;
2836
- case "settings":
2837
- this.player.showSettings();
2838
- return true;
2955
+ case "speed-menu":
2956
+ if (this.player.controlBar && this.player.controlBar.controls.speed) {
2957
+ this.player.controlBar.showSpeedMenu(this.player.controlBar.controls.speed);
2958
+ return true;
2959
+ }
2960
+ return false;
2961
+ case "quality-menu":
2962
+ if (this.player.controlBar && this.player.controlBar.controls.quality) {
2963
+ this.player.controlBar.showQualityMenu(this.player.controlBar.controls.quality);
2964
+ return true;
2965
+ }
2966
+ return false;
2967
+ case "chapters-menu":
2968
+ if (this.player.controlBar && this.player.controlBar.controls.chapters) {
2969
+ this.player.controlBar.showChaptersMenu(this.player.controlBar.controls.chapters);
2970
+ return true;
2971
+ }
2972
+ return false;
2973
+ case "transcript-toggle":
2974
+ if (this.player.transcriptManager) {
2975
+ this.player.transcriptManager.toggleTranscript();
2976
+ return true;
2977
+ }
2978
+ return false;
2839
2979
  default:
2840
2980
  return false;
2841
2981
  }
@@ -2902,317 +3042,6 @@ var VidPly = (() => {
2902
3042
  }
2903
3043
  };
2904
3044
 
2905
- // src/controls/SettingsDialog.js
2906
- var SettingsDialog = class {
2907
- constructor(player) {
2908
- this.player = player;
2909
- this.element = null;
2910
- this.isOpen = false;
2911
- this.init();
2912
- }
2913
- init() {
2914
- this.createElement();
2915
- }
2916
- createElement() {
2917
- this.overlay = DOMUtils.createElement("div", {
2918
- className: `${this.player.options.classPrefix}-settings-overlay`,
2919
- attributes: {
2920
- "role": "dialog",
2921
- "aria-modal": "true",
2922
- "aria-label": i18n.t("settings.title")
2923
- }
2924
- });
2925
- this.overlay.style.display = "none";
2926
- this.element = DOMUtils.createElement("div", {
2927
- className: `${this.player.options.classPrefix}-settings-dialog`
2928
- });
2929
- const header = DOMUtils.createElement("div", {
2930
- className: `${this.player.options.classPrefix}-settings-header`
2931
- });
2932
- const title = DOMUtils.createElement("h2", {
2933
- textContent: i18n.t("settings.title"),
2934
- attributes: {
2935
- "id": `${this.player.options.classPrefix}-settings-title`
2936
- }
2937
- });
2938
- const closeButton = DOMUtils.createElement("button", {
2939
- className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-settings-close`,
2940
- attributes: {
2941
- "type": "button",
2942
- "aria-label": i18n.t("settings.close")
2943
- }
2944
- });
2945
- closeButton.appendChild(createIconElement("close"));
2946
- closeButton.addEventListener("click", () => this.hide());
2947
- header.appendChild(title);
2948
- header.appendChild(closeButton);
2949
- const content = DOMUtils.createElement("div", {
2950
- className: `${this.player.options.classPrefix}-settings-content`
2951
- });
2952
- content.appendChild(this.createSpeedSettings());
2953
- if (this.player.captionManager && this.player.captionManager.tracks.length > 0) {
2954
- content.appendChild(this.createCaptionSettings());
2955
- }
2956
- const footer = DOMUtils.createElement("div", {
2957
- className: `${this.player.options.classPrefix}-settings-footer`
2958
- });
2959
- const resetButton = DOMUtils.createElement("button", {
2960
- className: `${this.player.options.classPrefix}-button`,
2961
- textContent: i18n.t("settings.reset"),
2962
- attributes: {
2963
- "type": "button"
2964
- }
2965
- });
2966
- resetButton.addEventListener("click", () => this.resetSettings());
2967
- footer.appendChild(resetButton);
2968
- this.element.appendChild(header);
2969
- this.element.appendChild(content);
2970
- this.element.appendChild(footer);
2971
- this.overlay.appendChild(this.element);
2972
- this.player.container.appendChild(this.overlay);
2973
- this.overlay.addEventListener("click", (e) => {
2974
- if (e.target === this.overlay) {
2975
- this.hide();
2976
- }
2977
- });
2978
- document.addEventListener("keydown", (e) => {
2979
- if (e.key === "Escape" && this.isOpen) {
2980
- this.hide();
2981
- }
2982
- });
2983
- }
2984
- formatSpeedLabel(speed) {
2985
- if (speed === 1) {
2986
- return i18n.t("speeds.normal");
2987
- }
2988
- const speedStr = speed.toLocaleString(i18n.getLanguage(), {
2989
- minimumFractionDigits: 0,
2990
- maximumFractionDigits: 2
2991
- });
2992
- return `${speedStr}\xD7`;
2993
- }
2994
- createSpeedSettings() {
2995
- const section = DOMUtils.createElement("div", {
2996
- className: `${this.player.options.classPrefix}-settings-section`
2997
- });
2998
- const label = DOMUtils.createElement("label", {
2999
- textContent: i18n.t("settings.speed"),
3000
- attributes: {
3001
- "for": `${this.player.options.classPrefix}-speed-select`
3002
- }
3003
- });
3004
- const select = DOMUtils.createElement("select", {
3005
- className: `${this.player.options.classPrefix}-settings-select`,
3006
- attributes: {
3007
- "id": `${this.player.options.classPrefix}-speed-select`
3008
- }
3009
- });
3010
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
3011
- speeds.forEach((speed) => {
3012
- const option = DOMUtils.createElement("option", {
3013
- textContent: this.formatSpeedLabel(speed),
3014
- attributes: {
3015
- "value": String(speed)
3016
- }
3017
- });
3018
- if (speed === this.player.state.playbackSpeed) {
3019
- option.selected = true;
3020
- }
3021
- select.appendChild(option);
3022
- });
3023
- select.addEventListener("change", (e) => {
3024
- this.player.setPlaybackSpeed(parseFloat(e.target.value));
3025
- });
3026
- section.appendChild(label);
3027
- section.appendChild(select);
3028
- return section;
3029
- }
3030
- createCaptionSettings() {
3031
- const section = DOMUtils.createElement("div", {
3032
- className: `${this.player.options.classPrefix}-settings-section`
3033
- });
3034
- const heading = DOMUtils.createElement("h3", {
3035
- textContent: i18n.t("settings.captions")
3036
- });
3037
- section.appendChild(heading);
3038
- const trackLabel = DOMUtils.createElement("label", {
3039
- textContent: i18n.t("captions.select"),
3040
- attributes: {
3041
- "for": `${this.player.options.classPrefix}-caption-track-select`
3042
- }
3043
- });
3044
- const trackSelect = DOMUtils.createElement("select", {
3045
- className: `${this.player.options.classPrefix}-settings-select`,
3046
- attributes: {
3047
- "id": `${this.player.options.classPrefix}-caption-track-select`
3048
- }
3049
- });
3050
- const offOption = DOMUtils.createElement("option", {
3051
- textContent: i18n.t("captions.off"),
3052
- attributes: { "value": "-1" }
3053
- });
3054
- trackSelect.appendChild(offOption);
3055
- const tracks = this.player.captionManager.getAvailableTracks();
3056
- tracks.forEach((track) => {
3057
- const option = DOMUtils.createElement("option", {
3058
- textContent: track.label,
3059
- attributes: { "value": String(track.index) }
3060
- });
3061
- trackSelect.appendChild(option);
3062
- });
3063
- trackSelect.addEventListener("change", (e) => {
3064
- const index = parseInt(e.target.value);
3065
- if (index === -1) {
3066
- this.player.disableCaptions();
3067
- } else {
3068
- this.player.captionManager.switchTrack(index);
3069
- }
3070
- });
3071
- section.appendChild(trackLabel);
3072
- section.appendChild(trackSelect);
3073
- section.appendChild(this.createCaptionStyleControl("fontSize", i18n.t("captions.fontSize"), [
3074
- { label: i18n.t("fontSizes.small"), value: "80%" },
3075
- { label: i18n.t("fontSizes.medium"), value: "100%" },
3076
- { label: i18n.t("fontSizes.large"), value: "120%" },
3077
- { label: i18n.t("fontSizes.xlarge"), value: "150%" }
3078
- ]));
3079
- section.appendChild(this.createCaptionStyleControl("fontFamily", i18n.t("captions.fontFamily"), [
3080
- { label: i18n.t("fontFamilies.sansSerif"), value: "sans-serif" },
3081
- { label: i18n.t("fontFamilies.serif"), value: "serif" },
3082
- { label: i18n.t("fontFamilies.monospace"), value: "monospace" }
3083
- ]));
3084
- section.appendChild(this.createColorControl("color", i18n.t("captions.color")));
3085
- section.appendChild(this.createColorControl("backgroundColor", i18n.t("captions.backgroundColor")));
3086
- section.appendChild(this.createRangeControl("opacity", i18n.t("captions.opacity"), 0, 1, 0.1));
3087
- return section;
3088
- }
3089
- createCaptionStyleControl(property, label, options) {
3090
- const wrapper = DOMUtils.createElement("div", {
3091
- className: `${this.player.options.classPrefix}-settings-control`
3092
- });
3093
- const labelEl = DOMUtils.createElement("label", {
3094
- textContent: label,
3095
- attributes: {
3096
- "for": `${this.player.options.classPrefix}-caption-${property}`
3097
- }
3098
- });
3099
- const select = DOMUtils.createElement("select", {
3100
- className: `${this.player.options.classPrefix}-settings-select`,
3101
- attributes: {
3102
- "id": `${this.player.options.classPrefix}-caption-${property}`
3103
- }
3104
- });
3105
- options.forEach((opt) => {
3106
- const option = DOMUtils.createElement("option", {
3107
- textContent: opt.label,
3108
- attributes: { "value": opt.value }
3109
- });
3110
- if (opt.value === this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]) {
3111
- option.selected = true;
3112
- }
3113
- select.appendChild(option);
3114
- });
3115
- select.addEventListener("change", (e) => {
3116
- this.player.captionManager.setCaptionStyle(property, e.target.value);
3117
- });
3118
- wrapper.appendChild(labelEl);
3119
- wrapper.appendChild(select);
3120
- return wrapper;
3121
- }
3122
- createColorControl(property, label) {
3123
- const wrapper = DOMUtils.createElement("div", {
3124
- className: `${this.player.options.classPrefix}-settings-control`
3125
- });
3126
- const labelEl = DOMUtils.createElement("label", {
3127
- textContent: label,
3128
- attributes: {
3129
- "for": `${this.player.options.classPrefix}-caption-${property}`
3130
- }
3131
- });
3132
- const input = DOMUtils.createElement("input", {
3133
- className: `${this.player.options.classPrefix}-settings-color`,
3134
- attributes: {
3135
- "type": "color",
3136
- "id": `${this.player.options.classPrefix}-caption-${property}`,
3137
- "value": this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]
3138
- }
3139
- });
3140
- input.addEventListener("change", (e) => {
3141
- this.player.captionManager.setCaptionStyle(property, e.target.value);
3142
- });
3143
- wrapper.appendChild(labelEl);
3144
- wrapper.appendChild(input);
3145
- return wrapper;
3146
- }
3147
- createRangeControl(property, label, min, max, step) {
3148
- const wrapper = DOMUtils.createElement("div", {
3149
- className: `${this.player.options.classPrefix}-settings-control`
3150
- });
3151
- const labelEl = DOMUtils.createElement("label", {
3152
- textContent: label,
3153
- attributes: {
3154
- "for": `${this.player.options.classPrefix}-caption-${property}`
3155
- }
3156
- });
3157
- const input = DOMUtils.createElement("input", {
3158
- className: `${this.player.options.classPrefix}-settings-range`,
3159
- attributes: {
3160
- "type": "range",
3161
- "id": `${this.player.options.classPrefix}-caption-${property}`,
3162
- "min": String(min),
3163
- "max": String(max),
3164
- "step": String(step),
3165
- "value": String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
3166
- }
3167
- });
3168
- const valueDisplay = DOMUtils.createElement("span", {
3169
- className: `${this.player.options.classPrefix}-settings-value`,
3170
- textContent: String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
3171
- });
3172
- input.addEventListener("input", (e) => {
3173
- const value = parseFloat(e.target.value);
3174
- valueDisplay.textContent = value.toFixed(1);
3175
- this.player.captionManager.setCaptionStyle(property, value);
3176
- });
3177
- wrapper.appendChild(labelEl);
3178
- wrapper.appendChild(input);
3179
- wrapper.appendChild(valueDisplay);
3180
- return wrapper;
3181
- }
3182
- resetSettings() {
3183
- this.player.setPlaybackSpeed(1);
3184
- if (this.player.captionManager) {
3185
- this.player.captionManager.setCaptionStyle("fontSize", "100%");
3186
- this.player.captionManager.setCaptionStyle("fontFamily", "sans-serif");
3187
- this.player.captionManager.setCaptionStyle("color", "#FFFFFF");
3188
- this.player.captionManager.setCaptionStyle("backgroundColor", "#000000");
3189
- this.player.captionManager.setCaptionStyle("opacity", 0.8);
3190
- }
3191
- this.hide();
3192
- setTimeout(() => this.show(), 100);
3193
- }
3194
- show() {
3195
- this.overlay.style.display = "flex";
3196
- this.isOpen = true;
3197
- const closeButton = this.element.querySelector(`.${this.player.options.classPrefix}-settings-close`);
3198
- if (closeButton) {
3199
- closeButton.focus();
3200
- }
3201
- this.player.emit("settingsopen");
3202
- }
3203
- hide() {
3204
- this.overlay.style.display = "none";
3205
- this.isOpen = false;
3206
- this.player.container.focus();
3207
- this.player.emit("settingsclose");
3208
- }
3209
- destroy() {
3210
- if (this.overlay && this.overlay.parentNode) {
3211
- this.overlay.parentNode.removeChild(this.overlay);
3212
- }
3213
- }
3214
- };
3215
-
3216
3045
  // src/controls/TranscriptManager.js
3217
3046
  var TranscriptManager = class {
3218
3047
  constructor(player) {
@@ -4500,16 +4329,18 @@ var VidPly = (() => {
4500
4329
  "play-pause": [" ", "p", "k"],
4501
4330
  "volume-up": ["ArrowUp"],
4502
4331
  "volume-down": ["ArrowDown"],
4503
- "seek-forward": ["ArrowRight", "f"],
4504
- "seek-backward": ["ArrowLeft", "r"],
4505
- "seek-forward-large": ["l"],
4506
- "seek-backward-large": ["j"],
4332
+ "seek-forward": ["ArrowRight"],
4333
+ "seek-backward": ["ArrowLeft"],
4507
4334
  "mute": ["m"],
4508
4335
  "fullscreen": ["f"],
4509
4336
  "captions": ["c"],
4337
+ "caption-style-menu": ["a"],
4510
4338
  "speed-up": [">"],
4511
4339
  "speed-down": ["<"],
4512
- "settings": ["s"]
4340
+ "speed-menu": ["s"],
4341
+ "quality-menu": ["q"],
4342
+ "chapters-menu": ["j"],
4343
+ "transcript-toggle": ["t"]
4513
4344
  },
4514
4345
  // Accessibility
4515
4346
  ariaLabels: {},
@@ -4598,9 +4429,6 @@ var VidPly = (() => {
4598
4429
  if (this.options.keyboard) {
4599
4430
  this.keyboardManager = new KeyboardManager(this);
4600
4431
  }
4601
- if (this.options.settingsButton) {
4602
- this.settingsDialog = new SettingsDialog(this);
4603
- }
4604
4432
  this.setupResponsiveHandlers();
4605
4433
  if (this.options.startTime > 0) {
4606
4434
  this.seek(this.options.startTime);
@@ -4778,6 +4606,17 @@ var VidPly = (() => {
4778
4606
  this.captionManager.destroy();
4779
4607
  this.captionManager = new CaptionManager(this);
4780
4608
  }
4609
+ if (this.transcriptManager) {
4610
+ const wasVisible = this.transcriptManager.isVisible;
4611
+ this.transcriptManager.destroy();
4612
+ this.transcriptManager = new TranscriptManager(this);
4613
+ if (wasVisible) {
4614
+ this.transcriptManager.showTranscript();
4615
+ }
4616
+ }
4617
+ if (this.controlBar) {
4618
+ this.updateControlBar();
4619
+ }
4781
4620
  this.emit("sourcechange", config);
4782
4621
  this.log("Media loaded successfully");
4783
4622
  } catch (error) {
@@ -4789,6 +4628,17 @@ var VidPly = (() => {
4789
4628
  * @param {string} src - New source URL
4790
4629
  * @returns {boolean}
4791
4630
  */
4631
+ /**
4632
+ * Update control bar to refresh button visibility based on available features
4633
+ */
4634
+ updateControlBar() {
4635
+ if (!this.controlBar) return;
4636
+ const controlBar = this.controlBar;
4637
+ controlBar.element.innerHTML = "";
4638
+ controlBar.createControls();
4639
+ controlBar.attachEvents();
4640
+ controlBar.setupAutoHide();
4641
+ }
4792
4642
  shouldChangeRenderer(src) {
4793
4643
  if (!this.renderer) return true;
4794
4644
  const isYouTube = src.includes("youtube.com") || src.includes("youtu.be");
@@ -4853,12 +4703,14 @@ var VidPly = (() => {
4853
4703
  this.renderer.setMuted(true);
4854
4704
  }
4855
4705
  this.state.muted = true;
4706
+ this.emit("volumechange");
4856
4707
  }
4857
4708
  unmute() {
4858
4709
  if (this.renderer) {
4859
4710
  this.renderer.setMuted(false);
4860
4711
  }
4861
4712
  this.state.muted = false;
4713
+ this.emit("volumechange");
4862
4714
  }
4863
4715
  toggleMute() {
4864
4716
  if (this.state.muted) {
@@ -5092,15 +4944,11 @@ var VidPly = (() => {
5092
4944
  }
5093
4945
  }
5094
4946
  // Settings
4947
+ // Settings dialog removed - using individual control buttons instead
5095
4948
  showSettings() {
5096
- if (this.settingsDialog) {
5097
- this.settingsDialog.show();
5098
- }
4949
+ console.warn("[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)");
5099
4950
  }
5100
4951
  hideSettings() {
5101
- if (this.settingsDialog) {
5102
- this.settingsDialog.hide();
5103
- }
5104
4952
  }
5105
4953
  // Utility methods
5106
4954
  getCurrentTime() {
@@ -5197,9 +5045,6 @@ var VidPly = (() => {
5197
5045
  if (this.keyboardManager) {
5198
5046
  this.keyboardManager.destroy();
5199
5047
  }
5200
- if (this.settingsDialog) {
5201
- this.settingsDialog.destroy();
5202
- }
5203
5048
  if (this.transcriptManager) {
5204
5049
  this.transcriptManager.destroy();
5205
5050
  }
@@ -5253,7 +5098,9 @@ var VidPly = (() => {
5253
5098
  this.trackInfoElement = null;
5254
5099
  this.handleTrackEnd = this.handleTrackEnd.bind(this);
5255
5100
  this.handleTrackError = this.handleTrackError.bind(this);
5101
+ this.player.playlistManager = this;
5256
5102
  this.init();
5103
+ this.updatePlayerControls();
5257
5104
  }
5258
5105
  init() {
5259
5106
  this.player.on("ended", this.handleTrackEnd);
@@ -5262,6 +5109,17 @@ var VidPly = (() => {
5262
5109
  this.createUI();
5263
5110
  }
5264
5111
  }
5112
+ /**
5113
+ * Update player controls to add playlist navigation buttons
5114
+ */
5115
+ updatePlayerControls() {
5116
+ if (!this.player.controlBar) return;
5117
+ const controlBar = this.player.controlBar;
5118
+ controlBar.element.innerHTML = "";
5119
+ controlBar.createControls();
5120
+ controlBar.attachEvents();
5121
+ controlBar.setupAutoHide();
5122
+ }
5265
5123
  /**
5266
5124
  * Load a playlist
5267
5125
  * @param {Array} tracks - Array of track objects
@@ -5282,8 +5140,9 @@ var VidPly = (() => {
5282
5140
  /**
5283
5141
  * Play a specific track
5284
5142
  * @param {number} index - Track index
5143
+ * @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
5285
5144
  */
5286
- play(index) {
5145
+ play(index, userInitiated = false) {
5287
5146
  if (index < 0 || index >= this.tracks.length) {
5288
5147
  console.warn("VidPly Playlist: Invalid track index", index);
5289
5148
  return;
@@ -5303,6 +5162,9 @@ var VidPly = (() => {
5303
5162
  item: track,
5304
5163
  total: this.tracks.length
5305
5164
  });
5165
+ if (userInitiated && this.player.container) {
5166
+ this.player.container.focus();
5167
+ }
5306
5168
  setTimeout(() => {
5307
5169
  this.player.play();
5308
5170
  }, 100);
@@ -5364,12 +5226,17 @@ var VidPly = (() => {
5364
5226
  return;
5365
5227
  }
5366
5228
  this.trackInfoElement = DOMUtils.createElement("div", {
5367
- className: "vidply-track-info"
5229
+ className: "vidply-track-info",
5230
+ role: "status",
5231
+ "aria-live": "polite",
5232
+ "aria-atomic": "true"
5368
5233
  });
5369
5234
  this.trackInfoElement.style.display = "none";
5370
5235
  this.container.appendChild(this.trackInfoElement);
5371
5236
  this.playlistPanel = DOMUtils.createElement("div", {
5372
- className: "vidply-playlist-panel"
5237
+ className: "vidply-playlist-panel",
5238
+ role: "region",
5239
+ "aria-label": "Media playlist"
5373
5240
  });
5374
5241
  this.playlistPanel.style.display = "none";
5375
5242
  this.container.appendChild(this.playlistPanel);
@@ -5381,10 +5248,14 @@ var VidPly = (() => {
5381
5248
  if (!this.trackInfoElement) return;
5382
5249
  const trackNumber = this.currentIndex + 1;
5383
5250
  const totalTracks = this.tracks.length;
5251
+ const trackTitle = track.title || "Untitled";
5252
+ const trackArtist = track.artist || "";
5253
+ const announcement = `Now playing: Track ${trackNumber} of ${totalTracks}. ${trackTitle}${trackArtist ? " by " + trackArtist : ""}`;
5384
5254
  this.trackInfoElement.innerHTML = `
5385
- <div class="vidply-track-number">Track ${trackNumber} of ${totalTracks}</div>
5386
- <div class="vidply-track-title">${DOMUtils.escapeHTML(track.title || "Untitled")}</div>
5387
- ${track.artist ? `<div class="vidply-track-artist">${DOMUtils.escapeHTML(track.artist)}</div>` : ""}
5255
+ <span class="vidply-sr-only">${DOMUtils.escapeHTML(announcement)}</span>
5256
+ <div class="vidply-track-number" aria-hidden="true">Track ${trackNumber} of ${totalTracks}</div>
5257
+ <div class="vidply-track-title" aria-hidden="true">${DOMUtils.escapeHTML(trackTitle)}</div>
5258
+ ${trackArtist ? `<div class="vidply-track-artist" aria-hidden="true">${DOMUtils.escapeHTML(trackArtist)}</div>` : ""}
5388
5259
  `;
5389
5260
  this.trackInfoElement.style.display = "block";
5390
5261
  }
@@ -5394,14 +5265,29 @@ var VidPly = (() => {
5394
5265
  renderPlaylist() {
5395
5266
  if (!this.playlistPanel) return;
5396
5267
  this.playlistPanel.innerHTML = "";
5397
- const header = DOMUtils.createElement("div", {
5398
- className: "vidply-playlist-header"
5268
+ const header = DOMUtils.createElement("h2", {
5269
+ className: "vidply-playlist-header",
5270
+ id: "vidply-playlist-heading"
5399
5271
  });
5400
5272
  header.textContent = `Playlist (${this.tracks.length})`;
5401
5273
  this.playlistPanel.appendChild(header);
5402
- const list = DOMUtils.createElement("div", {
5403
- className: "vidply-playlist-list"
5404
- });
5274
+ const instructions = DOMUtils.createElement("div", {
5275
+ className: "vidply-sr-only",
5276
+ "aria-hidden": "false"
5277
+ });
5278
+ instructions.textContent = "Use arrow keys to navigate between tracks. Press Enter or Space to play a track. Press Home or End to jump to first or last track.";
5279
+ this.playlistPanel.appendChild(instructions);
5280
+ const list = DOMUtils.createElement("ul", {
5281
+ className: "vidply-playlist-list",
5282
+ "aria-labelledby": "vidply-playlist-heading",
5283
+ "aria-describedby": "vidply-playlist-instructions"
5284
+ });
5285
+ const listDescription = DOMUtils.createElement("div", {
5286
+ className: "vidply-sr-only",
5287
+ id: "vidply-playlist-instructions"
5288
+ });
5289
+ listDescription.textContent = `Playlist with ${this.tracks.length} ${this.tracks.length === 1 ? "track" : "tracks"}`;
5290
+ this.playlistPanel.appendChild(listDescription);
5405
5291
  this.tracks.forEach((track, index) => {
5406
5292
  const item = this.createPlaylistItem(track, index);
5407
5293
  list.appendChild(item);
@@ -5413,20 +5299,39 @@ var VidPly = (() => {
5413
5299
  * Create playlist item element
5414
5300
  */
5415
5301
  createPlaylistItem(track, index) {
5416
- const item = DOMUtils.createElement("div", {
5302
+ const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
5303
+ const trackTitle = track.title || `Track ${index + 1}`;
5304
+ const trackArtist = track.artist ? ` by ${track.artist}` : "";
5305
+ const isActive = index === this.currentIndex;
5306
+ const statusText = isActive ? "Currently playing" : "Not playing";
5307
+ const actionText = isActive ? "Press Enter to restart" : "Press Enter to play";
5308
+ const item = DOMUtils.createElement("li", {
5417
5309
  className: "vidply-playlist-item",
5418
- role: "button",
5419
- tabIndex: 0,
5420
- "aria-label": `Play ${track.title || "Track " + (index + 1)}`
5421
- });
5422
- if (index === this.currentIndex) {
5310
+ tabIndex: index === 0 ? 0 : -1,
5311
+ // Only first item is in tab order initially
5312
+ "aria-label": `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`,
5313
+ "aria-posinset": index + 1,
5314
+ "aria-setsize": this.tracks.length,
5315
+ "data-playlist-index": index
5316
+ });
5317
+ if (isActive) {
5423
5318
  item.classList.add("vidply-playlist-item-active");
5319
+ item.setAttribute("aria-current", "true");
5320
+ item.setAttribute("tabIndex", "0");
5424
5321
  }
5322
+ const positionInfo = DOMUtils.createElement("span", {
5323
+ className: "vidply-sr-only"
5324
+ });
5325
+ positionInfo.textContent = `${trackPosition}: `;
5326
+ item.appendChild(positionInfo);
5425
5327
  const thumbnail = DOMUtils.createElement("div", {
5426
- className: "vidply-playlist-thumbnail"
5328
+ className: "vidply-playlist-thumbnail",
5329
+ "aria-hidden": "true"
5427
5330
  });
5428
5331
  if (track.poster) {
5429
5332
  thumbnail.style.backgroundImage = `url(${track.poster})`;
5333
+ thumbnail.setAttribute("role", "img");
5334
+ thumbnail.setAttribute("aria-label", `${trackTitle} thumbnail`);
5430
5335
  } else {
5431
5336
  const icon = createIconElement("music");
5432
5337
  icon.classList.add("vidply-playlist-thumbnail-icon");
@@ -5434,12 +5339,13 @@ var VidPly = (() => {
5434
5339
  }
5435
5340
  item.appendChild(thumbnail);
5436
5341
  const info = DOMUtils.createElement("div", {
5437
- className: "vidply-playlist-item-info"
5342
+ className: "vidply-playlist-item-info",
5343
+ "aria-hidden": "true"
5438
5344
  });
5439
5345
  const title = DOMUtils.createElement("div", {
5440
5346
  className: "vidply-playlist-item-title"
5441
5347
  });
5442
- title.textContent = track.title || `Track ${index + 1}`;
5348
+ title.textContent = trackTitle;
5443
5349
  info.appendChild(title);
5444
5350
  if (track.artist) {
5445
5351
  const artist = DOMUtils.createElement("div", {
@@ -5449,20 +5355,64 @@ var VidPly = (() => {
5449
5355
  info.appendChild(artist);
5450
5356
  }
5451
5357
  item.appendChild(info);
5358
+ if (isActive) {
5359
+ const statusIndicator = DOMUtils.createElement("span", {
5360
+ className: "vidply-sr-only"
5361
+ });
5362
+ statusIndicator.textContent = " (Currently playing)";
5363
+ item.appendChild(statusIndicator);
5364
+ }
5452
5365
  const playIcon = createIconElement("play");
5453
5366
  playIcon.classList.add("vidply-playlist-item-icon");
5367
+ playIcon.setAttribute("aria-hidden", "true");
5454
5368
  item.appendChild(playIcon);
5455
5369
  item.addEventListener("click", () => {
5456
- this.play(index);
5370
+ this.play(index, true);
5457
5371
  });
5458
5372
  item.addEventListener("keydown", (e) => {
5459
- if (e.key === "Enter" || e.key === " ") {
5460
- e.preventDefault();
5461
- this.play(index);
5462
- }
5373
+ this.handlePlaylistItemKeydown(e, index);
5463
5374
  });
5464
5375
  return item;
5465
5376
  }
5377
+ /**
5378
+ * Handle keyboard navigation in playlist items
5379
+ */
5380
+ handlePlaylistItemKeydown(e, index) {
5381
+ const items = Array.from(this.playlistPanel.querySelectorAll(".vidply-playlist-item"));
5382
+ let newIndex = -1;
5383
+ switch (e.key) {
5384
+ case "Enter":
5385
+ case " ":
5386
+ e.preventDefault();
5387
+ this.play(index, true);
5388
+ break;
5389
+ case "ArrowDown":
5390
+ e.preventDefault();
5391
+ if (index < items.length - 1) {
5392
+ newIndex = index + 1;
5393
+ }
5394
+ break;
5395
+ case "ArrowUp":
5396
+ e.preventDefault();
5397
+ if (index > 0) {
5398
+ newIndex = index - 1;
5399
+ }
5400
+ break;
5401
+ case "Home":
5402
+ e.preventDefault();
5403
+ newIndex = 0;
5404
+ break;
5405
+ case "End":
5406
+ e.preventDefault();
5407
+ newIndex = items.length - 1;
5408
+ break;
5409
+ }
5410
+ if (newIndex !== -1 && newIndex !== index) {
5411
+ items[index].setAttribute("tabIndex", "-1");
5412
+ items[newIndex].setAttribute("tabIndex", "0");
5413
+ items[newIndex].focus();
5414
+ }
5415
+ }
5466
5416
  /**
5467
5417
  * Update playlist UI (highlight current track)
5468
5418
  */
@@ -5470,11 +5420,25 @@ var VidPly = (() => {
5470
5420
  if (!this.playlistPanel) return;
5471
5421
  const items = this.playlistPanel.querySelectorAll(".vidply-playlist-item");
5472
5422
  items.forEach((item, index) => {
5423
+ const track = this.tracks[index];
5424
+ const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
5425
+ const trackTitle = track.title || `Track ${index + 1}`;
5426
+ const trackArtist = track.artist ? ` by ${track.artist}` : "";
5473
5427
  if (index === this.currentIndex) {
5474
5428
  item.classList.add("vidply-playlist-item-active");
5429
+ item.setAttribute("aria-current", "true");
5430
+ item.setAttribute("tabIndex", "0");
5431
+ const statusText = "Currently playing";
5432
+ const actionText = "Press Enter to restart";
5433
+ item.setAttribute("aria-label", `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
5475
5434
  item.scrollIntoView({ behavior: "smooth", block: "nearest" });
5476
5435
  } else {
5477
5436
  item.classList.remove("vidply-playlist-item-active");
5437
+ item.removeAttribute("aria-current");
5438
+ item.setAttribute("tabIndex", "-1");
5439
+ const statusText = "Not playing";
5440
+ const actionText = "Press Enter to play";
5441
+ item.setAttribute("aria-label", `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
5478
5442
  }
5479
5443
  });
5480
5444
  }
@@ -5492,10 +5456,24 @@ var VidPly = (() => {
5492
5456
  currentIndex: this.currentIndex,
5493
5457
  totalTracks: this.tracks.length,
5494
5458
  currentTrack: this.getCurrentTrack(),
5495
- hasNext: this.currentIndex < this.tracks.length - 1,
5496
- hasPrevious: this.currentIndex > 0
5459
+ hasNext: this.hasNext(),
5460
+ hasPrevious: this.hasPrevious()
5497
5461
  };
5498
5462
  }
5463
+ /**
5464
+ * Check if there is a next track
5465
+ */
5466
+ hasNext() {
5467
+ if (this.options.loop) return true;
5468
+ return this.currentIndex < this.tracks.length - 1;
5469
+ }
5470
+ /**
5471
+ * Check if there is a previous track
5472
+ */
5473
+ hasPrevious() {
5474
+ if (this.options.loop) return true;
5475
+ return this.currentIndex > 0;
5476
+ }
5499
5477
  /**
5500
5478
  * Add track to playlist
5501
5479
  */