Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/webamp-docs/docs/05_features/01_hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const webamp = new Webamp({
webamp.renderWhenReady(document.getElementById("winamp-container"));
```

By default, hotkeys are handled anywhere in the page. If your page has its own keyboard interactions, pass `enableHotkeys: "scoped"` _(since [Unreleased](../12_changelog.md#unreleased))_ to only handle hotkeys while focus is within Webamp.

## Hotkeys

| Key Combo | Action |
Expand Down
6 changes: 5 additions & 1 deletion packages/webamp-docs/docs/06_API/02_webamp-constructor.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ In keeping with the original Winamp, **double size mode does not apply to resiza

### `enableHotkeys`

A boolean indicating if global hotkeys should be enabled. **Default:** `false`.
Controls whether hotkeys are enabled. **Default:** `false`.

- `true`: Hotkeys are handled anywhere in the page.
- `"scoped"` _(since [Unreleased](../12_changelog.md#unreleased))_: Hotkeys are only handled while focus is within Webamp. Use this if your page has its own keyboard interactions.
- `false`: Hotkeys are disabled.

```ts
const webamp = new Webamp({
Expand Down
1 change: 1 addition & 0 deletions packages/webamp-docs/docs/12_changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ If you want access to the changes in this section before they are officially rel
- The Milkdrop window now has a stable id, `#milkdrop-window`, like `#main-window`, `#equalizer-window` and `#playlist-window`.
- Added a new Webamp instance method: [`webamp.onMediaError(cb)`](./06_API/03_instance-methods.md#onmediaerrorcb-error--code-number---void---void), which is called when a track fails to load or play.
- Added a new Webamp instance method: [`webamp.setTracks(tracks)`](./06_API/03_instance-methods.md#settrackstracks-track-void), which replaces the playlist without starting playback.
- The [`enableHotkeys`](./06_API/02_webamp-constructor.md#enablehotkeys) option now accepts `"scoped"`, which only handles hotkeys while focus is within Webamp.

### Bug Fixes

Expand Down
17 changes: 15 additions & 2 deletions packages/webamp/js/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
StrictMode,
} from "react";
import ReactDOM from "react-dom";
import { FilePicker } from "../types";
import { FilePicker, Dispatch } from "../types";
import { WINDOWS } from "../constants";
import * as Selectors from "../selectors";
import * as Actions from "../actionCreators";
Expand All @@ -21,7 +21,8 @@ import EqualizerWindow from "./EqualizerWindow";
import Skin from "./Skin";

import { IMedia } from "../media";
import { useTypedSelector, useActionCreator } from "../hooks";
import { useTypedSelector, useActionCreator, useTypedDispatch } from "../hooks";
import { bindHotkeys } from "../hotkeys";
import Css from "./Css";

// @ts-ignore
Expand All @@ -31,6 +32,7 @@ interface Props {
filePickers: FilePicker[];
media: IMedia;
parentDomNode: HTMLElement;
enableHotkeys: boolean | "scoped";
onMount?: () => void;
}

Expand All @@ -42,6 +44,7 @@ export default function App({
filePickers,
onMount,
parentDomNode,
enableHotkeys,
}: Props) {
const closed = useTypedSelector(Selectors.getClosed);
const genWindowsInfo = useTypedSelector(Selectors.getGenWindows);
Expand All @@ -64,6 +67,16 @@ export default function App({
webampNode.style.zIndex = String(zIndex);
}, [webampNode, zIndex]);

const dispatch = useTypedDispatch();
useEffect(() => {
if (!enableHotkeys) {
return;
}
// Scoped hotkeys only see key events from within Webamp's DOM node.
const target = enableHotkeys === "scoped" ? webampNode : document;
return bindHotkeys(dispatch as Dispatch, target);
}, [enableHotkeys, dispatch, webampNode]);

useLayoutEffect(() => {
parentDomNode.appendChild(webampNode);
return () => {
Expand Down
54 changes: 54 additions & 0 deletions packages/webamp/js/hotkeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { bindHotkeys } from "./hotkeys";

function pressR(target: EventTarget) {
// R toggles repeat.
target.dispatchEvent(
new KeyboardEvent("keydown", { keyCode: 82, bubbles: true })
);
}

describe("bindHotkeys", () => {
let webampNode: HTMLElement;
let button: HTMLElement;
let outside: HTMLElement;

beforeEach(() => {
webampNode = document.createElement("div");
button = document.createElement("button");
webampNode.appendChild(button);
outside = document.createElement("button");
document.body.append(webampNode, outside);
});

afterEach(() => {
webampNode.remove();
outside.remove();
});

it("handles hotkeys anywhere in the document by default", () => {
const dispatch = vi.fn();
const unbind = bindHotkeys(dispatch);
pressR(outside);
pressR(button);
unbind();
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenCalledWith({ type: "TOGGLE_REPEAT" });
});

it("only handles hotkeys within the target when given one", () => {
const dispatch = vi.fn();
const unbind = bindHotkeys(dispatch, webampNode);
pressR(outside);
expect(dispatch).not.toHaveBeenCalled();
pressR(button);
expect(dispatch).toHaveBeenCalledWith({ type: "TOGGLE_REPEAT" });
unbind();
});

it("stops handling hotkeys once unbound", () => {
const dispatch = vi.fn();
bindHotkeys(dispatch, webampNode)();
pressR(button);
expect(dispatch).not.toHaveBeenCalled();
});
});
14 changes: 11 additions & 3 deletions packages/webamp/js/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import { Dispatch } from "./types";

const IGNORE_EVENTS_FROM_TAGS = new Set(["input", "textarea", "select"]);

export function bindHotkeys(dispatch: Dispatch): () => void {
// By default hotkeys are handled anywhere in the document. Pass a `target` to
// only handle them when focus is within that element.
export function bindHotkeys(
dispatch: Dispatch,
target: Document | HTMLElement = document
): () => void {
let currentPos: number = 0;
const trigger = [
78, // N
Expand Down Expand Up @@ -148,9 +153,12 @@ export function bindHotkeys(dispatch: Dispatch): () => void {
}
}
};
document.addEventListener("keydown", listener);
// TypeScript can't pick the right overload for the `Document | HTMLElement`
// union.
const eventListener = listener as EventListener;
target.addEventListener("keydown", eventListener);

return () => {
document.removeEventListener("keydown", listener);
target.removeEventListener("keydown", eventListener);
};
}
9 changes: 7 additions & 2 deletions packages/webamp/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,11 +688,16 @@ export interface Options {
enableDoubleSizeMode?: boolean;

/**
* Should global hotkeys be enabled?
* Should hotkeys be enabled?
*
* - `true`: Hotkeys are handled anywhere in the page.
* - `"scoped"`: Hotkeys are only handled while focus is within Webamp. Useful
* if the page has other keyboard interactions.
* - `false`: Hotkeys are disabled.
*
* Default: `false`
*/
enableHotkeys?: boolean;
enableHotkeys?: boolean | "scoped";

/**
* An array of additional file pickers.
Expand Down
7 changes: 1 addition & 6 deletions packages/webamp/js/webampLazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
} from "./types";
import getStore from "./store";
import App from "./components/App";
import { bindHotkeys } from "./hotkeys";
import Media, { IMedia, IMediaClass } from "./media";
import * as Selectors from "./selectors";
import * as Actions from "./actionCreators";
Expand Down Expand Up @@ -83,7 +82,6 @@ class Webamp {
initialTracks,
initialSkin,
availableSkins,
enableHotkeys = false,
zIndex,
requireJSZip,
requireMusicMetadata,
Expand Down Expand Up @@ -214,10 +212,6 @@ class Webamp {
}

this.store.dispatch(Actions.setWindowLayout(options.windowLayout));

if (enableHotkeys) {
this._disposable.add(bindHotkeys(this.store.dispatch));
}
}

/**
Expand Down Expand Up @@ -556,6 +550,7 @@ class Webamp {
<App
media={this.media}
filePickers={this.options.filePickers || []}
enableHotkeys={this.options.enableHotkeys ?? false}
onMount={onMount}
parentDomNode={contained ? node : document.body}
/>
Expand Down
Loading