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.
@@ -1212,6 +1212,42 @@ var ControlBar = class {
1212
1212
  document.addEventListener("keydown", handleEscape);
1213
1213
  }, 100);
1214
1214
  }
1215
+ // Helper method to add keyboard navigation to menus (arrow keys)
1216
+ attachMenuKeyboardNavigation(menu) {
1217
+ const menuItems = Array.from(menu.querySelectorAll(`.${this.player.options.classPrefix}-menu-item`));
1218
+ if (menuItems.length === 0) return;
1219
+ const handleKeyDown = (e) => {
1220
+ const currentIndex = menuItems.indexOf(document.activeElement);
1221
+ switch (e.key) {
1222
+ case "ArrowDown":
1223
+ e.preventDefault();
1224
+ const nextIndex = (currentIndex + 1) % menuItems.length;
1225
+ menuItems[nextIndex].focus();
1226
+ break;
1227
+ case "ArrowUp":
1228
+ e.preventDefault();
1229
+ const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
1230
+ menuItems[prevIndex].focus();
1231
+ break;
1232
+ case "Home":
1233
+ e.preventDefault();
1234
+ menuItems[0].focus();
1235
+ break;
1236
+ case "End":
1237
+ e.preventDefault();
1238
+ menuItems[menuItems.length - 1].focus();
1239
+ break;
1240
+ case "Enter":
1241
+ case " ":
1242
+ e.preventDefault();
1243
+ if (document.activeElement && menuItems.includes(document.activeElement)) {
1244
+ document.activeElement.click();
1245
+ }
1246
+ break;
1247
+ }
1248
+ };
1249
+ menu.addEventListener("keydown", handleKeyDown);
1250
+ }
1215
1251
  createElement() {
1216
1252
  this.element = DOMUtils.createElement("div", {
1217
1253
  className: `${this.player.options.classPrefix}-controls`,
@@ -1615,12 +1651,26 @@ var ControlBar = class {
1615
1651
  }
1616
1652
  createTimeDisplay() {
1617
1653
  const container = DOMUtils.createElement("div", {
1618
- className: `${this.player.options.classPrefix}-time`
1654
+ className: `${this.player.options.classPrefix}-time`,
1655
+ attributes: {
1656
+ "role": "group",
1657
+ "aria-label": "Time display"
1658
+ }
1619
1659
  });
1620
1660
  this.controls.currentTimeDisplay = DOMUtils.createElement("span", {
1621
1661
  className: `${this.player.options.classPrefix}-current-time`,
1622
- textContent: "00:00"
1662
+ attributes: {
1663
+ "aria-label": "0 seconds"
1664
+ }
1665
+ });
1666
+ const currentTimeVisual = DOMUtils.createElement("span", {
1667
+ textContent: "00:00",
1668
+ attributes: {
1669
+ "aria-hidden": "true"
1670
+ }
1623
1671
  });
1672
+ this.controls.currentTimeDisplay.appendChild(currentTimeVisual);
1673
+ this.controls.currentTimeVisual = currentTimeVisual;
1624
1674
  const separator = DOMUtils.createElement("span", {
1625
1675
  textContent: " / ",
1626
1676
  attributes: {
@@ -1629,8 +1679,18 @@ var ControlBar = class {
1629
1679
  });
1630
1680
  this.controls.durationDisplay = DOMUtils.createElement("span", {
1631
1681
  className: `${this.player.options.classPrefix}-duration`,
1632
- textContent: "00:00"
1682
+ attributes: {
1683
+ "aria-label": "Duration: 0 seconds"
1684
+ }
1685
+ });
1686
+ const durationVisual = DOMUtils.createElement("span", {
1687
+ textContent: "00:00",
1688
+ attributes: {
1689
+ "aria-hidden": "true"
1690
+ }
1633
1691
  });
1692
+ this.controls.durationDisplay.appendChild(durationVisual);
1693
+ this.controls.durationVisual = durationVisual;
1634
1694
  container.appendChild(this.controls.currentTimeDisplay);
1635
1695
  container.appendChild(separator);
1636
1696
  container.appendChild(this.controls.durationDisplay);
@@ -1706,7 +1766,8 @@ var ControlBar = class {
1706
1766
  className: `${this.player.options.classPrefix}-menu-item`,
1707
1767
  attributes: {
1708
1768
  "type": "button",
1709
- "role": "menuitem"
1769
+ "role": "menuitem",
1770
+ "tabindex": "-1"
1710
1771
  }
1711
1772
  });
1712
1773
  const timeLabel = DOMUtils.createElement("span", {
@@ -1726,6 +1787,13 @@ var ControlBar = class {
1726
1787
  });
1727
1788
  menu.appendChild(item);
1728
1789
  }
1790
+ this.attachMenuKeyboardNavigation(menu);
1791
+ setTimeout(() => {
1792
+ const firstItem = menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
1793
+ if (firstItem) {
1794
+ firstItem.focus();
1795
+ }
1796
+ }, 0);
1729
1797
  }
1730
1798
  }
1731
1799
  button.appendChild(menu);
@@ -1779,19 +1847,22 @@ var ControlBar = class {
1779
1847
  });
1780
1848
  menu.appendChild(noQualityItem);
1781
1849
  } else {
1850
+ let activeItem = null;
1782
1851
  if (isHLS) {
1783
1852
  const autoItem = DOMUtils.createElement("button", {
1784
1853
  className: `${this.player.options.classPrefix}-menu-item`,
1785
1854
  textContent: i18n.t("player.auto"),
1786
1855
  attributes: {
1787
1856
  "type": "button",
1788
- "role": "menuitem"
1857
+ "role": "menuitem",
1858
+ "tabindex": "-1"
1789
1859
  }
1790
1860
  });
1791
1861
  const isAuto = this.player.renderer.hls && this.player.renderer.hls.currentLevel === -1;
1792
1862
  if (isAuto) {
1793
1863
  autoItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1794
1864
  autoItem.appendChild(createIconElement("check"));
1865
+ activeItem = autoItem;
1795
1866
  }
1796
1867
  autoItem.addEventListener("click", () => {
1797
1868
  if (this.player.renderer.switchQuality) {
@@ -1807,12 +1878,14 @@ var ControlBar = class {
1807
1878
  textContent: quality.name || `${quality.height}p`,
1808
1879
  attributes: {
1809
1880
  "type": "button",
1810
- "role": "menuitem"
1881
+ "role": "menuitem",
1882
+ "tabindex": "-1"
1811
1883
  }
1812
1884
  });
1813
1885
  if (quality.index === currentQuality) {
1814
1886
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1815
1887
  item.appendChild(createIconElement("check"));
1888
+ activeItem = item;
1816
1889
  }
1817
1890
  item.addEventListener("click", () => {
1818
1891
  if (this.player.renderer.switchQuality) {
@@ -1822,6 +1895,13 @@ var ControlBar = class {
1822
1895
  });
1823
1896
  menu.appendChild(item);
1824
1897
  });
1898
+ this.attachMenuKeyboardNavigation(menu);
1899
+ setTimeout(() => {
1900
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
1901
+ if (focusTarget) {
1902
+ focusTarget.focus();
1903
+ }
1904
+ }, 0);
1825
1905
  }
1826
1906
  } else {
1827
1907
  const noSupportItem = DOMUtils.createElement("div", {
@@ -1915,6 +1995,12 @@ var ControlBar = class {
1915
1995
  menu.style.minWidth = "220px";
1916
1996
  button.appendChild(menu);
1917
1997
  this.attachMenuCloseHandler(menu, button, true);
1998
+ setTimeout(() => {
1999
+ const firstSelect = menu.querySelector("select");
2000
+ if (firstSelect) {
2001
+ firstSelect.focus();
2002
+ }
2003
+ }, 0);
1918
2004
  }
1919
2005
  createStyleControl(label, property, options) {
1920
2006
  const group = DOMUtils.createElement("div", {
@@ -2127,18 +2213,21 @@ var ControlBar = class {
2127
2213
  }
2128
2214
  });
2129
2215
  const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
2216
+ let activeItem = null;
2130
2217
  speeds.forEach((speed) => {
2131
2218
  const item = DOMUtils.createElement("button", {
2132
2219
  className: `${this.player.options.classPrefix}-menu-item`,
2133
2220
  textContent: this.formatSpeedLabel(speed),
2134
2221
  attributes: {
2135
2222
  "type": "button",
2136
- "role": "menuitem"
2223
+ "role": "menuitem",
2224
+ "tabindex": "-1"
2137
2225
  }
2138
2226
  });
2139
2227
  if (speed === this.player.state.playbackSpeed) {
2140
2228
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
2141
2229
  item.appendChild(createIconElement("check"));
2230
+ activeItem = item;
2142
2231
  }
2143
2232
  item.addEventListener("click", () => {
2144
2233
  this.player.setPlaybackSpeed(speed);
@@ -2147,7 +2236,14 @@ var ControlBar = class {
2147
2236
  menu.appendChild(item);
2148
2237
  });
2149
2238
  button.appendChild(menu);
2239
+ this.attachMenuKeyboardNavigation(menu);
2150
2240
  this.attachMenuCloseHandler(menu, button);
2241
+ setTimeout(() => {
2242
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
2243
+ if (focusTarget) {
2244
+ focusTarget.focus();
2245
+ }
2246
+ }, 0);
2151
2247
  }
2152
2248
  createCaptionsButton() {
2153
2249
  const button = DOMUtils.createElement("button", {
@@ -2190,17 +2286,20 @@ var ControlBar = class {
2190
2286
  this.attachMenuCloseHandler(menu, button);
2191
2287
  return;
2192
2288
  }
2289
+ let activeItem = null;
2193
2290
  const offItem = DOMUtils.createElement("button", {
2194
2291
  className: `${this.player.options.classPrefix}-menu-item`,
2195
2292
  textContent: i18n.t("captions.off"),
2196
2293
  attributes: {
2197
2294
  "type": "button",
2198
- "role": "menuitem"
2295
+ "role": "menuitem",
2296
+ "tabindex": "-1"
2199
2297
  }
2200
2298
  });
2201
2299
  if (!this.player.state.captionsEnabled) {
2202
2300
  offItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
2203
2301
  offItem.appendChild(createIconElement("check"));
2302
+ activeItem = offItem;
2204
2303
  }
2205
2304
  offItem.addEventListener("click", () => {
2206
2305
  this.player.disableCaptions();
@@ -2216,12 +2315,14 @@ var ControlBar = class {
2216
2315
  attributes: {
2217
2316
  "type": "button",
2218
2317
  "role": "menuitem",
2219
- "lang": track.language
2318
+ "lang": track.language,
2319
+ "tabindex": "-1"
2220
2320
  }
2221
2321
  });
2222
2322
  if (this.player.state.captionsEnabled && this.player.captionManager.currentTrack === this.player.captionManager.tracks[track.index]) {
2223
2323
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
2224
2324
  item.appendChild(createIconElement("check"));
2325
+ activeItem = item;
2225
2326
  }
2226
2327
  item.addEventListener("click", () => {
2227
2328
  this.player.captionManager.switchTrack(track.index);
@@ -2231,7 +2332,14 @@ var ControlBar = class {
2231
2332
  menu.appendChild(item);
2232
2333
  });
2233
2334
  button.appendChild(menu);
2335
+ this.attachMenuKeyboardNavigation(menu);
2234
2336
  this.attachMenuCloseHandler(menu, button);
2337
+ setTimeout(() => {
2338
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
2339
+ if (focusTarget) {
2340
+ focusTarget.focus();
2341
+ }
2342
+ }, 0);
2235
2343
  }
2236
2344
  updateCaptionsButton() {
2237
2345
  if (!this.controls.captions) return;
@@ -2406,36 +2514,43 @@ var ControlBar = class {
2406
2514
  const percent = this.player.state.currentTime / this.player.state.duration * 100;
2407
2515
  this.controls.played.style.width = `${percent}%`;
2408
2516
  this.controls.progress.setAttribute("aria-valuenow", String(Math.round(percent)));
2409
- if (this.controls.currentTimeDisplay) {
2410
- this.controls.currentTimeDisplay.textContent = TimeUtils.formatTime(this.player.state.currentTime);
2517
+ if (this.controls.currentTimeVisual) {
2518
+ const currentTime = this.player.state.currentTime;
2519
+ this.controls.currentTimeVisual.textContent = TimeUtils.formatTime(currentTime);
2520
+ this.controls.currentTimeDisplay.setAttribute("aria-label", TimeUtils.formatDuration(currentTime));
2411
2521
  }
2412
2522
  }
2413
2523
  updateDuration() {
2414
- if (this.controls.durationDisplay) {
2415
- this.controls.durationDisplay.textContent = TimeUtils.formatTime(this.player.state.duration);
2524
+ if (this.controls.durationVisual) {
2525
+ const duration = this.player.state.duration;
2526
+ this.controls.durationVisual.textContent = TimeUtils.formatTime(duration);
2527
+ this.controls.durationDisplay.setAttribute("aria-label", "Duration: " + TimeUtils.formatDuration(duration));
2416
2528
  }
2417
2529
  }
2418
2530
  updateVolumeDisplay() {
2419
- if (!this.controls.volumeFill) return;
2420
2531
  const percent = this.player.state.volume * 100;
2421
- this.controls.volumeFill.style.height = `${percent}%`;
2532
+ if (this.controls.volumeFill) {
2533
+ this.controls.volumeFill.style.height = `${percent}%`;
2534
+ }
2422
2535
  if (this.controls.mute) {
2423
2536
  const icon = this.controls.mute.querySelector(".vidply-icon");
2424
- let iconName;
2425
- if (this.player.state.muted || this.player.state.volume === 0) {
2426
- iconName = "volumeMuted";
2427
- } else if (this.player.state.volume < 0.3) {
2428
- iconName = "volumeLow";
2429
- } else if (this.player.state.volume < 0.7) {
2430
- iconName = "volumeMedium";
2431
- } else {
2432
- iconName = "volumeHigh";
2537
+ if (icon) {
2538
+ let iconName;
2539
+ if (this.player.state.muted || this.player.state.volume === 0) {
2540
+ iconName = "volumeMuted";
2541
+ } else if (this.player.state.volume < 0.3) {
2542
+ iconName = "volumeLow";
2543
+ } else if (this.player.state.volume < 0.7) {
2544
+ iconName = "volumeMedium";
2545
+ } else {
2546
+ iconName = "volumeHigh";
2547
+ }
2548
+ icon.innerHTML = createIconElement(iconName).innerHTML;
2549
+ this.controls.mute.setAttribute(
2550
+ "aria-label",
2551
+ this.player.state.muted ? i18n.t("player.unmute") : i18n.t("player.mute")
2552
+ );
2433
2553
  }
2434
- icon.innerHTML = createIconElement(iconName).innerHTML;
2435
- this.controls.mute.setAttribute(
2436
- "aria-label",
2437
- this.player.state.muted ? i18n.t("player.unmute") : i18n.t("player.mute")
2438
- );
2439
2554
  }
2440
2555
  if (this.controls.volumeSlider) {
2441
2556
  this.controls.volumeSlider.setAttribute("aria-valuenow", String(Math.round(percent)));
@@ -2620,7 +2735,6 @@ var CaptionManager = class {
2620
2735
  this.updateCaptions();
2621
2736
  };
2622
2737
  selectedTrack.track.addEventListener("cuechange", this.cueChangeHandler);
2623
- this.element.style.display = "block";
2624
2738
  this.player.emit("captionsenabled", selectedTrack);
2625
2739
  }
2626
2740
  }
@@ -2763,6 +2877,9 @@ var KeyboardManager = class {
2763
2877
  }
2764
2878
  }
2765
2879
  }
2880
+ if (!handled && this.player.options.debug) {
2881
+ console.log("[VidPly] Unhandled key:", e.key, "code:", e.code, "shiftKey:", e.shiftKey);
2882
+ }
2766
2883
  }
2767
2884
  executeAction(action, event) {
2768
2885
  switch (action) {
@@ -2781,12 +2898,6 @@ var KeyboardManager = class {
2781
2898
  case "seek-backward":
2782
2899
  this.player.seekBackward();
2783
2900
  return true;
2784
- case "seek-forward-large":
2785
- this.player.seekForward(this.player.options.seekIntervalLarge);
2786
- return true;
2787
- case "seek-backward-large":
2788
- this.player.seekBackward(this.player.options.seekIntervalLarge);
2789
- return true;
2790
2901
  case "mute":
2791
2902
  this.player.toggleMute();
2792
2903
  return true;
@@ -2795,14 +2906,22 @@ var KeyboardManager = class {
2795
2906
  return true;
2796
2907
  case "captions":
2797
2908
  if (this.player.captionManager && this.player.captionManager.tracks.length > 1) {
2798
- const captionsButton = document.querySelector(".vidply-captions");
2799
- if (captionsButton && this.player.controlBar) {
2909
+ const captionsButton = this.player.controlBar && this.player.controlBar.controls.captions;
2910
+ if (captionsButton) {
2800
2911
  this.player.controlBar.showCaptionsMenu(captionsButton);
2912
+ } else {
2913
+ this.player.toggleCaptions();
2801
2914
  }
2802
2915
  } else {
2803
2916
  this.player.toggleCaptions();
2804
2917
  }
2805
2918
  return true;
2919
+ case "caption-style-menu":
2920
+ if (this.player.controlBar && this.player.controlBar.controls.captionStyle) {
2921
+ this.player.controlBar.showCaptionStyleMenu(this.player.controlBar.controls.captionStyle);
2922
+ return true;
2923
+ }
2924
+ return false;
2806
2925
  case "speed-up":
2807
2926
  this.player.setPlaybackSpeed(
2808
2927
  Math.min(2, this.player.state.playbackSpeed + 0.25)
@@ -2813,9 +2932,30 @@ var KeyboardManager = class {
2813
2932
  Math.max(0.25, this.player.state.playbackSpeed - 0.25)
2814
2933
  );
2815
2934
  return true;
2816
- case "settings":
2817
- this.player.showSettings();
2818
- return true;
2935
+ case "speed-menu":
2936
+ if (this.player.controlBar && this.player.controlBar.controls.speed) {
2937
+ this.player.controlBar.showSpeedMenu(this.player.controlBar.controls.speed);
2938
+ return true;
2939
+ }
2940
+ return false;
2941
+ case "quality-menu":
2942
+ if (this.player.controlBar && this.player.controlBar.controls.quality) {
2943
+ this.player.controlBar.showQualityMenu(this.player.controlBar.controls.quality);
2944
+ return true;
2945
+ }
2946
+ return false;
2947
+ case "chapters-menu":
2948
+ if (this.player.controlBar && this.player.controlBar.controls.chapters) {
2949
+ this.player.controlBar.showChaptersMenu(this.player.controlBar.controls.chapters);
2950
+ return true;
2951
+ }
2952
+ return false;
2953
+ case "transcript-toggle":
2954
+ if (this.player.transcriptManager) {
2955
+ this.player.transcriptManager.toggleTranscript();
2956
+ return true;
2957
+ }
2958
+ return false;
2819
2959
  default:
2820
2960
  return false;
2821
2961
  }
@@ -2882,317 +3022,6 @@ var KeyboardManager = class {
2882
3022
  }
2883
3023
  };
2884
3024
 
2885
- // src/controls/SettingsDialog.js
2886
- var SettingsDialog = class {
2887
- constructor(player) {
2888
- this.player = player;
2889
- this.element = null;
2890
- this.isOpen = false;
2891
- this.init();
2892
- }
2893
- init() {
2894
- this.createElement();
2895
- }
2896
- createElement() {
2897
- this.overlay = DOMUtils.createElement("div", {
2898
- className: `${this.player.options.classPrefix}-settings-overlay`,
2899
- attributes: {
2900
- "role": "dialog",
2901
- "aria-modal": "true",
2902
- "aria-label": i18n.t("settings.title")
2903
- }
2904
- });
2905
- this.overlay.style.display = "none";
2906
- this.element = DOMUtils.createElement("div", {
2907
- className: `${this.player.options.classPrefix}-settings-dialog`
2908
- });
2909
- const header = DOMUtils.createElement("div", {
2910
- className: `${this.player.options.classPrefix}-settings-header`
2911
- });
2912
- const title = DOMUtils.createElement("h2", {
2913
- textContent: i18n.t("settings.title"),
2914
- attributes: {
2915
- "id": `${this.player.options.classPrefix}-settings-title`
2916
- }
2917
- });
2918
- const closeButton = DOMUtils.createElement("button", {
2919
- className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-settings-close`,
2920
- attributes: {
2921
- "type": "button",
2922
- "aria-label": i18n.t("settings.close")
2923
- }
2924
- });
2925
- closeButton.appendChild(createIconElement("close"));
2926
- closeButton.addEventListener("click", () => this.hide());
2927
- header.appendChild(title);
2928
- header.appendChild(closeButton);
2929
- const content = DOMUtils.createElement("div", {
2930
- className: `${this.player.options.classPrefix}-settings-content`
2931
- });
2932
- content.appendChild(this.createSpeedSettings());
2933
- if (this.player.captionManager && this.player.captionManager.tracks.length > 0) {
2934
- content.appendChild(this.createCaptionSettings());
2935
- }
2936
- const footer = DOMUtils.createElement("div", {
2937
- className: `${this.player.options.classPrefix}-settings-footer`
2938
- });
2939
- const resetButton = DOMUtils.createElement("button", {
2940
- className: `${this.player.options.classPrefix}-button`,
2941
- textContent: i18n.t("settings.reset"),
2942
- attributes: {
2943
- "type": "button"
2944
- }
2945
- });
2946
- resetButton.addEventListener("click", () => this.resetSettings());
2947
- footer.appendChild(resetButton);
2948
- this.element.appendChild(header);
2949
- this.element.appendChild(content);
2950
- this.element.appendChild(footer);
2951
- this.overlay.appendChild(this.element);
2952
- this.player.container.appendChild(this.overlay);
2953
- this.overlay.addEventListener("click", (e) => {
2954
- if (e.target === this.overlay) {
2955
- this.hide();
2956
- }
2957
- });
2958
- document.addEventListener("keydown", (e) => {
2959
- if (e.key === "Escape" && this.isOpen) {
2960
- this.hide();
2961
- }
2962
- });
2963
- }
2964
- formatSpeedLabel(speed) {
2965
- if (speed === 1) {
2966
- return i18n.t("speeds.normal");
2967
- }
2968
- const speedStr = speed.toLocaleString(i18n.getLanguage(), {
2969
- minimumFractionDigits: 0,
2970
- maximumFractionDigits: 2
2971
- });
2972
- return `${speedStr}\xD7`;
2973
- }
2974
- createSpeedSettings() {
2975
- const section = DOMUtils.createElement("div", {
2976
- className: `${this.player.options.classPrefix}-settings-section`
2977
- });
2978
- const label = DOMUtils.createElement("label", {
2979
- textContent: i18n.t("settings.speed"),
2980
- attributes: {
2981
- "for": `${this.player.options.classPrefix}-speed-select`
2982
- }
2983
- });
2984
- const select = DOMUtils.createElement("select", {
2985
- className: `${this.player.options.classPrefix}-settings-select`,
2986
- attributes: {
2987
- "id": `${this.player.options.classPrefix}-speed-select`
2988
- }
2989
- });
2990
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
2991
- speeds.forEach((speed) => {
2992
- const option = DOMUtils.createElement("option", {
2993
- textContent: this.formatSpeedLabel(speed),
2994
- attributes: {
2995
- "value": String(speed)
2996
- }
2997
- });
2998
- if (speed === this.player.state.playbackSpeed) {
2999
- option.selected = true;
3000
- }
3001
- select.appendChild(option);
3002
- });
3003
- select.addEventListener("change", (e) => {
3004
- this.player.setPlaybackSpeed(parseFloat(e.target.value));
3005
- });
3006
- section.appendChild(label);
3007
- section.appendChild(select);
3008
- return section;
3009
- }
3010
- createCaptionSettings() {
3011
- const section = DOMUtils.createElement("div", {
3012
- className: `${this.player.options.classPrefix}-settings-section`
3013
- });
3014
- const heading = DOMUtils.createElement("h3", {
3015
- textContent: i18n.t("settings.captions")
3016
- });
3017
- section.appendChild(heading);
3018
- const trackLabel = DOMUtils.createElement("label", {
3019
- textContent: i18n.t("captions.select"),
3020
- attributes: {
3021
- "for": `${this.player.options.classPrefix}-caption-track-select`
3022
- }
3023
- });
3024
- const trackSelect = DOMUtils.createElement("select", {
3025
- className: `${this.player.options.classPrefix}-settings-select`,
3026
- attributes: {
3027
- "id": `${this.player.options.classPrefix}-caption-track-select`
3028
- }
3029
- });
3030
- const offOption = DOMUtils.createElement("option", {
3031
- textContent: i18n.t("captions.off"),
3032
- attributes: { "value": "-1" }
3033
- });
3034
- trackSelect.appendChild(offOption);
3035
- const tracks = this.player.captionManager.getAvailableTracks();
3036
- tracks.forEach((track) => {
3037
- const option = DOMUtils.createElement("option", {
3038
- textContent: track.label,
3039
- attributes: { "value": String(track.index) }
3040
- });
3041
- trackSelect.appendChild(option);
3042
- });
3043
- trackSelect.addEventListener("change", (e) => {
3044
- const index = parseInt(e.target.value);
3045
- if (index === -1) {
3046
- this.player.disableCaptions();
3047
- } else {
3048
- this.player.captionManager.switchTrack(index);
3049
- }
3050
- });
3051
- section.appendChild(trackLabel);
3052
- section.appendChild(trackSelect);
3053
- section.appendChild(this.createCaptionStyleControl("fontSize", i18n.t("captions.fontSize"), [
3054
- { label: i18n.t("fontSizes.small"), value: "80%" },
3055
- { label: i18n.t("fontSizes.medium"), value: "100%" },
3056
- { label: i18n.t("fontSizes.large"), value: "120%" },
3057
- { label: i18n.t("fontSizes.xlarge"), value: "150%" }
3058
- ]));
3059
- section.appendChild(this.createCaptionStyleControl("fontFamily", i18n.t("captions.fontFamily"), [
3060
- { label: i18n.t("fontFamilies.sansSerif"), value: "sans-serif" },
3061
- { label: i18n.t("fontFamilies.serif"), value: "serif" },
3062
- { label: i18n.t("fontFamilies.monospace"), value: "monospace" }
3063
- ]));
3064
- section.appendChild(this.createColorControl("color", i18n.t("captions.color")));
3065
- section.appendChild(this.createColorControl("backgroundColor", i18n.t("captions.backgroundColor")));
3066
- section.appendChild(this.createRangeControl("opacity", i18n.t("captions.opacity"), 0, 1, 0.1));
3067
- return section;
3068
- }
3069
- createCaptionStyleControl(property, label, options) {
3070
- const wrapper = DOMUtils.createElement("div", {
3071
- className: `${this.player.options.classPrefix}-settings-control`
3072
- });
3073
- const labelEl = DOMUtils.createElement("label", {
3074
- textContent: label,
3075
- attributes: {
3076
- "for": `${this.player.options.classPrefix}-caption-${property}`
3077
- }
3078
- });
3079
- const select = DOMUtils.createElement("select", {
3080
- className: `${this.player.options.classPrefix}-settings-select`,
3081
- attributes: {
3082
- "id": `${this.player.options.classPrefix}-caption-${property}`
3083
- }
3084
- });
3085
- options.forEach((opt) => {
3086
- const option = DOMUtils.createElement("option", {
3087
- textContent: opt.label,
3088
- attributes: { "value": opt.value }
3089
- });
3090
- if (opt.value === this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]) {
3091
- option.selected = true;
3092
- }
3093
- select.appendChild(option);
3094
- });
3095
- select.addEventListener("change", (e) => {
3096
- this.player.captionManager.setCaptionStyle(property, e.target.value);
3097
- });
3098
- wrapper.appendChild(labelEl);
3099
- wrapper.appendChild(select);
3100
- return wrapper;
3101
- }
3102
- createColorControl(property, label) {
3103
- const wrapper = DOMUtils.createElement("div", {
3104
- className: `${this.player.options.classPrefix}-settings-control`
3105
- });
3106
- const labelEl = DOMUtils.createElement("label", {
3107
- textContent: label,
3108
- attributes: {
3109
- "for": `${this.player.options.classPrefix}-caption-${property}`
3110
- }
3111
- });
3112
- const input = DOMUtils.createElement("input", {
3113
- className: `${this.player.options.classPrefix}-settings-color`,
3114
- attributes: {
3115
- "type": "color",
3116
- "id": `${this.player.options.classPrefix}-caption-${property}`,
3117
- "value": this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`]
3118
- }
3119
- });
3120
- input.addEventListener("change", (e) => {
3121
- this.player.captionManager.setCaptionStyle(property, e.target.value);
3122
- });
3123
- wrapper.appendChild(labelEl);
3124
- wrapper.appendChild(input);
3125
- return wrapper;
3126
- }
3127
- createRangeControl(property, label, min, max, step) {
3128
- const wrapper = DOMUtils.createElement("div", {
3129
- className: `${this.player.options.classPrefix}-settings-control`
3130
- });
3131
- const labelEl = DOMUtils.createElement("label", {
3132
- textContent: label,
3133
- attributes: {
3134
- "for": `${this.player.options.classPrefix}-caption-${property}`
3135
- }
3136
- });
3137
- const input = DOMUtils.createElement("input", {
3138
- className: `${this.player.options.classPrefix}-settings-range`,
3139
- attributes: {
3140
- "type": "range",
3141
- "id": `${this.player.options.classPrefix}-caption-${property}`,
3142
- "min": String(min),
3143
- "max": String(max),
3144
- "step": String(step),
3145
- "value": String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
3146
- }
3147
- });
3148
- const valueDisplay = DOMUtils.createElement("span", {
3149
- className: `${this.player.options.classPrefix}-settings-value`,
3150
- textContent: String(this.player.options[`captions${property.charAt(0).toUpperCase() + property.slice(1)}`])
3151
- });
3152
- input.addEventListener("input", (e) => {
3153
- const value = parseFloat(e.target.value);
3154
- valueDisplay.textContent = value.toFixed(1);
3155
- this.player.captionManager.setCaptionStyle(property, value);
3156
- });
3157
- wrapper.appendChild(labelEl);
3158
- wrapper.appendChild(input);
3159
- wrapper.appendChild(valueDisplay);
3160
- return wrapper;
3161
- }
3162
- resetSettings() {
3163
- this.player.setPlaybackSpeed(1);
3164
- if (this.player.captionManager) {
3165
- this.player.captionManager.setCaptionStyle("fontSize", "100%");
3166
- this.player.captionManager.setCaptionStyle("fontFamily", "sans-serif");
3167
- this.player.captionManager.setCaptionStyle("color", "#FFFFFF");
3168
- this.player.captionManager.setCaptionStyle("backgroundColor", "#000000");
3169
- this.player.captionManager.setCaptionStyle("opacity", 0.8);
3170
- }
3171
- this.hide();
3172
- setTimeout(() => this.show(), 100);
3173
- }
3174
- show() {
3175
- this.overlay.style.display = "flex";
3176
- this.isOpen = true;
3177
- const closeButton = this.element.querySelector(`.${this.player.options.classPrefix}-settings-close`);
3178
- if (closeButton) {
3179
- closeButton.focus();
3180
- }
3181
- this.player.emit("settingsopen");
3182
- }
3183
- hide() {
3184
- this.overlay.style.display = "none";
3185
- this.isOpen = false;
3186
- this.player.container.focus();
3187
- this.player.emit("settingsclose");
3188
- }
3189
- destroy() {
3190
- if (this.overlay && this.overlay.parentNode) {
3191
- this.overlay.parentNode.removeChild(this.overlay);
3192
- }
3193
- }
3194
- };
3195
-
3196
3025
  // src/controls/TranscriptManager.js
3197
3026
  var TranscriptManager = class {
3198
3027
  constructor(player) {
@@ -4480,16 +4309,18 @@ var Player = class extends EventEmitter {
4480
4309
  "play-pause": [" ", "p", "k"],
4481
4310
  "volume-up": ["ArrowUp"],
4482
4311
  "volume-down": ["ArrowDown"],
4483
- "seek-forward": ["ArrowRight", "f"],
4484
- "seek-backward": ["ArrowLeft", "r"],
4485
- "seek-forward-large": ["l"],
4486
- "seek-backward-large": ["j"],
4312
+ "seek-forward": ["ArrowRight"],
4313
+ "seek-backward": ["ArrowLeft"],
4487
4314
  "mute": ["m"],
4488
4315
  "fullscreen": ["f"],
4489
4316
  "captions": ["c"],
4317
+ "caption-style-menu": ["a"],
4490
4318
  "speed-up": [">"],
4491
4319
  "speed-down": ["<"],
4492
- "settings": ["s"]
4320
+ "speed-menu": ["s"],
4321
+ "quality-menu": ["q"],
4322
+ "chapters-menu": ["j"],
4323
+ "transcript-toggle": ["t"]
4493
4324
  },
4494
4325
  // Accessibility
4495
4326
  ariaLabels: {},
@@ -4578,9 +4409,6 @@ var Player = class extends EventEmitter {
4578
4409
  if (this.options.keyboard) {
4579
4410
  this.keyboardManager = new KeyboardManager(this);
4580
4411
  }
4581
- if (this.options.settingsButton) {
4582
- this.settingsDialog = new SettingsDialog(this);
4583
- }
4584
4412
  this.setupResponsiveHandlers();
4585
4413
  if (this.options.startTime > 0) {
4586
4414
  this.seek(this.options.startTime);
@@ -4758,6 +4586,17 @@ var Player = class extends EventEmitter {
4758
4586
  this.captionManager.destroy();
4759
4587
  this.captionManager = new CaptionManager(this);
4760
4588
  }
4589
+ if (this.transcriptManager) {
4590
+ const wasVisible = this.transcriptManager.isVisible;
4591
+ this.transcriptManager.destroy();
4592
+ this.transcriptManager = new TranscriptManager(this);
4593
+ if (wasVisible) {
4594
+ this.transcriptManager.showTranscript();
4595
+ }
4596
+ }
4597
+ if (this.controlBar) {
4598
+ this.updateControlBar();
4599
+ }
4761
4600
  this.emit("sourcechange", config);
4762
4601
  this.log("Media loaded successfully");
4763
4602
  } catch (error) {
@@ -4769,6 +4608,17 @@ var Player = class extends EventEmitter {
4769
4608
  * @param {string} src - New source URL
4770
4609
  * @returns {boolean}
4771
4610
  */
4611
+ /**
4612
+ * Update control bar to refresh button visibility based on available features
4613
+ */
4614
+ updateControlBar() {
4615
+ if (!this.controlBar) return;
4616
+ const controlBar = this.controlBar;
4617
+ controlBar.element.innerHTML = "";
4618
+ controlBar.createControls();
4619
+ controlBar.attachEvents();
4620
+ controlBar.setupAutoHide();
4621
+ }
4772
4622
  shouldChangeRenderer(src) {
4773
4623
  if (!this.renderer) return true;
4774
4624
  const isYouTube = src.includes("youtube.com") || src.includes("youtu.be");
@@ -4833,12 +4683,14 @@ var Player = class extends EventEmitter {
4833
4683
  this.renderer.setMuted(true);
4834
4684
  }
4835
4685
  this.state.muted = true;
4686
+ this.emit("volumechange");
4836
4687
  }
4837
4688
  unmute() {
4838
4689
  if (this.renderer) {
4839
4690
  this.renderer.setMuted(false);
4840
4691
  }
4841
4692
  this.state.muted = false;
4693
+ this.emit("volumechange");
4842
4694
  }
4843
4695
  toggleMute() {
4844
4696
  if (this.state.muted) {
@@ -5072,15 +4924,11 @@ var Player = class extends EventEmitter {
5072
4924
  }
5073
4925
  }
5074
4926
  // Settings
4927
+ // Settings dialog removed - using individual control buttons instead
5075
4928
  showSettings() {
5076
- if (this.settingsDialog) {
5077
- this.settingsDialog.show();
5078
- }
4929
+ console.warn("[VidPly] Settings dialog has been removed. Use individual control buttons (speed, captions, etc.)");
5079
4930
  }
5080
4931
  hideSettings() {
5081
- if (this.settingsDialog) {
5082
- this.settingsDialog.hide();
5083
- }
5084
4932
  }
5085
4933
  // Utility methods
5086
4934
  getCurrentTime() {
@@ -5177,9 +5025,6 @@ var Player = class extends EventEmitter {
5177
5025
  if (this.keyboardManager) {
5178
5026
  this.keyboardManager.destroy();
5179
5027
  }
5180
- if (this.settingsDialog) {
5181
- this.settingsDialog.destroy();
5182
- }
5183
5028
  if (this.transcriptManager) {
5184
5029
  this.transcriptManager.destroy();
5185
5030
  }
@@ -5233,7 +5078,9 @@ var PlaylistManager = class {
5233
5078
  this.trackInfoElement = null;
5234
5079
  this.handleTrackEnd = this.handleTrackEnd.bind(this);
5235
5080
  this.handleTrackError = this.handleTrackError.bind(this);
5081
+ this.player.playlistManager = this;
5236
5082
  this.init();
5083
+ this.updatePlayerControls();
5237
5084
  }
5238
5085
  init() {
5239
5086
  this.player.on("ended", this.handleTrackEnd);
@@ -5242,6 +5089,17 @@ var PlaylistManager = class {
5242
5089
  this.createUI();
5243
5090
  }
5244
5091
  }
5092
+ /**
5093
+ * Update player controls to add playlist navigation buttons
5094
+ */
5095
+ updatePlayerControls() {
5096
+ if (!this.player.controlBar) return;
5097
+ const controlBar = this.player.controlBar;
5098
+ controlBar.element.innerHTML = "";
5099
+ controlBar.createControls();
5100
+ controlBar.attachEvents();
5101
+ controlBar.setupAutoHide();
5102
+ }
5245
5103
  /**
5246
5104
  * Load a playlist
5247
5105
  * @param {Array} tracks - Array of track objects
@@ -5262,8 +5120,9 @@ var PlaylistManager = class {
5262
5120
  /**
5263
5121
  * Play a specific track
5264
5122
  * @param {number} index - Track index
5123
+ * @param {boolean} userInitiated - Whether this was triggered by user action (default: false)
5265
5124
  */
5266
- play(index) {
5125
+ play(index, userInitiated = false) {
5267
5126
  if (index < 0 || index >= this.tracks.length) {
5268
5127
  console.warn("VidPly Playlist: Invalid track index", index);
5269
5128
  return;
@@ -5283,6 +5142,9 @@ var PlaylistManager = class {
5283
5142
  item: track,
5284
5143
  total: this.tracks.length
5285
5144
  });
5145
+ if (userInitiated && this.player.container) {
5146
+ this.player.container.focus();
5147
+ }
5286
5148
  setTimeout(() => {
5287
5149
  this.player.play();
5288
5150
  }, 100);
@@ -5344,12 +5206,17 @@ var PlaylistManager = class {
5344
5206
  return;
5345
5207
  }
5346
5208
  this.trackInfoElement = DOMUtils.createElement("div", {
5347
- className: "vidply-track-info"
5209
+ className: "vidply-track-info",
5210
+ role: "status",
5211
+ "aria-live": "polite",
5212
+ "aria-atomic": "true"
5348
5213
  });
5349
5214
  this.trackInfoElement.style.display = "none";
5350
5215
  this.container.appendChild(this.trackInfoElement);
5351
5216
  this.playlistPanel = DOMUtils.createElement("div", {
5352
- className: "vidply-playlist-panel"
5217
+ className: "vidply-playlist-panel",
5218
+ role: "region",
5219
+ "aria-label": "Media playlist"
5353
5220
  });
5354
5221
  this.playlistPanel.style.display = "none";
5355
5222
  this.container.appendChild(this.playlistPanel);
@@ -5361,10 +5228,14 @@ var PlaylistManager = class {
5361
5228
  if (!this.trackInfoElement) return;
5362
5229
  const trackNumber = this.currentIndex + 1;
5363
5230
  const totalTracks = this.tracks.length;
5231
+ const trackTitle = track.title || "Untitled";
5232
+ const trackArtist = track.artist || "";
5233
+ const announcement = `Now playing: Track ${trackNumber} of ${totalTracks}. ${trackTitle}${trackArtist ? " by " + trackArtist : ""}`;
5364
5234
  this.trackInfoElement.innerHTML = `
5365
- <div class="vidply-track-number">Track ${trackNumber} of ${totalTracks}</div>
5366
- <div class="vidply-track-title">${DOMUtils.escapeHTML(track.title || "Untitled")}</div>
5367
- ${track.artist ? `<div class="vidply-track-artist">${DOMUtils.escapeHTML(track.artist)}</div>` : ""}
5235
+ <span class="vidply-sr-only">${DOMUtils.escapeHTML(announcement)}</span>
5236
+ <div class="vidply-track-number" aria-hidden="true">Track ${trackNumber} of ${totalTracks}</div>
5237
+ <div class="vidply-track-title" aria-hidden="true">${DOMUtils.escapeHTML(trackTitle)}</div>
5238
+ ${trackArtist ? `<div class="vidply-track-artist" aria-hidden="true">${DOMUtils.escapeHTML(trackArtist)}</div>` : ""}
5368
5239
  `;
5369
5240
  this.trackInfoElement.style.display = "block";
5370
5241
  }
@@ -5374,14 +5245,29 @@ var PlaylistManager = class {
5374
5245
  renderPlaylist() {
5375
5246
  if (!this.playlistPanel) return;
5376
5247
  this.playlistPanel.innerHTML = "";
5377
- const header = DOMUtils.createElement("div", {
5378
- className: "vidply-playlist-header"
5248
+ const header = DOMUtils.createElement("h2", {
5249
+ className: "vidply-playlist-header",
5250
+ id: "vidply-playlist-heading"
5379
5251
  });
5380
5252
  header.textContent = `Playlist (${this.tracks.length})`;
5381
5253
  this.playlistPanel.appendChild(header);
5382
- const list = DOMUtils.createElement("div", {
5383
- className: "vidply-playlist-list"
5384
- });
5254
+ const instructions = DOMUtils.createElement("div", {
5255
+ className: "vidply-sr-only",
5256
+ "aria-hidden": "false"
5257
+ });
5258
+ 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.";
5259
+ this.playlistPanel.appendChild(instructions);
5260
+ const list = DOMUtils.createElement("ul", {
5261
+ className: "vidply-playlist-list",
5262
+ "aria-labelledby": "vidply-playlist-heading",
5263
+ "aria-describedby": "vidply-playlist-instructions"
5264
+ });
5265
+ const listDescription = DOMUtils.createElement("div", {
5266
+ className: "vidply-sr-only",
5267
+ id: "vidply-playlist-instructions"
5268
+ });
5269
+ listDescription.textContent = `Playlist with ${this.tracks.length} ${this.tracks.length === 1 ? "track" : "tracks"}`;
5270
+ this.playlistPanel.appendChild(listDescription);
5385
5271
  this.tracks.forEach((track, index) => {
5386
5272
  const item = this.createPlaylistItem(track, index);
5387
5273
  list.appendChild(item);
@@ -5393,20 +5279,39 @@ var PlaylistManager = class {
5393
5279
  * Create playlist item element
5394
5280
  */
5395
5281
  createPlaylistItem(track, index) {
5396
- const item = DOMUtils.createElement("div", {
5282
+ const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
5283
+ const trackTitle = track.title || `Track ${index + 1}`;
5284
+ const trackArtist = track.artist ? ` by ${track.artist}` : "";
5285
+ const isActive = index === this.currentIndex;
5286
+ const statusText = isActive ? "Currently playing" : "Not playing";
5287
+ const actionText = isActive ? "Press Enter to restart" : "Press Enter to play";
5288
+ const item = DOMUtils.createElement("li", {
5397
5289
  className: "vidply-playlist-item",
5398
- role: "button",
5399
- tabIndex: 0,
5400
- "aria-label": `Play ${track.title || "Track " + (index + 1)}`
5401
- });
5402
- if (index === this.currentIndex) {
5290
+ tabIndex: index === 0 ? 0 : -1,
5291
+ // Only first item is in tab order initially
5292
+ "aria-label": `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`,
5293
+ "aria-posinset": index + 1,
5294
+ "aria-setsize": this.tracks.length,
5295
+ "data-playlist-index": index
5296
+ });
5297
+ if (isActive) {
5403
5298
  item.classList.add("vidply-playlist-item-active");
5299
+ item.setAttribute("aria-current", "true");
5300
+ item.setAttribute("tabIndex", "0");
5404
5301
  }
5302
+ const positionInfo = DOMUtils.createElement("span", {
5303
+ className: "vidply-sr-only"
5304
+ });
5305
+ positionInfo.textContent = `${trackPosition}: `;
5306
+ item.appendChild(positionInfo);
5405
5307
  const thumbnail = DOMUtils.createElement("div", {
5406
- className: "vidply-playlist-thumbnail"
5308
+ className: "vidply-playlist-thumbnail",
5309
+ "aria-hidden": "true"
5407
5310
  });
5408
5311
  if (track.poster) {
5409
5312
  thumbnail.style.backgroundImage = `url(${track.poster})`;
5313
+ thumbnail.setAttribute("role", "img");
5314
+ thumbnail.setAttribute("aria-label", `${trackTitle} thumbnail`);
5410
5315
  } else {
5411
5316
  const icon = createIconElement("music");
5412
5317
  icon.classList.add("vidply-playlist-thumbnail-icon");
@@ -5414,12 +5319,13 @@ var PlaylistManager = class {
5414
5319
  }
5415
5320
  item.appendChild(thumbnail);
5416
5321
  const info = DOMUtils.createElement("div", {
5417
- className: "vidply-playlist-item-info"
5322
+ className: "vidply-playlist-item-info",
5323
+ "aria-hidden": "true"
5418
5324
  });
5419
5325
  const title = DOMUtils.createElement("div", {
5420
5326
  className: "vidply-playlist-item-title"
5421
5327
  });
5422
- title.textContent = track.title || `Track ${index + 1}`;
5328
+ title.textContent = trackTitle;
5423
5329
  info.appendChild(title);
5424
5330
  if (track.artist) {
5425
5331
  const artist = DOMUtils.createElement("div", {
@@ -5429,20 +5335,64 @@ var PlaylistManager = class {
5429
5335
  info.appendChild(artist);
5430
5336
  }
5431
5337
  item.appendChild(info);
5338
+ if (isActive) {
5339
+ const statusIndicator = DOMUtils.createElement("span", {
5340
+ className: "vidply-sr-only"
5341
+ });
5342
+ statusIndicator.textContent = " (Currently playing)";
5343
+ item.appendChild(statusIndicator);
5344
+ }
5432
5345
  const playIcon = createIconElement("play");
5433
5346
  playIcon.classList.add("vidply-playlist-item-icon");
5347
+ playIcon.setAttribute("aria-hidden", "true");
5434
5348
  item.appendChild(playIcon);
5435
5349
  item.addEventListener("click", () => {
5436
- this.play(index);
5350
+ this.play(index, true);
5437
5351
  });
5438
5352
  item.addEventListener("keydown", (e) => {
5439
- if (e.key === "Enter" || e.key === " ") {
5440
- e.preventDefault();
5441
- this.play(index);
5442
- }
5353
+ this.handlePlaylistItemKeydown(e, index);
5443
5354
  });
5444
5355
  return item;
5445
5356
  }
5357
+ /**
5358
+ * Handle keyboard navigation in playlist items
5359
+ */
5360
+ handlePlaylistItemKeydown(e, index) {
5361
+ const items = Array.from(this.playlistPanel.querySelectorAll(".vidply-playlist-item"));
5362
+ let newIndex = -1;
5363
+ switch (e.key) {
5364
+ case "Enter":
5365
+ case " ":
5366
+ e.preventDefault();
5367
+ this.play(index, true);
5368
+ break;
5369
+ case "ArrowDown":
5370
+ e.preventDefault();
5371
+ if (index < items.length - 1) {
5372
+ newIndex = index + 1;
5373
+ }
5374
+ break;
5375
+ case "ArrowUp":
5376
+ e.preventDefault();
5377
+ if (index > 0) {
5378
+ newIndex = index - 1;
5379
+ }
5380
+ break;
5381
+ case "Home":
5382
+ e.preventDefault();
5383
+ newIndex = 0;
5384
+ break;
5385
+ case "End":
5386
+ e.preventDefault();
5387
+ newIndex = items.length - 1;
5388
+ break;
5389
+ }
5390
+ if (newIndex !== -1 && newIndex !== index) {
5391
+ items[index].setAttribute("tabIndex", "-1");
5392
+ items[newIndex].setAttribute("tabIndex", "0");
5393
+ items[newIndex].focus();
5394
+ }
5395
+ }
5446
5396
  /**
5447
5397
  * Update playlist UI (highlight current track)
5448
5398
  */
@@ -5450,11 +5400,25 @@ var PlaylistManager = class {
5450
5400
  if (!this.playlistPanel) return;
5451
5401
  const items = this.playlistPanel.querySelectorAll(".vidply-playlist-item");
5452
5402
  items.forEach((item, index) => {
5403
+ const track = this.tracks[index];
5404
+ const trackPosition = `Track ${index + 1} of ${this.tracks.length}`;
5405
+ const trackTitle = track.title || `Track ${index + 1}`;
5406
+ const trackArtist = track.artist ? ` by ${track.artist}` : "";
5453
5407
  if (index === this.currentIndex) {
5454
5408
  item.classList.add("vidply-playlist-item-active");
5409
+ item.setAttribute("aria-current", "true");
5410
+ item.setAttribute("tabIndex", "0");
5411
+ const statusText = "Currently playing";
5412
+ const actionText = "Press Enter to restart";
5413
+ item.setAttribute("aria-label", `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
5455
5414
  item.scrollIntoView({ behavior: "smooth", block: "nearest" });
5456
5415
  } else {
5457
5416
  item.classList.remove("vidply-playlist-item-active");
5417
+ item.removeAttribute("aria-current");
5418
+ item.setAttribute("tabIndex", "-1");
5419
+ const statusText = "Not playing";
5420
+ const actionText = "Press Enter to play";
5421
+ item.setAttribute("aria-label", `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
5458
5422
  }
5459
5423
  });
5460
5424
  }
@@ -5472,10 +5436,24 @@ var PlaylistManager = class {
5472
5436
  currentIndex: this.currentIndex,
5473
5437
  totalTracks: this.tracks.length,
5474
5438
  currentTrack: this.getCurrentTrack(),
5475
- hasNext: this.currentIndex < this.tracks.length - 1,
5476
- hasPrevious: this.currentIndex > 0
5439
+ hasNext: this.hasNext(),
5440
+ hasPrevious: this.hasPrevious()
5477
5441
  };
5478
5442
  }
5443
+ /**
5444
+ * Check if there is a next track
5445
+ */
5446
+ hasNext() {
5447
+ if (this.options.loop) return true;
5448
+ return this.currentIndex < this.tracks.length - 1;
5449
+ }
5450
+ /**
5451
+ * Check if there is a previous track
5452
+ */
5453
+ hasPrevious() {
5454
+ if (this.options.loop) return true;
5455
+ return this.currentIndex > 0;
5456
+ }
5479
5457
  /**
5480
5458
  * Add track to playlist
5481
5459
  */