slop game oder so
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* HTML-Overlays über dem Spiel-Canvas (Disclaimer, Pause, Namenseingabe).
|
||||
*
|
||||
* Warum HTML statt Canvas-Text: Formulare, Schieberegler und längere Texte sind so
|
||||
* für Screenreader und Tastatur bedienbar, und Browser-Features (Autokorrektur aus,
|
||||
* Textauswahl) funktionieren ohne Nachbau.
|
||||
*
|
||||
* Sicherheit: Dynamische Inhalte werden ausschließlich per `textContent` gesetzt,
|
||||
* niemals per `innerHTML` – Spielernamen aus der Highscore-Liste können also
|
||||
* kein HTML einschleusen.
|
||||
* @module systems/Overlay
|
||||
*/
|
||||
|
||||
/**
|
||||
* Erzeugt ein Element mit Klasse und optionalem Text.
|
||||
* @param {string} tag
|
||||
* @param {string} [className]
|
||||
* @param {string} [text]
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} OverlayButton
|
||||
* @property {string} label
|
||||
* @property {() => void} onClick
|
||||
* @property {boolean} [secondary]
|
||||
* @property {string} [testId] data-testid für End-to-End-Tests.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} OverlayOptions
|
||||
* @property {string} [title]
|
||||
* @property {Node[]} [content]
|
||||
* @property {OverlayButton[]} [buttons]
|
||||
* @property {string} [id]
|
||||
* @property {() => void} [onEscape] Wird bei Esc aufgerufen (z. B. Schließen/Fortsetzen).
|
||||
*/
|
||||
|
||||
export class Overlay {
|
||||
/**
|
||||
* Öffnet ein Overlay im Element #overlay-root.
|
||||
* @param {OverlayOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
const root = document.getElementById('overlay-root') ?? document.body;
|
||||
this.previousFocus = document.activeElement;
|
||||
this.onEscape = options.onEscape;
|
||||
|
||||
this.element = el('div', 'overlay');
|
||||
if (options.id) this.element.id = options.id;
|
||||
this.element.setAttribute('role', 'dialog');
|
||||
this.element.setAttribute('aria-modal', 'true');
|
||||
|
||||
this.panel = el('div', 'overlay__panel');
|
||||
this.element.append(this.panel);
|
||||
|
||||
if (options.title) {
|
||||
const heading = el('h2', 'disclaimer__title', options.title);
|
||||
heading.id = `${options.id ?? 'overlay'}-title`;
|
||||
this.element.setAttribute('aria-labelledby', heading.id);
|
||||
this.panel.append(heading);
|
||||
}
|
||||
for (const node of options.content ?? []) this.panel.append(node);
|
||||
|
||||
this.actions = el('div', 'overlay__actions');
|
||||
for (const button of options.buttons ?? []) this.addButton(button);
|
||||
if (this.actions.childElementCount > 0) this.panel.append(this.actions);
|
||||
|
||||
this.handleKeyDown = (event) => {
|
||||
// Tasten im Overlay gehören dem Overlay: Ohne stopPropagation würde z. B. das
|
||||
// Enter, mit dem man den Disclaimer bestätigt, danach auch das Menü auslösen.
|
||||
event.stopPropagation();
|
||||
if (event.key === 'Escape' && this.onEscape) {
|
||||
event.preventDefault();
|
||||
this.onEscape();
|
||||
}
|
||||
};
|
||||
this.element.addEventListener('keydown', this.handleKeyDown);
|
||||
root.append(this.element);
|
||||
|
||||
// Fokus ins Overlay, damit Tastaturnutzer sofort bedienen können.
|
||||
const focusTarget = this.panel.querySelector('input, button, a');
|
||||
if (focusTarget instanceof HTMLElement) focusTarget.focus();
|
||||
else {
|
||||
this.panel.tabIndex = -1;
|
||||
this.panel.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OverlayButton} button
|
||||
* @returns {HTMLButtonElement}
|
||||
*/
|
||||
addButton(button) {
|
||||
const node = /** @type {HTMLButtonElement} */ (el('button', `button${button.secondary ? ' button--secondary' : ''}`, button.label));
|
||||
node.type = 'button';
|
||||
if (button.testId) node.dataset.testid = button.testId;
|
||||
node.addEventListener('click', () => button.onClick());
|
||||
this.actions.append(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Entfernt das Overlay und gibt den Fokus zurück an das Spiel. */
|
||||
close() {
|
||||
this.element.removeEventListener('keydown', this.handleKeyDown);
|
||||
this.element.remove();
|
||||
const game = document.querySelector('#game canvas');
|
||||
if (game instanceof HTMLElement) {
|
||||
game.tabIndex = -1;
|
||||
game.focus({ preventScroll: true });
|
||||
} else if (this.previousFocus instanceof HTMLElement) {
|
||||
this.previousFocus.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user