vidply 1.0.1 → 1.0.3

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.
@@ -109,6 +109,51 @@ export class ControlBar {
109
109
  }, 100);
110
110
  }
111
111
 
112
+ // Helper method to add keyboard navigation to menus (arrow keys)
113
+ attachMenuKeyboardNavigation(menu) {
114
+ const menuItems = Array.from(menu.querySelectorAll(`.${this.player.options.classPrefix}-menu-item`));
115
+
116
+ if (menuItems.length === 0) return;
117
+
118
+ const handleKeyDown = (e) => {
119
+ const currentIndex = menuItems.indexOf(document.activeElement);
120
+
121
+ switch (e.key) {
122
+ case 'ArrowDown':
123
+ e.preventDefault();
124
+ const nextIndex = (currentIndex + 1) % menuItems.length;
125
+ menuItems[nextIndex].focus();
126
+ break;
127
+
128
+ case 'ArrowUp':
129
+ e.preventDefault();
130
+ const prevIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
131
+ menuItems[prevIndex].focus();
132
+ break;
133
+
134
+ case 'Home':
135
+ e.preventDefault();
136
+ menuItems[0].focus();
137
+ break;
138
+
139
+ case 'End':
140
+ e.preventDefault();
141
+ menuItems[menuItems.length - 1].focus();
142
+ break;
143
+
144
+ case 'Enter':
145
+ case ' ':
146
+ e.preventDefault();
147
+ if (document.activeElement && menuItems.includes(document.activeElement)) {
148
+ document.activeElement.click();
149
+ }
150
+ break;
151
+ }
152
+ };
153
+
154
+ menu.addEventListener('keydown', handleKeyDown);
155
+ }
156
+
112
157
  createElement() {
113
158
  this.element = DOMUtils.createElement('div', {
114
159
  className: `${this.player.options.classPrefix}-controls`,
@@ -771,7 +816,8 @@ export class ControlBar {
771
816
  className: `${this.player.options.classPrefix}-menu-item`,
772
817
  attributes: {
773
818
  'type': 'button',
774
- 'role': 'menuitem'
819
+ 'role': 'menuitem',
820
+ 'tabindex': '-1'
775
821
  }
776
822
  });
777
823
 
@@ -796,6 +842,17 @@ export class ControlBar {
796
842
 
797
843
  menu.appendChild(item);
798
844
  }
845
+
846
+ // Add keyboard navigation
847
+ this.attachMenuKeyboardNavigation(menu);
848
+
849
+ // Focus first item
850
+ setTimeout(() => {
851
+ const firstItem = menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
852
+ if (firstItem) {
853
+ firstItem.focus();
854
+ }
855
+ }, 0);
799
856
  }
800
857
  }
801
858
 
@@ -869,6 +926,8 @@ export class ControlBar {
869
926
  });
870
927
  menu.appendChild(noQualityItem);
