-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: initial keyboard shortcut support #263
Changes from all commits
b32326a
653e7d6
9ee35c4
569eff8
bd96d27
324a465
1d08679
56b60a7
c79aaef
cc6af1a
8c10b0c
b2f212d
38f65f8
b9d63e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,6 +82,28 @@ Example (disabling gestures via `gestures-disabled`): | |
</media-controller> | ||
``` | ||
|
||
- `nohotkeys` - Use this to turn off keyboard shortcuts. | ||
|
||
Example (hotkeys disabled): | ||
|
||
```html | ||
<media-controller nohotkeys> | ||
<video | ||
slot="media" | ||
src="https://stream.mux.com/DS00Spx1CV902MCtPj5WknGlR102V5HFkDe/high.mp4" | ||
></video> | ||
<media-control-bar> | ||
<media-seek-backward-button></media-seek-backward-button> | ||
<media-play-button></media-play-button> | ||
<media-seek-forward-button></media-seek-forward-button> | ||
<media-mute-button></media-mute-button> | ||
<media-volume-range></media-volume-range> | ||
<media-time-range></media-time-range> | ||
<media-time-display></media-time-display> | ||
</media-control-bar> | ||
</media-controller> | ||
``` | ||
|
||
# Styling | ||
|
||
- `aspect-ratio` - While this is [a standard CSS style](https://css-tricks.com/almanac/properties/a/aspect-ratio/), it's fairly new, and you're likely to want to use it frequently on `<media-controller/>`, at least for `video` use cases. Most often, you'll want the `aspect-ratio` to match your video content's aspect ratio. | ||
|
@@ -272,3 +294,31 @@ As we work through other common use cases, both internally and with the communit | |
- Example use cases: | ||
- [Basic example](https://media-chrome.mux.dev/examples/basic.html) ([view source](https://github.com/muxinc/media-chrome/blob/main/examples/basic.html)) | ||
- [Spotify theme](https://media-chrome.mux.dev/examples/themes/spotify-theme.html) ([view source](https://github.com/muxinc/media-chrome/blob/main/examples/themes/spotify-theme.html)) | ||
|
||
## Keyboard Shortcuts | ||
|
||
By default, Media Controller has keyboard shortcuts that will trigger behavior when specific keys are pressed when the focus is inside the Media Controller. | ||
The following controls are supported: | ||
| Key | Behavior | | ||
|---------|-------------------| | ||
| Space | Toggle Playback | | ||
| `k` | Toggle Playback | | ||
| `m` | Toggle mute | | ||
| `f` | Toggle fullscreen | | ||
| ⬅ | Seek back 10s | | ||
| ➡ | Seek forward 10s | | ||
|
||
If you are implementing an interactive element that uses any of these keys, you can stopPropagation in your `keyup` handler. Alternatively, you can add a `keysUsed` property on the element or a `keysused` attribute. The values are those that match the `key` property on the KeyboardEvent. You can find a list of those values [on mdn](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Additionally, since the DOM list can't have the Space key represented as `" "`, we will accept `Space` as an alternative name for it. | ||
Example (`keysused` attribute): | ||
```html | ||
<media-time-range keysused="ArrowLeft ArrowRight Space"></media-time-range> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😎 |
||
``` | ||
|
||
Example (`keysUsed` property): | ||
```js | ||
class MyInteractiveElement extends window.HTMLElement { | ||
get keysUsed() { | ||
return ['Enter', ' ']; | ||
} | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,18 @@ const { | |
MEDIA_PLAYBACK_RATE_REQUEST, | ||
} = MediaUIEvents; | ||
|
||
const ButtonPressedKeys = ['Enter', ' ', 'f', 'm', 'k', 'ArrowLeft', 'ArrowRight']; | ||
const DEFAULT_SEEK_OFFSET = 10; | ||
|
||
/** | ||
* Media Controller should not mimic the HTMLMediaElement API. | ||
* @see https://github.com/muxinc/media-chrome/pull/182#issuecomment-1067370339 | ||
*/ | ||
Comment on lines
+51
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved from the bottom of the class here, which seems more appropriate. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
class MediaController extends MediaContainer { | ||
static get observedAttributes() { | ||
return super.observedAttributes.concat('nohotkeys'); | ||
} | ||
|
||
constructor() { | ||
super(); | ||
|
||
|
@@ -477,6 +488,20 @@ class MediaController extends MediaContainer { | |
); | ||
}, | ||
}; | ||
|
||
this.enableHotkeys(); | ||
} | ||
|
||
attributeChangedCallback(attrName, oldValue, newValue) { | ||
if (attrName === 'nohotkeys') { | ||
if (newValue !== oldValue && newValue === '') { | ||
this.disableHotkeys(); | ||
} else if (newValue !== oldValue && newValue === null) { | ||
this.enableHotkeys(); | ||
} | ||
} | ||
|
||
super.attributeChangedCallback(attrName, oldValue, newValue); | ||
} | ||
|
||
mediaSetCallback(media) { | ||
|
@@ -710,10 +735,121 @@ class MediaController extends MediaContainer { | |
els.splice(index, 1); | ||
} | ||
|
||
/** | ||
* Media Controller should not mimic the HTMLMediaElement API. | ||
* @see https://github.com/muxinc/media-chrome/pull/182#issuecomment-1067370339 | ||
*/ | ||
#keyUpHandler(e) { | ||
const { key } = e; | ||
if (!ButtonPressedKeys.includes(key)) { | ||
this.removeEventListener('keyup', this.#keyUpHandler); | ||
return; | ||
} | ||
|
||
this.keyboardShortcutHandler(e); | ||
} | ||
|
||
#keyDownHandler(e) { | ||
const { metaKey, altKey, key } = e; | ||
if (metaKey || altKey || !ButtonPressedKeys.includes(key)) { | ||
this.removeEventListener('keyup', this.#keyUpHandler); | ||
return; | ||
} | ||
this.addEventListener('keyup', this.#keyUpHandler); | ||
} | ||
|
||
enableHotkeys() { | ||
this.addEventListener('keydown', this.#keyDownHandler); | ||
} | ||
|
||
disableHotkeys() { | ||
this.removeEventListener('keydown', this.#keyDownHandler); | ||
this.removeEventListener('keyup', this.#keyUpHandler); | ||
} | ||
|
||
keyboardShortcutHandler(e) { | ||
// if the event's key is already handled by the target, skip keyboard shortcuts | ||
// keysUsed is either an attribute or a property. | ||
// The attribute is a DOM array and the property is a JS array | ||
// In the attribute Space represents the space key and gets convered to ' ' | ||
const keysUsed = (e.target.getAttribute('keysused')?.split(' ') ?? e.target?.keysUsed ?? []) | ||
.map(key => key === 'Space' ? ' ' : key) | ||
.filter(Boolean); | ||
|
||
if (keysUsed.includes(e.key)) { | ||
return; | ||
} | ||
|
||
let eventName, currentTimeStr, currentTime, detail, evt; | ||
const seekOffset = DEFAULT_SEEK_OFFSET; | ||
|
||
// These event triggers were copied from the revelant buttons | ||
switch (e.key) { | ||
case ' ': | ||
case 'k': | ||
eventName = | ||
this.getAttribute(MediaUIAttributes.MEDIA_PAUSED) != null | ||
? MediaUIEvents.MEDIA_PLAY_REQUEST | ||
: MediaUIEvents.MEDIA_PAUSE_REQUEST; | ||
this.dispatchEvent( | ||
new window.CustomEvent(eventName, { composed: true, bubbles: true }) | ||
); | ||
break; | ||
|
||
case 'm': | ||
eventName = | ||
this.getAttribute(MediaUIAttributes.MEDIA_VOLUME_LEVEL) === 'off' | ||
? MediaUIEvents.MEDIA_UNMUTE_REQUEST | ||
: MediaUIEvents.MEDIA_MUTE_REQUEST; | ||
this.dispatchEvent( | ||
new window.CustomEvent(eventName, { composed: true, bubbles: true }) | ||
); | ||
break; | ||
|
||
case 'f': | ||
eventName = | ||
this.getAttribute(MediaUIAttributes.MEDIA_IS_FULLSCREEN) != null | ||
? MediaUIEvents.MEDIA_EXIT_FULLSCREEN_REQUEST | ||
: MediaUIEvents.MEDIA_ENTER_FULLSCREEN_REQUEST; | ||
this.dispatchEvent( | ||
new window.CustomEvent(eventName, { composed: true, bubbles: true }) | ||
); | ||
break; | ||
|
||
case 'ArrowLeft': | ||
currentTimeStr = this.getAttribute( | ||
MediaUIAttributes.MEDIA_CURRENT_TIME | ||
); | ||
currentTime = | ||
currentTimeStr && !Number.isNaN(+currentTimeStr) | ||
? +currentTimeStr | ||
: DEFAULT_TIME; | ||
detail = Math.max(currentTime - seekOffset, 0); | ||
evt = new window.CustomEvent(MediaUIEvents.MEDIA_SEEK_REQUEST, { | ||
composed: true, | ||
bubbles: true, | ||
detail, | ||
}); | ||
this.dispatchEvent(evt); | ||
break; | ||
|
||
case 'ArrowRight': | ||
currentTimeStr = this.getAttribute( | ||
MediaUIAttributes.MEDIA_CURRENT_TIME | ||
); | ||
currentTime = | ||
currentTimeStr && !Number.isNaN(+currentTimeStr) | ||
? +currentTimeStr | ||
: DEFAULT_TIME; | ||
detail = Math.max(currentTime + seekOffset, 0); | ||
evt = new window.CustomEvent(MediaUIEvents.MEDIA_SEEK_REQUEST, { | ||
composed: true, | ||
bubbles: true, | ||
detail, | ||
}); | ||
this.dispatchEvent(evt); | ||
break; | ||
|
||
default: | ||
break; | ||
} | ||
} | ||
} | ||
|
||
const getPaused = (controller) => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