vidply 1.0.21 → 1.0.24

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
@@ -426,6 +426,63 @@ var VidPly = (() => {
426
426
  const safeHtml = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "").replace(/on\w+\s*=/gi, "").replace(/javascript:/gi, "");
427
427
  temp.innerHTML = safeHtml;
428
428
  return temp.innerHTML;
429
+ },
430
+ /**
431
+ * Create a tooltip element that is aria-hidden (not read by screen readers)
432
+ * @param {string} text - Tooltip text
433
+ * @param {string} classPrefix - Class prefix for styling
434
+ * @returns {HTMLElement} Tooltip element
435
+ */
436
+ createTooltip(text, classPrefix = "vidply") {
437
+ const tooltip = this.createElement("span", {
438
+ className: `${classPrefix}-tooltip`,
439
+ textContent: text,
440
+ attributes: {
441
+ "aria-hidden": "true"
442
+ }
443
+ });
444
+ return tooltip;
445
+ },
446
+ /**
447
+ * Attach a tooltip to an element
448
+ * @param {HTMLElement} element - Element to attach tooltip to
449
+ * @param {string} text - Tooltip text
450
+ * @param {string} classPrefix - Class prefix for styling
451
+ */
452
+ attachTooltip(element, text, classPrefix = "vidply") {
453
+ if (!element || !text) return;
454
+ const existingTooltip = element.querySelector(`.${classPrefix}-tooltip`);
455
+ if (existingTooltip) {
456
+ existingTooltip.remove();
457
+ }
458
+ const tooltip = this.createTooltip(text, classPrefix);
459
+ element.appendChild(tooltip);
460
+ const showTooltip = () => {
461
+ tooltip.classList.add(`${classPrefix}-tooltip-visible`);
462
+ };
463
+ const hideTooltip = () => {
464
+ tooltip.classList.remove(`${classPrefix}-tooltip-visible`);
465
+ };
466
+ element.addEventListener("mouseenter", showTooltip);
467
+ element.addEventListener("mouseleave", hideTooltip);
468
+ element.addEventListener("focus", showTooltip);
469
+ element.addEventListener("blur", hideTooltip);
470
+ },
471
+ /**
472
+ * Create visible button text that is hidden by CSS but visible when CSS is disabled
473
+ * @param {string} text - Button text
474
+ * @param {string} classPrefix - Class prefix for styling
475
+ * @returns {HTMLElement} Button text element
476
+ */
477
+ createButtonText(text, classPrefix = "vidply") {
478
+ const buttonText = this.createElement("span", {
479
+ className: `${classPrefix}-button-text`,
480
+ textContent: text,
481
+ attributes: {
482
+ "aria-hidden": "true"
483
+ }
484
+ });
485
+ return buttonText;
429
486
  }
430
487
  };
431
488
 
@@ -592,7 +649,11 @@ var VidPly = (() => {
592
649
  nowPlaying: "Now playing: Track {current} of {total}. {title}{artist}",
593
650
  by: " by ",
594
651
  untitled: "Untitled",
595
- trackUntitled: "Track {number}"
652
+ trackUntitled: "Track {number}",
653
+ currentlyPlaying: "Currently playing",
654
+ notPlaying: "Not playing",
655
+ pressEnterPlay: "Press Enter to play",
656
+ pressEnterRestart: "Press Enter to restart"
596
657
  }
597
658
  };
598
659
 
@@ -759,7 +820,11 @@ var VidPly = (() => {
759
820
  nowPlaying: "L\xE4uft gerade: Titel {current} von {total}. {title}{artist}",
760
821
  by: " von ",
761
822
  untitled: "Ohne Titel",
762
- trackUntitled: "Titel {number}"
823
+ trackUntitled: "Titel {number}",
824
+ currentlyPlaying: "Wird gerade abgespielt",
825
+ notPlaying: "Nicht aktiv",
826
+ pressEnterPlay: "Eingabetaste zum Abspielen",
827
+ pressEnterRestart: "Eingabetaste zum Neustart"
763
828
  }
764
829
  };
765
830
 
@@ -926,7 +991,11 @@ var VidPly = (() => {
926
991
  nowPlaying: "Reproduciendo ahora: Pista {current} de {total}. {title}{artist}",
927
992
  by: " por ",
928
993
  untitled: "Sin t\xEDtulo",
929
- trackUntitled: "Pista {number}"
994
+ trackUntitled: "Pista {number}",
995
+ currentlyPlaying: "Reproduciendo actualmente",
996
+ notPlaying: "Sin reproducir",
997
+ pressEnterPlay: "Pulsa Enter para reproducir",
998
+ pressEnterRestart: "Pulsa Enter para reiniciar"
930
999
  }
931
1000
  };
932
1001
 
@@ -1093,7 +1162,11 @@ var VidPly = (() => {
1093
1162
  nowPlaying: "Lecture en cours : Piste {current} sur {total}. {title}{artist}",
1094
1163
  by: " par ",
1095
1164
  untitled: "Sans titre",
1096
- trackUntitled: "Piste {number}"
1165
+ trackUntitled: "Piste {number}",
1166
+ currentlyPlaying: "En cours de lecture",
1167
+ notPlaying: "Non en lecture",
1168
+ pressEnterPlay: "Appuyez sur Entr\xE9e pour lire",
1169
+ pressEnterRestart: "Appuyez sur Entr\xE9e pour recommencer"
1097
1170
  }
1098
1171
  };
1099
1172
 
@@ -1260,7 +1333,11 @@ var VidPly = (() => {
1260
1333
  nowPlaying: "\u518D\u751F\u4E2D: \u30C8\u30E9\u30C3\u30AF {current}/{total}. {title}{artist}",
1261
1334
  by: " - ",
1262
1335
  untitled: "\u30BF\u30A4\u30C8\u30EB\u306A\u3057",
1263
- trackUntitled: "\u30C8\u30E9\u30C3\u30AF {number}"
1336
+ trackUntitled: "\u30C8\u30E9\u30C3\u30AF {number}",
1337
+ currentlyPlaying: "\u518D\u751F\u4E2D",
1338
+ notPlaying: "\u505C\u6B62\u4E2D",
1339
+ pressEnterPlay: "Enter\u30AD\u30FC\u3067\u518D\u751F",
1340
+ pressEnterRestart: "Enter\u30AD\u30FC\u3067\u6700\u521D\u304B\u3089\u518D\u751F"
1264
1341
  }
1265
1342
  };
1266
1343
 