871
928
  } else {
929
+ let activeItem = null;
930
+
872
931
  // Auto quality option (only for HLS)
873
932
  if (isHLS) {
874
933
  const autoItem = DOMUtils.createElement('button', {
@@ -876,7 +935,8 @@ export class ControlBar {
876
935
  textContent: i18n.t('player.auto'),
877
936
  attributes: {
878
937
  'type': 'button',
879
- 'role': 'menuitem'
938
+ 'role': 'menuitem',
939
+ 'tabindex': '-1'
880
940
  }
881
941
  });
882
942
 
@@ -885,6 +945,7 @@ export class ControlBar {
885
945
  if (isAuto) {
886
946
  autoItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
887
947
  autoItem.appendChild(createIconElement('check'));
948
+ activeItem = autoItem;
888
949
  }
889
950
 
890
951
  autoItem.addEventListener('click', () => {
@@ -904,7 +965,8 @@ export class ControlBar {
904
965
  textContent: quality.name || `${quality.height}p`,
905
966
  attributes: {
906
967
  'type': 'button',
907
- 'role': 'menuitem'
968
+ 'role': 'menuitem',
969
+ 'tabindex': '-1'
908
970
  }
909
971
  });
910
972
 
@@ -912,6 +974,7 @@ export class ControlBar {
912
974
  if (quality.index === currentQuality) {
913
975
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
914
976
  item.appendChild(createIconElement('check'));
977
+ activeItem = item;
915
978
  }
916
979
 
917
980
  item.addEventListener('click', () => {
@@ -923,6 +986,17 @@ export class ControlBar {
923
986
 
924
987
  menu.appendChild(item);
925
988
  });
989
+
990
+ // Add keyboard navigation
991
+ this.attachMenuKeyboardNavigation(menu);
992
+
993
+ // Focus active item or first item
994
+ setTimeout(() => {
995
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
996
+ if (focusTarget) {
997
+ focusTarget.focus();
998
+ }
999
+ }, 0);
926
1000
  }
927
1001
  } else {
928
1002
  // No quality support
@@ -1054,6 +1128,14 @@ export class ControlBar {
1054
1128
 
1055
1129
  // Close menu on outside click (but not when interacting with controls)
1056
1130
  this.attachMenuCloseHandler(menu, button, true);
1131
+
1132
+ // Auto-focus the first select element
1133
+ setTimeout(() => {
1134
+ const firstSelect = menu.querySelector('select');
1135
+ if (firstSelect) {
1136
+ firstSelect.focus();
1137
+ }
1138
+ }, 0);
1057
1139
  }
1058
1140
 
1059
1141
  createStyleControl(label, property, options) {
@@ -1308,6 +1390,7 @@ export class ControlBar {
1308
1390
  });
1309
1391
 
1310
1392
  const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
1393
+ let activeItem = null;
1311
1394
 
1312
1395
  speeds.forEach(speed => {
1313
1396
  const item = DOMUtils.createElement('button', {
@@ -1315,13 +1398,15 @@ export class ControlBar {
1315
1398
  textContent: this.formatSpeedLabel(speed),
1316
1399
  attributes: {
1317
1400
  'type': 'button',
1318
- 'role': 'menuitem'
1401
+ 'role': 'menuitem',
1402
+ 'tabindex': '-1'
1319
1403
  }
1320
1404
  });
1321
1405
 
1322
1406
  if (speed === this.player.state.playbackSpeed) {
1323
1407
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1324
1408
  item.appendChild(createIconElement('check'));
1409
+ activeItem = item;
1325
1410
  }
1326
1411
 
1327
1412
  item.addEventListener('click', () => {
@@ -1335,8 +1420,19 @@ export class ControlBar {
1335
1420
  // Append menu directly to button for proper positioning
1336
1421
  button.appendChild(menu);
1337
1422
 
1423
+ // Add keyboard navigation
1424
+ this.attachMenuKeyboardNavigation(menu);
1425
+
1338
1426
  // Close menu on outside click
1339
1427
  this.attachMenuCloseHandler(menu, button);
1428
+
1429
+ // Focus the active item or first item
1430
+ setTimeout(() => {
1431
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
1432
+ if (focusTarget) {
1433
+ focusTarget.focus();
1434
+ }
1435
+ }, 0);
1340
1436
  }
1341
1437
 
1342
1438
  createCaptionsButton() {
@@ -1394,19 +1490,23 @@ export class ControlBar {
1394
1490
  return;
1395
1491
  }
1396
1492
 
1493
+ let activeItem = null;
1494
+
1397
1495
  // Off option
1398
1496
  const offItem = DOMUtils.createElement('button', {
1399
1497
  className: `${this.player.options.classPrefix}-menu-item`,
1400
1498
  textContent: i18n.t('captions.off'),
1401
1499
  attributes: {
1402
1500
  'type': 'button',
1403
- 'role': 'menuitem'
1501
+ 'role': 'menuitem',
1502
+ 'tabindex': '-1'
1404
1503
  }
1405
1504
  });
1406
1505
 
1407
1506
  if (!this.player.state.captionsEnabled) {
1408
1507
  offItem.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1409
1508
  offItem.appendChild(createIconElement('check'));
1509
+ activeItem = offItem;
1410
1510
  }
1411
1511
 
1412
1512
  offItem.addEventListener('click', () => {
@@ -1426,7 +1526,8 @@ export class ControlBar {
1426
1526
  attributes: {
1427
1527
  'type': 'button',
1428
1528
  'role': 'menuitem',
1429
- 'lang': track.language
1529
+ 'lang': track.language,
1530
+ 'tabindex': '-1'
1430
1531
  }
1431
1532
  });
1432
1533
 
@@ -1435,6 +1536,7 @@ export class ControlBar {
1435
1536
  this.player.captionManager.currentTrack === this.player.captionManager.tracks[track.index]) {
1436
1537
  item.classList.add(`${this.player.options.classPrefix}-menu-item-active`);
1437
1538
  item.appendChild(createIconElement('check'));
1539
+ activeItem = item;
1438
1540
  }
1439
1541
 
1440
1542
  item.addEventListener('click', () => {
@@ -1449,8 +1551,19 @@ export class ControlBar {
1449
1551
  // Append menu directly to button for proper positioning
1450
1552
  button.appendChild(menu);
1451
1553
 
1452
- // Close menu on outside click
1554
+ // Add keyboard navigation for the menu
1555
+ this.attachMenuKeyboardNavigation(menu);
1556
+
1557
+ // Close menu on outside click and Escape key
1453
1558
  this.attachMenuCloseHandler(menu, button);
1559
+
1560
+ // Focus the active item or the first item
1561
+ setTimeout(() => {
1562
+ const focusTarget = activeItem || menu.querySelector(`.${this.player.options.classPrefix}-menu-item`);
1563
+ if (focusTarget) {
1564
+ focusTarget.focus();
1565
+ }
1566
+ }, 0);
1454
1567
  }
1455
1568
 
1456
1569
  updateCaptionsButton() {
@@ -1689,33 +1802,38 @@ export class ControlBar {
1689
1802
  }
1690
1803
 
1691
1804
  updateVolumeDisplay() {
1692
- if (!this.controls.volumeFill) return;
1693
-
1694
1805
  const percent = this.player.state.volume * 100;
1695
- this.controls.volumeFill.style.height = `${percent}%`;
1696
1806
 
1697
- // Update mute button icon
1807
+ // Update volume fill bar if it exists
1808
+ if (this.controls.volumeFill) {
1809
+ this.controls.volumeFill.style.height = `${percent}%`;
1810
+ }
1811
+
1812
+ // Update mute button icon (should always work even if slider not shown)
1698
1813
  if (this.controls.mute) {
1699
1814
  const icon = this.controls.mute.querySelector('.vidply-icon');
1700
- let iconName;
1701
-
1702
- if (this.player.state.muted || this.player.state.volume === 0) {
1703
- iconName = 'volumeMuted';
1704
- } else if (this.player.state.volume < 0.3) {
1705
- iconName = 'volumeLow';
1706
- } else if (this.player.state.volume < 0.7) {
1707
- iconName = 'volumeMedium';
1708
- } else {
1709
- iconName = 'volumeHigh';
1710
- }
1815
+ if (icon) {
1816
+ let iconName;
1817
+
1818
+ if (this.player.state.muted || this.player.state.volume === 0) {
1819
+ iconName = 'volumeMuted';
1820
+ } else if (this.player.state.volume < 0.3) {
1821
+ iconName = 'volumeLow';
1822
+ } else if (this.player.state.volume < 0.7) {
1823
+ iconName = 'volumeMedium';
1824
+ } else {
1825
+ iconName = 'volumeHigh';
1826
+ }
1711
1827
 
1712
- icon.innerHTML = createIconElement(iconName).innerHTML;
1828
+ icon.innerHTML = createIconElement(iconName).innerHTML;
1713
1829
 
1714
- this.controls.mute.setAttribute('aria-label',
1715
- this.player.state.muted ? i18n.t('player.unmute') : i18n.t('player.mute')
1716
- );
1830
+ this.controls.mute.setAttribute('aria-label',
1831
+ this.player.state.muted ? i18n.t('player.unmute') : i18n.t('player.mute')
1832
+ );
1833
+ }
1717
1834
  }
1718
1835
 
1836
+ // Update volume slider attribute if it exists
1719
1837
  if (this.controls.volumeSlider) {
1720
1838
  this.controls.volumeSlider.setAttribute('aria-valuenow', String(Math.round(percent)));
1721
1839
  }
@@ -47,6 +47,11 @@ export class KeyboardManager {
47
47
  }
48
48
  }
49
49
  }
50
+
51
+ // Log unhandled keys for debugging (in development)
52
+ if (!handled && this.player.options.debug) {
53
+ console.log('[VidPly] Unhandled key:', e.key, 'code:', e.code, 'shiftKey:', e.shiftKey);
54
+ }
50
55
  }
51
56
 
52
57
  executeAction(action, event) {
@@ -71,14 +76,6 @@ export class KeyboardManager {
71
76
  this.player.seekBackward();
72
77
  return true;
73
78
 
74
- case 'seek-forward-large':
75
- this.player.seekForward(this.player.options.seekIntervalLarge);
76
- return true;
77
-
78
- case 'seek-backward-large':
79
- this.player.seekBackward(this.player.options.seekIntervalLarge);
80
- return true;
81
-
82
79
  case 'mute':
83
80
  this.player.toggleMute();
84
81
  return true;
@@ -91,15 +88,27 @@ export class KeyboardManager {
91
88
  // If only one caption track, toggle on/off
92
89
  // If multiple tracks, open caption menu
93
90
  if (this.player.captionManager && this.player.captionManager.tracks.length > 1) {
94
- const captionsButton = document.querySelector('.vidply-captions');
95
- if (captionsButton && this.player.controlBar) {
91
+ // Get captions button from control bar
92
+ const captionsButton = this.player.controlBar && this.player.controlBar.controls.captions;
93
+ if (captionsButton) {
96
94
  this.player.controlBar.showCaptionsMenu(captionsButton);
95
+ } else {
96
+ // Fallback to toggle if button doesn't exist
97
+ this.player.toggleCaptions();
97
98
  }
98
99
  } else {
99
100
  this.player.toggleCaptions();
100
101
  }
101
102
  return true;
102
103
 
104
+ case 'caption-style-menu':
105
+ // Open caption style menu
106
+ if (this.player.controlBar && this.player.controlBar.controls.captionStyle) {
107
+ this.player.controlBar.showCaptionStyleMenu(this.player.controlBar.controls.captionStyle);
108
+ return true;
109
+ }
110
+ return false;
111
+
103
112
  case 'speed-up':
104
113
  this.player.setPlaybackSpeed(
105
114
  Math.min(2, this.player.state.playbackSpeed + 0.25)
@@ -112,9 +121,37 @@ export class KeyboardManager {
112
121
  );
113
122
  return true;
114
123
 
115
- case 'settings':
116
- this.player.showSettings();
117
- return true;
124
+ case 'speed-menu':
125
+ // Open speed menu
126
+ if (this.player.controlBar && this.player.controlBar.controls.speed) {
127
+ this.player.controlBar.showSpeedMenu(this.player.controlBar.controls.speed);
128
+ return true;
129
+ }
130
+ return false;
131
+
132
+ case 'quality-menu':
133
+ // Open quality menu
134
+ if (this.player.controlBar && this.player.controlBar.controls.quality) {
135
+ this.player.controlBar.showQualityMenu(this.player.controlBar.controls.quality);
136
+ return true;
137
+ }
138
+ return false;
139
+
140
+ case 'chapters-menu':
141
+ // Open chapters menu
142
+ if (this.player.controlBar && this.player.controlBar.controls.chapters) {
143
+ this.player.controlBar.showChaptersMenu(this.player.controlBar.controls.chapters);
144
+ return true;
145
+ }
146
+ return false;
147
+
148
+ case 'transcript-toggle':
149
+ // Toggle transcript
150
+ if (this.player.transcriptManager) {
151
+ this.player.transcriptManager.toggleTranscript();
152
+ return true;
153
+ }
154
+ return false;
118
155
 
119
156
  default:
120
157
  return false;