@@ -1969,13 +2046,15 @@ var VidPly = (() => {
1969
2046
  e.preventDefault();
1970
2047
  e.stopPropagation();
1971
2048
  const nextIndex = (currentIndex + 1) % menuItems.length;
1972
- menuItems[nextIndex].focus();
2049
+ menuItems[nextIndex].focus({ preventScroll: false });
2050
+ menuItems[nextIndex].scrollIntoView({ behavior: "smooth", block: "nearest" });
1973
2051
  break;
1974
2052
  case "ArrowUp":
1975
2053
  e.preventDefault();
1976
2054
  e.stopPropagation();
1977
2055
  const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
1978
- menuItems[prevIndex].focus();
2056
+ menuItems[prevIndex].focus({ preventScroll: false });
2057
+ menuItems[prevIndex].scrollIntoView({ behavior: "smooth", block: "nearest" });
1979
2058
  break;
1980
2059
  case "ArrowLeft":
1981
2060
  case "ArrowRight":
@@ -1985,12 +2064,14 @@ var VidPly = (() => {
1985
2064
  case "Home":
1986
2065
  e.preventDefault();
1987
2066
  e.stopPropagation();
1988
- menuItems[0].focus();
2067
+ menuItems[0].focus({ preventScroll: false });
2068
+ menuItems[0].scrollIntoView({ behavior: "smooth", block: "nearest" });
1989
2069
  break;
1990
2070
  case "End":
1991
2071
  e.preventDefault();
1992
2072
  e.stopPropagation();
1993
- menuItems[menuItems.length - 1].focus();
2073
+ menuItems[menuItems.length - 1].focus({ preventScroll: false });
2074
+ menuItems[menuItems.length - 1].scrollIntoView({ behavior: "smooth", block: "nearest" });
1994
2075
  break;
1995
2076
  case "Enter":
1996
2077
  case " ":
@@ -2141,20 +2222,27 @@ var VidPly = (() => {
2141
2222
  buttonContainer.appendChild(leftButtons);
2142
2223
  buttonContainer.appendChild(this.rightButtons);
2143
2224
  this.element.appendChild(buttonContainer);
2144
- this.ensureButtonTitles(buttonContainer);
2225
+ this.ensureButtonTooltips(buttonContainer);
2145
2226
  }
2146
2227
  /**
2147
2228
  * Ensure all buttons in the controls have title attributes
2148
2229
  * Uses aria-label as title if title is not present
2149
2230
  */
2150
- ensureButtonTitles(container) {
2231
+ ensureButtonTooltips(container) {
2151
2232
  const buttons = container.querySelectorAll("button");
2152
2233
  buttons.forEach((button) => {
2153
- if (!button.hasAttribute("title")) {
2154
- const ariaLabel = button.getAttribute("aria-label");
2155
- if (ariaLabel) {
2156
- button.setAttribute("title", ariaLabel);
2157
- }
2234
+ if (button.querySelector(`.${this.player.options.classPrefix}-tooltip`)) {
2235
+ return;
2236
+ }
2237
+ if (button.querySelector(`.${this.player.options.classPrefix}-button-text`)) {
2238
+ return;
2239
+ }
2240
+ if (button.getAttribute("role") === "menuitem" || button.classList.contains(`${this.player.options.classPrefix}-settings-item`) || button.classList.contains(`${this.player.options.classPrefix}-menu-item`) || button.classList.contains(`${this.player.options.classPrefix}-transcript-settings-item`) || button.classList.contains(`${this.player.options.classPrefix}-sign-language-settings-item`) || button.classList.contains(`${this.player.options.classPrefix}-popup-settings-item`)) {
2241
+ return;
2242
+ }
2243
+ const ariaLabel = button.getAttribute("aria-label");
2244
+ if (ariaLabel) {
2245
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
2158
2246
  }
2159
2247
  });
2160
2248
  }
@@ -2254,7 +2342,6 @@ var VidPly = (() => {
2254
2342
  if (!this.isDraggingProgress) {
2255
2343
  const { time } = updateProgress(e.clientX);
2256
2344
  this.controls.progressTooltip.textContent = TimeUtils.formatTime(time);
2257
- this.controls.progressTooltip.setAttribute("aria-label", TimeUtils.formatDuration(time));
2258
2345
  this.controls.progressTooltip.style.left = `${e.clientX - progress.getBoundingClientRect().left}px`;
2259
2346
  this.controls.progressTooltip.style.display = "block";
2260
2347
  }
@@ -2727,7 +2814,7 @@ var VidPly = (() => {
2727
2814
  setTimeout(() => {
2728
2815
  const firstItem = menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
2729
2816
  if (firstItem) {
2730
- firstItem.focus();
2817
+ firstItem.focus({ preventScroll: true });
2731
2818
  }
2732
2819
  }, 0);
2733
2820
  }
@@ -2742,16 +2829,17 @@ var VidPly = (() => {
2742
2829
  this.attachMenuCloseHandler(menu, button);
2743
2830
  }
2744
2831
  createQualityButton() {
2832
+ const ariaLabel = i18n.t("player.quality");
2745
2833
  const button = DOMUtils.createElement("button", {
2746
2834
  className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-quality`,
2747
2835
  attributes: {
2748
2836
  "type": "button",
2749
- "aria-label": i18n.t("player.quality"),
2750
- "aria-expanded": "false",
2751
- "title": i18n.t("player.quality")
2837
+ "aria-label": ariaLabel,
2838
+ "aria-expanded": "false"
2752
2839
  }
2753
2840
  });
2754
2841
  button.appendChild(createIconElement("hd"));
2842
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
2755
2843
  const qualityText = DOMUtils.createElement("span", {
2756
2844
  className: `${this.player.options.classPrefix}-quality-text`,
2757
2845
  textContent: ""
@@ -2850,7 +2938,7 @@ var VidPly = (() => {
2850
2938
  setTimeout(() => {
2851
2939
  const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
2852
2940
  if (focusTarget) {
2853
- focusTarget.focus();
2941
+ focusTarget.focus({ preventScroll: true });
2854
2942
  }
2855
2943
  }, 0);
2856
2944
  }
@@ -2872,13 +2960,13 @@ var VidPly = (() => {
2872
2960
  this.attachMenuCloseHandler(menu, button);
2873
2961
  }
2874
2962
  createCaptionStyleButton() {
2963
+ const ariaLabel = i18n.t("player.captionStyling");
2875
2964
  const button = DOMUtils.createElement("button", {
2876
2965
  className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-caption-style`,
2877
2966
  attributes: {
2878
2967
  "type": "button",
2879
- "aria-label": i18n.t("player.captionStyling"),
2880
- "aria-expanded": "false",
2881
- "title": i18n.t("player.captionStyling")
2968
+ "aria-label": ariaLabel,
2969
+ "aria-expanded": "false"
2882
2970
  }
2883
2971
  });
2884
2972
  const textIcon = DOMUtils.createElement("span", {
@@ -2889,6 +2977,7 @@ var VidPly = (() => {
2889
2977
  }
2890
2978
  });
2891
2979
  button.appendChild(textIcon);
2980
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
2892
2981
  button.addEventListener("click", () => {
2893
2982
  this.showCaptionStyleMenu(button);
2894
2983
  });
@@ -3241,7 +3330,7 @@ var VidPly = (() => {
3241
3330
  setTimeout(() => {
3242
3331
  const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
3243
3332
  if (focusTarget) {
3244
- focusTarget.focus();
3333
+ focusTarget.focus({ preventScroll: true });
3245
3334
  }
3246
3335
  }, 0);
3247
3336
  }
@@ -3344,7 +3433,7 @@ var VidPly = (() => {
3344
3433
  setTimeout(() => {
3345
3434
  const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
3346
3435
  if (focusTarget) {
3347
- focusTarget.focus();
3436
+ focusTarget.focus({ preventScroll: true });
3348
3437
  }
3349
3438
  }, 0);
3350
3439
  }
@@ -3379,17 +3468,18 @@ var VidPly = (() => {
3379
3468
  this.controls.transcript.setAttribute("aria-expanded", isVisible ? "true" : "false");
3380
3469
  }
3381
3470
  createAudioDescriptionButton() {
3471
+ const ariaLabel = i18n.t("player.audioDescription");
3382
3472
  const button = DOMUtils.createElement("button", {
3383
3473
  className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-audio-description`,
3384
3474
  attributes: {
3385
3475
  "type": "button",
3386
- "aria-label": i18n.t("player.audioDescription"),
3476
+ "aria-label": ariaLabel,
3387
3477
  "role": "switch",
3388
- "aria-checked": "false",
3389
- "title": i18n.t("player.audioDescription")
3478
+ "aria-checked": "false"
3390
3479
  }
3391
3480
  });
3392
3481
  button.appendChild(createIconElement("audioDescription"));
3482
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
3393
3483
  button.addEventListener("click", async () => {
3394
3484
  await this.player.toggleAudioDescription();
3395
3485
  this.updateAudioDescriptionButton();
@@ -3405,16 +3495,17 @@ var VidPly = (() => {
3405
3495
  this.controls.audioDescription.setAttribute("aria-checked", isEnabled ? "true" : "false");
3406
3496
  }
3407
3497
  createSignLanguageButton() {
3498
+ const ariaLabel = i18n.t("player.signLanguage");
3408
3499
  const button = DOMUtils.createElement("button", {
3409
3500
  className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-sign-language`,
3410
3501
  attributes: {
3411
3502
  "type": "button",
3412
- "aria-label": i18n.t("player.signLanguage"),
3413
- "aria-expanded": "false",
3414
- "title": i18n.t("player.signLanguage")
3503
+ "aria-label": ariaLabel,
3504
+ "aria-expanded": "false"
3415
3505
  }
3416
3506
  });
3417
3507
  button.appendChild(createIconElement("signLanguage"));
3508
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
3418
3509
  button.addEventListener("click", () => {
3419
3510
  this.player.toggleSignLanguage();
3420
3511
  this.updateSignLanguageButton();
@@ -3433,6 +3524,60 @@ var VidPly = (() => {
3433
3524
  isEnabled ? i18n.t("signLanguage.hide") : i18n.t("signLanguage.show")
3434
3525
  );
3435
3526
  }
3527
+ /**
3528
+ * Update accessibility buttons visibility based on current track data.
3529
+ * Called when loading a new playlist track to show/hide buttons accordingly.
3530
+ */
3531
+ updateAccessibilityButtons() {
3532
+ const hasAudioDescription = this.hasAudioDescription();
3533
+ const hasSignLanguage = this.hasSignLanguage();
3534
+ if (hasAudioDescription) {
3535
+ if (!this.controls.audioDescription && this.player.options.audioDescriptionButton !== false) {
3536
+ const btn = this.createAudioDescriptionButton();
3537
+ btn.dataset.overflowPriority = "2";
3538
+ btn.dataset.overflowPriorityMobile = "3";
3539
+ const transcriptBtn = this.rightButtons.querySelector(`.${this.player.options.classPrefix}-transcript`);
3540
+ const playlistBtn = this.rightButtons.querySelector(`.${this.player.options.classPrefix}-playlist-toggle`);
3541
+ const insertBefore = transcriptBtn || playlistBtn || null;
3542
+ if (insertBefore) {
3543
+ this.rightButtons.insertBefore(btn, insertBefore);
3544
+ } else {
3545
+ this.rightButtons.appendChild(btn);
3546
+ }
3547
+ this.setupOverflowMenu();
3548
+ }
3549
+ if (this.controls.audioDescription) {
3550
+ this.controls.audioDescription.style.display = "";
3551
+ }
3552
+ } else {
3553
+ if (this.controls.audioDescription) {
3554
+ this.controls.audioDescription.style.display = "none";
3555
+ }
3556
+ }
3557
+ if (hasSignLanguage) {
3558
+ if (!this.controls.signLanguage && this.player.options.signLanguageButton !== false) {
3559
+ const btn = this.createSignLanguageButton();
3560
+ btn.dataset.overflowPriority = "3";
3561
+ btn.dataset.overflowPriorityMobile = "3";
3562
+ const qualityBtn = this.rightButtons.querySelector(`.${this.player.options.classPrefix}-quality`);
3563
+ const fullscreenBtn = this.rightButtons.querySelector(`.${this.player.options.classPrefix}-fullscreen`);
3564
+ const insertBefore = qualityBtn || fullscreenBtn || null;
3565
+ if (insertBefore) {
3566
+ this.rightButtons.insertBefore(btn, insertBefore);
3567
+ } else {
3568
+ this.rightButtons.appendChild(btn);
3569
+ }
3570
+ this.setupOverflowMenu();
3571
+ }
3572
+ if (this.controls.signLanguage) {
3573
+ this.controls.signLanguage.style.display = "";
3574
+ }
3575
+ } else {
3576
+ if (this.controls.signLanguage) {
3577
+ this.controls.signLanguage.style.display = "none";
3578
+ }
3579
+ }
3580
+ }
3436
3581
  createSettingsButton() {
3437
3582
  const button = DOMUtils.createElement("button", {
3438
3583
  className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-settings`,
@@ -3509,7 +3654,7 @@ var VidPly = (() => {
3509
3654
  icon.innerHTML = isPlaying ? createIconElement("pause").innerHTML : createIconElement("play").innerHTML;
3510
3655
  const newAriaLabel = isPlaying ? i18n.t("player.pause") : i18n.t("player.play");
3511
3656
  this.controls.playPause.setAttribute("aria-label", newAriaLabel);
3512
- this.controls.playPause.setAttribute("title", newAriaLabel);
3657
+ DOMUtils.attachTooltip(this.controls.playPause, newAriaLabel, this.player.options.classPrefix);
3513
3658
  }
3514
3659
  updateProgress() {
3515
3660
  if (!this.controls.played) return;
@@ -3565,7 +3710,7 @@ var VidPly = (() => {
3565
3710
  icon.innerHTML = createIconElement(iconName).innerHTML;
3566
3711
  const newMuteAriaLabel = this.player.state.muted ? i18n.t("player.unmute") : i18n.t("player.mute");
3567
3712
  this.controls.mute.setAttribute("aria-label", newMuteAriaLabel);
3568
- this.controls.mute.setAttribute("title", newMuteAriaLabel);
3713
+ DOMUtils.attachTooltip(this.controls.mute, newMuteAriaLabel, this.player.options.classPrefix);
3569
3714
  }
3570
3715
  }
3571
3716
  if (this.controls.volumeSlider) {
@@ -3677,16 +3822,17 @@ var VidPly = (() => {
3677
3822
  showControls();
3678
3823
  }
3679
3824
  createOverflowMenuButton() {
3825
+ const ariaLabel = i18n.t("player.moreOptions");
3680
3826
  const button = DOMUtils.createElement("button", {
3681
3827
  className: `${this.player.options.classPrefix}-button ${this.player.options.classPrefix}-overflow-menu`,
3682
3828
  attributes: {
3683
3829
  "type": "button",
3684
- "aria-label": i18n.t("player.moreOptions"),
3685
- "aria-expanded": "false",
3686
- "title": i18n.t("player.moreOptions")
3830
+ "aria-label": ariaLabel,
3831
+ "aria-expanded": "false"
3687
3832
  }
3688
3833
  });
3689
3834
  button.appendChild(createIconElement("moreVertical"));
3835
+ DOMUtils.attachTooltip(button, ariaLabel, this.player.options.classPrefix);
3690
3836
  button.addEventListener("click", () => {
3691
3837
  this.showOverflowMenu(button);
3692
3838
  });
@@ -3768,7 +3914,7 @@ var VidPly = (() => {
3768
3914
  setTimeout(() => {
3769
3915
  const firstItem = menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
3770
3916
  if (firstItem && firstItem.tagName === "BUTTON") {
3771
- firstItem.focus();
3917
+ firstItem.focus({ preventScroll: true });
3772
3918
  }
3773
3919
  }, 0);
3774
3920
  }
@@ -4699,7 +4845,8 @@ var VidPly = (() => {
4699
4845
  menuItems.forEach((item, idx) => {
4700
4846
  item.setAttribute("tabindex", idx === nextIndex ? "0" : "-1");
4701
4847
  });
4702
- menuItems[nextIndex].focus();
4848
+ menuItems[nextIndex].focus({ preventScroll: false });
4849
+ menuItems[nextIndex].scrollIntoView({ behavior: "smooth", block: "nearest" });
4703
4850
  break;
4704
4851
  case "ArrowUp":
4705
4852
  e.preventDefault();
@@ -4708,7 +4855,8 @@ var VidPly = (() => {
4708
4855
  menuItems.forEach((item, idx) => {
4709
4856
  item.setAttribute("tabindex", idx === prevIndex ? "0" : "-1");
4710
4857
  });
4711
- menuItems[prevIndex].focus();
4858
+ menuItems[prevIndex].focus({ preventScroll: false });
4859
+ menuItems[prevIndex].scrollIntoView({ behavior: "smooth", block: "nearest" });
4712
4860
  break;
4713
4861
  case "Home":
4714
4862
  e.preventDefault();
@@ -4716,7 +4864,8 @@ var VidPly = (() => {
4716
4864
  menuItems.forEach((item, idx) => {
4717
4865
  item.setAttribute("tabindex", idx === 0 ? "0" : "-1");
4718
4866
  });
4719
- menuItems[0].focus();
4867
+ menuItems[0].focus({ preventScroll: false });
4868
+ menuItems[0].scrollIntoView({ behavior: "smooth", block: "nearest" });
4720
4869
  break;
4721
4870
  case "End":
4722
4871
  e.preventDefault();
@@ -4725,7 +4874,8 @@ var VidPly = (() => {
4725
4874
  menuItems.forEach((item, idx) => {
4726
4875
  item.setAttribute("tabindex", idx === lastIndex ? "0" : "-1");
4727
4876
  });
4728
- menuItems[lastIndex].focus();
4877
+ menuItems[lastIndex].focus({ preventScroll: false });
4878
+ menuItems[lastIndex].scrollIntoView({ behavior: "smooth", block: "nearest" });
4729
4879
  break;
4730
4880
  case "Enter":
4731
4881
  case " ":
@@ -4763,6 +4913,7 @@ var VidPly = (() => {
4763
4913
  item.setAttribute("tabindex", index === 0 ? "0" : "-1");
4764
4914
  });
4765
4915
  focusElement(menuItems[0], { delay: 0 });
4916
+ menuItems[0].scrollIntoView({ behavior: "smooth", block: "nearest" });
4766
4917
  }
4767
4918
  }, delay);
4768
4919
  }
@@ -5555,7 +5706,7 @@ var VidPly = (() => {
5555
5706
  if (focusButton) {
5556
5707
  const transcriptButton = (_b = (_a = this.player.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.transcript;
5557
5708
  if (transcriptButton && typeof transcriptButton.focus === "function") {
5558
- transcriptButton.focus();
5709
+ transcriptButton.focus({ preventScroll: true });
5559
5710
  }
5560
5711
  }
5561
5712
  }
@@ -5580,15 +5731,17 @@ var VidPly = (() => {
5580
5731
  this.headerLeft = DOMUtils.createElement("div", {
5581
5732
  className: `${this.player.options.classPrefix}-transcript-header-left`
5582
5733
  });
5734
+ const settingsAriaLabel = i18n.t("transcript.settingsMenu");
5583
5735
  this.settingsButton = DOMUtils.createElement("button", {
5584
5736
  className: `${this.player.options.classPrefix}-transcript-settings`,
5585
5737
  attributes: {
5586
5738
  "type": "button",
5587
- "aria-label": i18n.t("transcript.settingsMenu"),
5739
+ "aria-label": settingsAriaLabel,
5588
5740
  "aria-expanded": "false"
5589
5741
  }
5590
5742
  });
5591
5743
  this.settingsButton.appendChild(createIconElement("settings"));
5744
+ DOMUtils.attachTooltip(this.settingsButton, settingsAriaLabel, this.player.options.classPrefix);
5592
5745
  this.handlers.settingsClick = (e) => {
5593
5746
  e.preventDefault();
5594
5747
  e.stopPropagation();
@@ -5622,8 +5775,7 @@ var VidPly = (() => {
5622
5775
  const autoscrollLabel = DOMUtils.createElement("label", {
5623
5776
  className: `${this.player.options.classPrefix}-transcript-autoscroll-label`,
5624
5777
  attributes: {
5625
- "for": autoscrollId,
5626
- "title": i18n.t("transcript.autoscroll")
5778
+ "for": autoscrollId
5627
5779
  }
5628
5780
  });
5629
5781
  this.autoscrollCheckbox = DOMUtils.createElement("input", {
@@ -5672,14 +5824,16 @@ var VidPly = (() => {
5672
5824
  this.languageSelectorWrapper = languageSelectorWrapper;
5673
5825
  preventDragOnElement(languageSelectorWrapper);
5674
5826
  this.headerLeft.appendChild(languageSelectorWrapper);
5827
+ const closeAriaLabel = i18n.t("transcript.close");
5675
5828
  const closeButton = DOMUtils.createElement("button", {
5676
5829
  className: `${this.player.options.classPrefix}-transcript-close`,
5677
5830
  attributes: {
5678
5831
  "type": "button",
5679
- "aria-label": i18n.t("transcript.close")
5832
+ "aria-label": closeAriaLabel
5680
5833
  }
5681
5834
  });
5682
5835
  closeButton.appendChild(createIconElement("close"));
5836
+ DOMUtils.attachTooltip(closeButton, closeAriaLabel, this.player.options.classPrefix);
5683
5837
  closeButton.addEventListener("click", () => this.hideTranscript({ focusButton: true }));
5684
5838
  this.transcriptHeader.appendChild(this.headerLeft);
5685
5839
  this.transcriptHeader.appendChild(closeButton);
@@ -6126,7 +6280,7 @@ var VidPly = (() => {
6126
6280
  console.log("[VidPly Metadata] Focusing element:", targetSelector);
6127
6281
  }
6128
6282
  this.setManagedTimeout(() => {
6129
- targetElement.focus();
6283
+ targetElement.focus({ preventScroll: true });
6130
6284
  }, 10);
6131
6285
  } else if (this.player.options.debug) {
6132
6286
  console.warn("[VidPly Metadata] Element not found:", targetSelector);
@@ -6341,7 +6495,7 @@ var VidPly = (() => {
6341
6495
  e.stopPropagation();
6342
6496
  const enabled = this.toggleResizeMode();
6343
6497
  if (enabled) {
6344
- this.transcriptWindow.focus();
6498
+ this.transcriptWindow.focus({ preventScroll: true });
6345
6499
  }
6346
6500
  return;
6347
6501
  }
@@ -6382,7 +6536,7 @@ var VidPly = (() => {
6382
6536
  if (this.settingsMenuVisible) {
6383
6537
  this.hideSettingsMenu();
6384
6538
  }
6385
- this.transcriptWindow.focus();
6539
+ this.transcriptWindow.focus({ preventScroll: true });
6386
6540
  }
6387
6541
  }
6388
6542
  /**
@@ -6425,7 +6579,7 @@ var VidPly = (() => {
6425
6579
  for (let i = 1; i < menuItems.length; i++) {
6426
6580
  menuItems[i].setAttribute("tabindex", "-1");
6427
6581
  }
6428
- menuItems[0].focus();
6582
+ menuItems[0].focus({ preventScroll: true });
6429
6583
  }
6430
6584
  }, 50);
6431
6585
  return;
@@ -6449,6 +6603,10 @@ var VidPly = (() => {
6449
6603
  });
6450
6604
  keyboardDragOption.setAttribute("role", "switch");
6451
6605
  keyboardDragOption.setAttribute("aria-checked", "false");
6606
+ const dragTooltip = keyboardDragOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
6607
+ if (dragTooltip) dragTooltip.remove();
6608
+ const dragButtonText = keyboardDragOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
6609
+ if (dragButtonText) dragButtonText.remove();
6452
6610
  this.dragOptionButton = keyboardDragOption;
6453
6611
  this.dragOptionText = keyboardDragOption.querySelector(`.${this.player.options.classPrefix}-settings-text`);
6454
6612
  this.updateDragOptionState();
@@ -6466,6 +6624,10 @@ var VidPly = (() => {
6466
6624
  }, 50);
6467
6625
  }
6468
6626
  });
6627
+ const styleTooltip = styleOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
6628
+ if (styleTooltip) styleTooltip.remove();
6629
+ const styleButtonText = styleOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
6630
+ if (styleButtonText) styleButtonText.remove();
6469
6631
  const resizeOption = createMenuItem({
6470
6632
  classPrefix: this.player.options.classPrefix,
6471
6633
  itemClass: `${this.player.options.classPrefix}-transcript-settings-item`,
@@ -6480,7 +6642,7 @@ var VidPly = (() => {
6480
6642
  this.hideSettingsMenu({ focusButton: false });
6481
6643
  setTimeout(() => {
6482
6644
  if (this.transcriptWindow) {
6483
- this.transcriptWindow.focus();
6645
+ this.transcriptWindow.focus({ preventScroll: true });
6484
6646
  }
6485
6647
  }, 20);
6486
6648
  } else {
@@ -6490,6 +6652,10 @@ var VidPly = (() => {
6490
6652
  });
6491
6653
  resizeOption.setAttribute("role", "switch");
6492
6654
  resizeOption.setAttribute("aria-checked", "false");
6655
+ const resizeTooltip = resizeOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
6656
+ if (resizeTooltip) resizeTooltip.remove();
6657
+ const resizeButtonText = resizeOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
6658
+ if (resizeButtonText) resizeButtonText.remove();
6493
6659
  this.resizeOptionButton = resizeOption;
6494
6660
  this.resizeOptionText = resizeOption.querySelector(`.${this.player.options.classPrefix}-settings-text`);
6495
6661
  this.updateResizeOptionState();
@@ -6505,6 +6671,10 @@ var VidPly = (() => {
6505
6671
  });
6506
6672
  showTimestampsOption.setAttribute("role", "switch");
6507
6673
  showTimestampsOption.setAttribute("aria-checked", this.showTimestamps ? "true" : "false");
6674
+ const timestampsTooltip = showTimestampsOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
6675
+ if (timestampsTooltip) timestampsTooltip.remove();
6676
+ const timestampsButtonText = showTimestampsOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
6677
+ if (timestampsButtonText) timestampsButtonText.remove();
6508
6678
  this.showTimestampsButton = showTimestampsOption;
6509
6679
  this.showTimestampsText = showTimestampsOption.querySelector(`.${this.player.options.classPrefix}-settings-text`);
6510
6680
  this.updateShowTimestampsState();
@@ -6517,6 +6687,10 @@ var VidPly = (() => {
6517
6687
  this.hideSettingsMenu();
6518
6688
  }
6519
6689
  });
6690
+ const closeTooltip = closeOption.querySelector(`.${this.player.options.classPrefix}-tooltip`);
6691
+ if (closeTooltip) closeTooltip.remove();
6692
+ const closeButtonText = closeOption.querySelector(`.${this.player.options.classPrefix}-button-text`);
6693
+ if (closeButtonText) closeButtonText.remove();
6520
6694
  this.settingsMenu.appendChild(keyboardDragOption);
6521
6695
  this.settingsMenu.appendChild(resizeOption);
6522
6696
  this.settingsMenu.appendChild(styleOption);
@@ -6558,7 +6732,7 @@ var VidPly = (() => {
6558
6732
  for (let i = 1; i < menuItems.length; i++) {
6559
6733
  menuItems[i].setAttribute("tabindex", "-1");
6560
6734
  }
6561
- menuItems[0].focus();
6735
+ menuItems[0].focus({ preventScroll: true });
6562
6736
  }
6563
6737
  }, 50);
6564
6738
  }
@@ -6632,7 +6806,7 @@ var VidPly = (() => {
6632
6806
  if (this.settingsButton) {
6633
6807
  this.settingsButton.setAttribute("aria-expanded", "false");
6634
6808
  if (focusButton) {
6635
- this.settingsButton.focus();
6809
+ this.settingsButton.focus({ preventScroll: true });
6636
6810
  }
6637
6811
  }
6638
6812
  }
@@ -6678,7 +6852,6 @@ var VidPly = (() => {
6678
6852
  const ariaLabel = isEnabled ? i18n.t("transcript.disableDragModeAria") : i18n.t("transcript.enableDragModeAria");
6679
6853
  this.dragOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
6680
6854
  this.dragOptionButton.setAttribute("aria-label", ariaLabel);
6681
- this.dragOptionButton.setAttribute("title", text);
6682
6855
  if (this.dragOptionText) {
6683
6856
  this.dragOptionText.textContent = text;
6684
6857
  }
@@ -6692,7 +6865,6 @@ var VidPly = (() => {
6692
6865
  const ariaLabel = isEnabled ? i18n.t("transcript.disableResizeModeAria") : i18n.t("transcript.enableResizeModeAria");
6693
6866
  this.resizeOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
6694
6867
  this.resizeOptionButton.setAttribute("aria-label", ariaLabel);
6695
- this.resizeOptionButton.setAttribute("title", text);
6696
6868
  if (this.resizeOptionText) {
6697
6869
  this.resizeOptionText.textContent = text;
6698
6870
  }
@@ -6711,7 +6883,6 @@ var VidPly = (() => {
6711
6883
  const ariaLabel = this.showTimestamps ? i18n.t("transcript.hideTimestampsAria") : i18n.t("transcript.showTimestampsAria");
6712
6884
  this.showTimestampsButton.setAttribute("aria-checked", this.showTimestamps ? "true" : "false");
6713
6885
  this.showTimestampsButton.setAttribute("aria-label", ariaLabel);
6714
- this.showTimestampsButton.setAttribute("title", text);
6715
6886
  if (this.showTimestampsText) {
6716
6887
  this.showTimestampsText.textContent = text;
6717
6888
  }
@@ -6780,7 +6951,7 @@ var VidPly = (() => {
6780
6951
  setTimeout(() => {
6781
6952
  const firstSelect = this.styleDialog.querySelector("select, input");
6782
6953
  if (firstSelect) {
6783
- firstSelect.focus();
6954
+ firstSelect.focus({ preventScroll: true });
6784
6955
  }
6785
6956
  }, 0);
6786
6957
  return;
@@ -6845,10 +7016,10 @@ var VidPly = (() => {
6845
7016
  const lastElement = focusableElements[focusableElements.length - 1];
6846
7017
  if (e.shiftKey && document.activeElement === firstElement) {
6847
7018
  e.preventDefault();
6848
- lastElement.focus();
7019
+ lastElement.focus({ preventScroll: true });
6849
7020
  } else if (!e.shiftKey && document.activeElement === lastElement) {
6850
7021
  e.preventDefault();
6851
- firstElement.focus();
7022
+ firstElement.focus({ preventScroll: true });
6852
7023
  }
6853
7024
  }
6854
7025
  };
@@ -6868,7 +7039,7 @@ var VidPly = (() => {
6868
7039
  setTimeout(() => {
6869
7040
  const firstSelect = this.styleDialog.querySelector("select, input");
6870
7041
  if (firstSelect) {
6871
- firstSelect.focus();
7042
+ firstSelect.focus({ preventScroll: true });
6872
7043
  }
6873
7044
  }, 0);
6874
7045
  }
@@ -6883,7 +7054,7 @@ var VidPly = (() => {
6883
7054
  document.removeEventListener("keydown", this.handlers.styleDialogKeydown);
6884
7055
  }
6885
7056
  if (this.settingsButton) {
6886
- this.settingsButton.focus();
7057
+ this.settingsButton.focus({ preventScroll: true });
6887
7058
  }
6888
7059
  }
6889
7060
  }
@@ -8447,7 +8618,12 @@ var VidPly = (() => {
8447
8618
  if (trackConfig.default) {
8448
8619
  track.default = true;
8449
8620
  }
8450
- this.element.appendChild(track);
8621
+ const firstChild = this.element.firstChild;
8622
+ if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName !== "TRACK") {
8623
+ this.element.insertBefore(track, firstChild);
8624
+ } else {
8625
+ this.element.appendChild(track);
8626
+ }
8451
8627
  });
8452
8628
  this.invalidateTrackCache();
8453
8629
  }
@@ -9048,6 +9224,11 @@ var VidPly = (() => {
9048
9224
  });
9049
9225
  }
9050
9226
  });
9227
+ const hasSrcAttribute = this.element.hasAttribute("src");
9228
+ const srcValue = hasSrcAttribute ? this.element.getAttribute("src") : null;
9229
+ if (hasSrcAttribute) {
9230
+ this.element.removeAttribute("src");
9231
+ }
9051
9232
  allSourceElements.forEach((sourceEl) => {
9052
9233
  sourceEl.remove();
9053
9234
  });
@@ -9204,7 +9385,10 @@ var VidPly = (() => {
9204
9385
  newTrackElement.setAttribute(attrName, attributes[attrName]);
9205
9386
  }
9206
9387
  });
9207
- if (nextSibling && nextSibling.parentNode) {
9388
+ const firstChild = parent.firstChild;
9389
+ if (firstChild && firstChild.nodeType === Node.ELEMENT_NODE && firstChild.tagName !== "TRACK") {
9390
+ parent.insertBefore(newTrackElement, firstChild);
9391
+ } else if (nextSibling && nextSibling.parentNode) {
9208
9392
  parent.insertBefore(newTrackElement, nextSibling);
9209
9393
  } else {
9210
9394
  parent.appendChild(newTrackElement);
@@ -9676,6 +9860,11 @@ var VidPly = (() => {
9676
9860
  });
9677
9861
  }
9678
9862
  });
9863
+ const hasSrcAttribute = this.element.hasAttribute("src");
9864
+ const srcValue = hasSrcAttribute ? this.element.getAttribute("src") : null;
9865
+ if (hasSrcAttribute) {
9866
+ this.element.removeAttribute("src");
9867
+ }
9679
9868
  allSourceElements.forEach((sourceEl) => {
9680
9869
  sourceEl.remove();
9681
9870
  });
@@ -9966,15 +10155,17 @@ var VidPly = (() => {
9966
10155
  const title = DOMUtils.createElement("h3", {
9967
10156
  textContent: i18n.t("player.signLanguageVideo")
9968
10157
  });
10158
+ const settingsAriaLabel = i18n.t("player.signLanguageSettings");
9969
10159
  this.signLanguageSettingsButton = DOMUtils.createElement("button", {
9970
10160
  className: `${this.options.classPrefix}-sign-language-settings`,
9971
10161
  attributes: {
9972
10162
  "type": "button",
9973
- "aria-label": i18n.t("player.signLanguageSettings"),
10163
+ "aria-label": settingsAriaLabel,
9974
10164
  "aria-expanded": "false"
9975
10165
  }
9976
10166
  });
9977
10167
  this.signLanguageSettingsButton.appendChild(createIconElement("settings"));
10168
+ DOMUtils.attachTooltip(this.signLanguageSettingsButton, settingsAriaLabel, this.options.classPrefix);
9978
10169
  this.signLanguageSettingsHandlers = {
9979
10170
  settingsClick: (e) => {
9980
10171
  e.preventDefault();
@@ -10042,19 +10233,21 @@ var VidPly = (() => {
10042
10233
  headerLeft.appendChild(signLanguageSelectorWrapper);
10043
10234
  }
10044
10235
  headerLeft.appendChild(title);
10236
+ const closeAriaLabel = i18n.t("player.closeSignLanguage");
10045
10237
  const closeButton = DOMUtils.createElement("button", {
10046
10238
  className: `${this.options.classPrefix}-sign-language-close`,
10047
10239
  attributes: {
10048
10240
  "type": "button",
10049
- "aria-label": i18n.t("player.closeSignLanguage")
10241
+ "aria-label": closeAriaLabel
10050
10242
  }
10051
10243
  });
10052
10244
  closeButton.appendChild(createIconElement("close"));
10245
+ DOMUtils.attachTooltip(closeButton, closeAriaLabel, this.options.classPrefix);
10053
10246
  closeButton.addEventListener("click", () => {
10054
10247
  this.disableSignLanguage();
10055
10248
  if (this.controlBar && this.controlBar.controls && this.controlBar.controls.signLanguage) {
10056
10249
  setTimeout(() => {
10057
- this.controlBar.controls.signLanguage.focus();
10250
+ this.controlBar.controls.signLanguage.focus({ preventScroll: true });
10058
10251
  }, 0);
10059
10252
  }
10060
10253
  });
@@ -10232,7 +10425,7 @@ var VidPly = (() => {
10232
10425
  e.stopPropagation();
10233
10426
  const enabled = this.toggleSignLanguageResizeMode();
10234
10427
  if (enabled) {
10235
- this.signLanguageWrapper.focus();
10428
+ this.signLanguageWrapper.focus({ preventScroll: true });
10236
10429
  }
10237
10430
  return;
10238
10431
  }
@@ -10250,7 +10443,7 @@ var VidPly = (() => {
10250
10443
  this.disableSignLanguage();
10251
10444
  if (this.controlBar && this.controlBar.controls && this.controlBar.controls.signLanguage) {
10252
10445
  setTimeout(() => {
10253
- this.controlBar.controls.signLanguage.focus();
10446
+ this.controlBar.controls.signLanguage.focus({ preventScroll: true });
10254
10447
  }, 0);
10255
10448
  }
10256
10449
  return;
@@ -10385,6 +10578,10 @@ var VidPly = (() => {
10385
10578
  });
10386
10579
  keyboardDragOption.setAttribute("role", "switch");
10387
10580
  keyboardDragOption.setAttribute("aria-checked", "false");
10581
+ const dragTooltip = keyboardDragOption.querySelector(`.${this.options.classPrefix}-tooltip`);
10582
+ if (dragTooltip) dragTooltip.remove();
10583
+ const dragButtonText = keyboardDragOption.querySelector(`.${this.options.classPrefix}-button-text`);
10584
+ if (dragButtonText) dragButtonText.remove();
10388
10585
  this.signLanguageDragOptionButton = keyboardDragOption;
10389
10586
  this.signLanguageDragOptionText = keyboardDragOption.querySelector(`.${this.options.classPrefix}-settings-text`);
10390
10587
  this.updateSignLanguageDragOptionState();
@@ -10402,7 +10599,7 @@ var VidPly = (() => {
10402
10599
  this.hideSignLanguageSettingsMenu({ focusButton: false });
10403
10600
  setTimeout(() => {
10404
10601
  if (this.signLanguageWrapper) {
10405
- this.signLanguageWrapper.focus();
10602
+ this.signLanguageWrapper.focus({ preventScroll: true });
10406
10603
  }
10407
10604
  }, 20);
10408
10605
  } else {
@@ -10412,6 +10609,10 @@ var VidPly = (() => {
10412
10609
  });
10413
10610
  resizeOption.setAttribute("role", "switch");
10414
10611
  resizeOption.setAttribute("aria-checked", "false");
10612
+ const resizeTooltip = resizeOption.querySelector(`.${this.options.classPrefix}-tooltip`);
10613
+ if (resizeTooltip) resizeTooltip.remove();
10614
+ const resizeButtonText = resizeOption.querySelector(`.${this.options.classPrefix}-button-text`);
10615
+ if (resizeButtonText) resizeButtonText.remove();
10415
10616
  this.signLanguageResizeOptionButton = resizeOption;
10416
10617
  this.signLanguageResizeOptionText = resizeOption.querySelector(`.${this.options.classPrefix}-settings-text`);
10417
10618
  this.updateSignLanguageResizeOptionState();
@@ -10424,6 +10625,10 @@ var VidPly = (() => {
10424
10625
  this.hideSignLanguageSettingsMenu();
10425
10626
  }
10426
10627
  });
10628
+ const closeTooltip = closeOption.querySelector(`.${this.options.classPrefix}-tooltip`);
10629
+ if (closeTooltip) closeTooltip.remove();
10630
+ const closeButtonText = closeOption.querySelector(`.${this.options.classPrefix}-button-text`);
10631
+ if (closeButtonText) closeButtonText.remove();
10427
10632
  this.signLanguageSettingsMenu.appendChild(keyboardDragOption);
10428
10633
  this.signLanguageSettingsMenu.appendChild(resizeOption);
10429
10634
  this.signLanguageSettingsMenu.appendChild(closeOption);
@@ -10470,7 +10675,7 @@ var VidPly = (() => {
10470
10675
  if (this.signLanguageSettingsButton) {
10471
10676
  this.signLanguageSettingsButton.setAttribute("aria-expanded", "false");
10472
10677
  if (focusButton) {
10473
- this.signLanguageSettingsButton.focus();
10678
+ this.signLanguageSettingsButton.focus({ preventScroll: true });
10474
10679
  }
10475
10680
  }
10476
10681
  }
@@ -10559,7 +10764,6 @@ var VidPly = (() => {
10559
10764
  const ariaLabel = isEnabled ? i18n.t("player.disableSignDragModeAria") : i18n.t("player.enableSignDragModeAria");
10560
10765
  this.signLanguageDragOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
10561
10766
  this.signLanguageDragOptionButton.setAttribute("aria-label", ariaLabel);
10562
- this.signLanguageDragOptionButton.setAttribute("title", text);
10563
10767
  if (this.signLanguageDragOptionText) {
10564
10768
  this.signLanguageDragOptionText.textContent = text;
10565
10769
  }
@@ -10573,7 +10777,6 @@ var VidPly = (() => {
10573
10777
  const ariaLabel = isEnabled ? i18n.t("player.disableSignResizeModeAria") : i18n.t("player.enableSignResizeModeAria");
10574
10778
  this.signLanguageResizeOptionButton.setAttribute("aria-checked", isEnabled ? "true" : "false");
10575
10779
  this.signLanguageResizeOptionButton.setAttribute("aria-label", ariaLabel);
10576
- this.signLanguageResizeOptionButton.setAttribute("title", text);
10577
10780
  if (this.signLanguageResizeOptionText) {
10578
10781
  this.signLanguageResizeOptionText.textContent = text;
10579
10782
  }
@@ -11007,23 +11210,23 @@ var VidPly = (() => {
11007
11210
  return;
11008
11211
  }
11009
11212
  if (target === "alert" && fallbackElement) {
11010
- fallbackElement.focus();
11213
+ fallbackElement.focus({ preventScroll: true });
11011
11214
  return;
11012
11215
  }
11013
11216
  if (target === "player") {
11014
11217
  if (this.container) {
11015
- this.container.focus();
11218
+ this.container.focus({ preventScroll: true });
11016
11219
  }
11017
11220
  return;
11018
11221
  }
11019
11222
  if (target === "media") {
11020
- this.element.focus();
11223
+ this.element.focus({ preventScroll: true });
11021
11224
  return;
11022
11225
  }
11023
11226
  if (target === "playButton") {
11024
11227
  const playButton = (_b = (_a = this.controlBar) == null ? void 0 : _a.controls) == null ? void 0 : _b.playPause;
11025
11228
  if (playButton) {
11026
- playButton.focus();
11229
+ playButton.focus({ preventScroll: true });
11027
11230
  }
11028
11231
  return;
11029
11232
  }
@@ -11033,7 +11236,7 @@ var VidPly = (() => {
11033
11236
  if (targetElement.tabIndex === -1 && !targetElement.hasAttribute("tabindex")) {
11034
11237
  targetElement.setAttribute("tabindex", "-1");
11035
11238
  }
11036
- targetElement.focus();
11239
+ targetElement.focus({ preventScroll: true });
11037
11240
  }
11038
11241
  }
11039
11242
  }
@@ -11077,7 +11280,7 @@ var VidPly = (() => {
11077
11280
  if (element.tabIndex === -1 && !element.hasAttribute("tabindex")) {
11078
11281
  element.setAttribute("tabindex", "-1");
11079
11282
  }
11080
- element.focus();
11283
+ element.focus({ preventScroll: true });
11081
11284
  }
11082
11285
  if (shouldShow && config.autoScroll !== false && options.autoScroll !== false) {
11083
11286
  element.scrollIntoView({ behavior: "smooth", block: "nearest" });
@@ -11208,8 +11411,7 @@ var VidPly = (() => {
11208
11411
  targetElement.setAttribute("tabindex", "-1");
11209
11412
  }
11210
11413
  this.setManagedTimeout(() => {
11211
- targetElement.focus();
11212
- targetElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
11414
+ targetElement.focus({ preventScroll: true });
11213
11415
  }, 10);
11214
11416
  } else if (this.options.debug) {
11215
11417
  this.log("[Metadata] Element not found:", normalizedSelector || targetSelector);
@@ -11285,6 +11487,8 @@ var VidPly = (() => {
11285
11487
  this.player.on("pause", this.handlePlaybackStateChange.bind(this));
11286
11488
  this.player.on("ended", this.handlePlaybackStateChange.bind(this));
11287
11489
  this.player.on("fullscreenchange", this.handleFullscreenChange.bind(this));
11490
+ this.player.on("audiodescriptionenabled", this.handleAudioDescriptionChange.bind(this));
11491
+ this.player.on("audiodescriptiondisabled", this.handleAudioDescriptionChange.bind(this));
11288
11492
  if (this.options.showPanel) {
11289
11493
  this.createUI();
11290
11494
  }
@@ -11512,6 +11716,52 @@ var VidPly = (() => {
11512
11716
  this.updatePlaylistVisibilityInFullscreen();
11513
11717
  }, 50);
11514
11718
  }
11719
+ /**
11720
+ * Handle audio description state changes
11721
+ * Updates duration displays to show audio-described version duration when AD is enabled
11722
+ */
11723
+ handleAudioDescriptionChange() {
11724
+ const currentTrack = this.getCurrentTrack();
11725
+ if (!currentTrack) return;
11726
+ this.updateTrackInfo(currentTrack);
11727
+ this.updatePlaylistUI();
11728
+ this.updatePlaylistDurations();
11729
+ }
11730
+ /**
11731
+ * Update the visual duration displays in the playlist panel
11732
+ * Called when audio description state changes
11733
+ */
11734
+ updatePlaylistDurations() {
11735
+ if (!this.playlistPanel) return;
11736
+ const items = this.playlistPanel.querySelectorAll(".vidply-playlist-item");
11737
+ items.forEach((item, index) => {
11738
+ const track = this.tracks[index];
11739
+ if (!track) return;
11740
+ const effectiveDuration = this.getEffectiveDuration(track);
11741
+ const trackDuration = effectiveDuration ? TimeUtils.formatTime(effectiveDuration) : "";
11742
+ const durationBadge = item.querySelector(".vidply-playlist-duration-badge");
11743
+ if (durationBadge) {
11744
+ durationBadge.textContent = trackDuration;
11745
+ }
11746
+ const inlineDuration = item.querySelector(".vidply-playlist-item-duration");
11747
+ if (inlineDuration) {
11748
+ inlineDuration.textContent = trackDuration;
11749
+ }
11750
+ });
11751
+ }
11752
+ /**
11753
+ * Get the effective duration for a track based on audio description state
11754
+ * @param {Object} track - Track object
11755
+ * @returns {number|null} - Duration in seconds or null if not available
11756
+ */
11757
+ getEffectiveDuration(track) {
11758
+ if (!track) return null;
11759
+ const isAudioDescriptionEnabled = this.player.state.audioDescriptionEnabled;
11760
+ if (isAudioDescriptionEnabled && track.audioDescriptionDuration) {
11761
+ return track.audioDescriptionDuration;
11762
+ }
11763
+ return track.duration || null;
11764
+ }
11515
11765
  /**
11516
11766
  * Update playlist visibility based on fullscreen and playback state
11517
11767
  * In fullscreen: show when paused/not started, hide when playing
@@ -11569,9 +11819,7 @@ var VidPly = (() => {
11569
11819
  this.trackInfoElement = DOMUtils.createElement("div", {
11570
11820
  className: "vidply-track-info",
11571
11821
  attributes: {
11572
- role: "status",
11573
- "aria-live": "polite",
11574
- "aria-atomic": "true"
11822
+ role: "status"
11575
11823
  }
11576
11824
  });
11577
11825
  this.trackInfoElement.style.display = "none";
@@ -11606,22 +11854,32 @@ var VidPly = (() => {
11606
11854
  const totalTracks = this.tracks.length;
11607
11855
  const trackTitle = track.title || i18n.t("playlist.untitled");
11608
11856
  const trackArtist = track.artist || "";
11857
+ const effectiveDuration = this.getEffectiveDuration(track);
11858
+ const trackDuration = effectiveDuration ? TimeUtils.formatTime(effectiveDuration) : "";
11859
+ const trackDurationReadable = effectiveDuration ? TimeUtils.formatDuration(effectiveDuration) : "";
11609
11860
  const artistPart = trackArtist ? i18n.t("playlist.by") + trackArtist : "";
11861
+ const durationPart = trackDurationReadable ? `. ${trackDurationReadable}` : "";
11610
11862
  const announcement = i18n.t("playlist.nowPlaying", {
11611
11863
  current: trackNumber,
11612
11864
  total: totalTracks,
11613
11865
  title: trackTitle,
11614
11866
  artist: artistPart
11615
- });
11867
+ }) + durationPart;
11616
11868
  const trackOfText = i18n.t("playlist.trackOf", {
11617
11869
  current: trackNumber,
11618
11870
  total: totalTracks
11619
11871
  });
11872
+ const durationHtml = trackDuration ? `<span class="vidply-track-duration" aria-hidden="true">${DOMUtils.escapeHTML(trackDuration)}</span>` : "";
11873
+ const trackDescription = track.description || "";
11620
11874
  this.trackInfoElement.innerHTML = `
11621
11875
  <span class="vidply-sr-only">${DOMUtils.escapeHTML(announcement)}</span>
11622
- <div class="vidply-track-number" aria-hidden="true">${DOMUtils.escapeHTML(trackOfText)}</div>
11876
+ <div class="vidply-track-header" aria-hidden="true">
11877
+ <span class="vidply-track-number">${DOMUtils.escapeHTML(trackOfText)}</span>
11878
+ ${durationHtml}
11879
+ </div>
11623
11880
  <div class="vidply-track-title" aria-hidden="true">${DOMUtils.escapeHTML(trackTitle)}</div>
11624
11881
  ${trackArtist ? `<div class="vidply-track-artist" aria-hidden="true">${DOMUtils.escapeHTML(trackArtist)}</div>` : ""}
11882
+ ${trackDescription ? `<div class="vidply-track-description" aria-hidden="true">${DOMUtils.escapeHTML(trackDescription)}</div>` : ""}
11625
11883
  `;
11626
11884
  this.trackInfoElement.style.display = "block";
11627
11885
  this.updateTrackArtwork(track);
@@ -11663,6 +11921,7 @@ var VidPly = (() => {
11663
11921
  const list = DOMUtils.createElement("ul", {
11664
11922
  className: "vidply-playlist-list",
11665
11923
  attributes: {
11924
+ role: "listbox",
11666
11925
  "aria-labelledby": `${this.uniqueId}-heading`,
11667
11926
  "aria-describedby": `${this.uniqueId}-keyboard-instructions`
11668
11927
  }
@@ -11686,9 +11945,14 @@ var VidPly = (() => {
11686
11945
  });
11687
11946
  const trackTitle = track.title || i18n.t("playlist.trackUntitled", { number: index + 1 });
11688
11947
  const trackArtist = track.artist ? i18n.t("playlist.by") + track.artist : "";
11948
+ const effectiveDuration = this.getEffectiveDuration(track);
11949
+ const trackDuration = effectiveDuration ? TimeUtils.formatTime(effectiveDuration) : "";
11950
+ const trackDurationReadable = effectiveDuration ? TimeUtils.formatDuration(effectiveDuration) : "";
11689
11951
  const isActive = index === this.currentIndex;
11690
- const statusText = isActive ? "Currently playing" : "Not playing";
11691
- const actionText = isActive ? "Press Enter to restart" : "Press Enter to play";
11952
+ let ariaLabel = `${trackTitle}${trackArtist}`;
11953
+ if (trackDurationReadable) {
11954
+ ariaLabel += `. ${trackDurationReadable}`;
11955
+ }
11692
11956
  const item = DOMUtils.createElement("li", {
11693
11957
  className: isActive ? "vidply-playlist-item vidply-playlist-item-active" : "vidply-playlist-item",
11694
11958
  attributes: {
@@ -11699,23 +11963,28 @@ var VidPly = (() => {
11699
11963
  className: "vidply-playlist-item-button",
11700
11964
  attributes: {
11701
11965
  type: "button",
11966
+ role: "option",
11702
11967
  tabIndex: index === 0 ? 0 : -1,
11703
11968
  // Only first item is in tab order initially
11704
- "aria-label": `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`,
11969
+ "aria-label": ariaLabel,
11705
11970
  "aria-posinset": index + 1,
11706
- "aria-setsize": this.tracks.length
11971
+ "aria-setsize": this.tracks.length,
11972
+ "aria-checked": isActive ? "true" : "false"
11707
11973
  }
11708
11974
  });
11709
11975
  if (isActive) {
11710
11976
  button.setAttribute("aria-current", "true");
11711
11977
  button.setAttribute("tabIndex", "0");
11712
11978
  }
11713
- const thumbnail = DOMUtils.createElement("span", {
11714
- className: "vidply-playlist-thumbnail",
11979
+ const thumbnailContainer = DOMUtils.createElement("span", {
11980
+ className: "vidply-playlist-thumbnail-container",
11715
11981
  attributes: {
11716
11982
  "aria-hidden": "true"
11717
11983
  }
11718
11984
  });
11985
+ const thumbnail = DOMUtils.createElement("span", {
11986
+ className: "vidply-playlist-thumbnail"
11987
+ });
11719
11988
  if (track.poster) {
11720
11989
  thumbnail.style.backgroundImage = `url(${track.poster})`;
11721
11990
  } else {
@@ -11723,18 +11992,37 @@ var VidPly = (() => {
11723
11992
  icon.classList.add("vidply-playlist-thumbnail-icon");
11724
11993
  thumbnail.appendChild(icon);
11725
11994
  }
11726
- button.appendChild(thumbnail);
11995
+ thumbnailContainer.appendChild(thumbnail);
11996
+ if (trackDuration && track.poster) {
11997
+ const durationBadge = DOMUtils.createElement("span", {
11998
+ className: "vidply-playlist-duration-badge"
11999
+ });
12000
+ durationBadge.textContent = trackDuration;
12001
+ thumbnailContainer.appendChild(durationBadge);
12002
+ }
12003
+ button.appendChild(thumbnailContainer);
11727
12004
  const info = DOMUtils.createElement("span", {
11728
12005
  className: "vidply-playlist-item-info",
11729
12006
  attributes: {
11730
12007
  "aria-hidden": "true"
11731
12008
  }
11732
12009
  });
12010
+ const titleRow = DOMUtils.createElement("span", {
12011
+ className: "vidply-playlist-item-title-row"
12012
+ });
11733
12013
  const title = DOMUtils.createElement("span", {
11734
12014
  className: "vidply-playlist-item-title"
11735
12015
  });
11736
12016
  title.textContent = trackTitle;
11737
- info.appendChild(title);
12017
+ titleRow.appendChild(title);
12018
+ if (trackDuration && !track.poster) {
12019
+ const inlineDuration = DOMUtils.createElement("span", {
12020
+ className: "vidply-playlist-item-duration"
12021
+ });
12022
+ inlineDuration.textContent = trackDuration;
12023
+ titleRow.appendChild(inlineDuration);
12024
+ }
12025
+ info.appendChild(titleRow);
11738
12026
  if (track.artist) {
11739
12027
  const artist = DOMUtils.createElement("span", {
11740
12028
  className: "vidply-playlist-item-artist"
@@ -11742,6 +12030,13 @@ var VidPly = (() => {
11742
12030
  artist.textContent = track.artist;
11743
12031
  info.appendChild(artist);
11744
12032
  }
12033
+ if (track.description) {
12034
+ const description = DOMUtils.createElement("span", {
12035
+ className: "vidply-playlist-item-description"
12036
+ });
12037
+ description.textContent = track.description;
12038
+ info.appendChild(description);
12039
+ }
11745
12040
  button.appendChild(info);
11746
12041
  const playIcon = createIconElement("play");
11747
12042
  playIcon.classList.add("vidply-playlist-item-icon");
@@ -11825,7 +12120,11 @@ var VidPly = (() => {
11825
12120
  if (newIndex !== -1 && newIndex !== index) {
11826
12121
  buttons[index].setAttribute("tabIndex", "-1");
11827
12122
  buttons[newIndex].setAttribute("tabIndex", "0");
11828
- buttons[newIndex].focus();
12123
+ buttons[newIndex].focus({ preventScroll: false });
12124
+ const item = buttons[newIndex].closest(".vidply-playlist-item");
12125
+ if (item) {
12126
+ item.scrollIntoView({ behavior: "smooth", block: "nearest" });
12127
+ }
11829
12128
  }
11830
12129
  if (announcement && this.navigationFeedback) {
11831
12130
  this.navigationFeedback.textContent = announcement;
@@ -11853,21 +12152,29 @@ var VidPly = (() => {
11853
12152
  });
11854
12153
  const trackTitle = track.title || i18n.t("playlist.trackUntitled", { number: index + 1 });
11855
12154
  const trackArtist = track.artist ? i18n.t("playlist.by") + track.artist : "";
12155
+ const effectiveDuration = this.getEffectiveDuration(track);
12156
+ const trackDurationReadable = effectiveDuration ? TimeUtils.formatDuration(effectiveDuration) : "";
11856
12157
  if (index === this.currentIndex) {
11857
12158
  item.classList.add("vidply-playlist-item-active");
11858
12159
  button.setAttribute("aria-current", "true");
12160
+ button.setAttribute("aria-checked", "true");
11859
12161
  button.setAttribute("tabIndex", "0");
11860
- const statusText = "Currently playing";
11861
- const actionText = "Press Enter to restart";
11862
- button.setAttribute("aria-label", `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
12162
+ let ariaLabel = `${trackTitle}${trackArtist}`;
12163
+ if (trackDurationReadable) {
12164
+ ariaLabel += `. ${trackDurationReadable}`;
12165
+ }
12166
+ button.setAttribute("aria-label", ariaLabel);
11863
12167
  item.scrollIntoView({ behavior: "smooth", block: "nearest" });
11864
12168
  } else {
11865
12169
  item.classList.remove("vidply-playlist-item-active");
11866
12170
  button.removeAttribute("aria-current");
12171
+ button.setAttribute("aria-checked", "false");
11867
12172
  button.setAttribute("tabIndex", "-1");
11868
- const statusText = "Not playing";
11869
- const actionText = "Press Enter to play";
11870
- button.setAttribute("aria-label", `${trackPosition}. ${trackTitle}${trackArtist}. ${statusText}. ${actionText}.`);
12173
+ let ariaLabel = `${trackTitle}${trackArtist}`;
12174
+ if (trackDurationReadable) {
12175
+ ariaLabel += `. ${trackDurationReadable}`;
12176
+ }
12177
+ button.setAttribute("aria-label", ariaLabel);
11871
12178
  }
11872
12179
  });
11873
12180
  }
@@ -11966,7 +12273,7 @@ var VidPly = (() => {
11966
12273
  setTimeout(() => {
11967
12274
  const firstItem = this.playlistPanel.querySelector('.vidply-playlist-item[tabindex="0"]');
11968
12275
  if (firstItem) {
11969
- firstItem.focus();
12276
+ firstItem.focus({ preventScroll: true });
11970
12277
  }
11971
12278
  }, 100);
11972
12279
  }
@@ -11980,7 +12287,7 @@ var VidPly = (() => {
11980
12287
  if (this.player.controlBar && this.player.controlBar.controls.playlistToggle) {
11981
12288
  this.player.controlBar.controls.playlistToggle.setAttribute("aria-expanded", "false");
11982
12289
  this.player.controlBar.controls.playlistToggle.setAttribute("aria-pressed", "false");
11983
- this.player.controlBar.controls.playlistToggle.focus();
12290
+ this.player.controlBar.controls.playlistToggle.focus({ preventScroll: true });
11984
12291
  }
11985
12292
  }
11986
12293
  return this.isPanelVisible;