From 6b3291c352262e73d3f2253d4e22b31e27e9389d Mon Sep 17 00:00:00 2001 From: "LB (Ben Johnston)" Date: Mon, 21 Aug 2023 08:39:41 +1000 Subject: [PATCH] Add support for `mod` key to be dynamic based on Mac/Windows - Adds `mod` to the `defaultSchema` which will resolve to `Meta` on a MacOS like device and `Control` on Windows like - Uses `mod` to be used as a key filter modifier for either `metaKey` or `ctrlKey` based on the resolved value - Add unit tests and documentation for the `mod` key, and an example to the slideshow page - Closes #654 --- docs/reference/actions.md | 13 +++-- examples/views/slideshow.ejs | 6 +- src/core/action.ts | 13 +++-- src/core/schema.ts | 3 + .../core/action_keyboard_filter_tests.ts | 57 ++++++++++++++++++- 5 files changed, 79 insertions(+), 13 deletions(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 5c7e909b..778b0ad9 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -120,12 +120,13 @@ If you want to subscribe to a compound filter using a modifier key, you can writ The list of supported modifier keys is shown below. -| Modifier | Notes | -| -------- | ------------------ | -| `alt` | `option` on MacOS | -| `ctrl` | | -| `meta` | Command key on MacOS | -| `shift` | | +| Modifier | Notes | +| -------- | ------------------------------------------------------ | +| `alt` | `option` on MacOS | +| `ctrl` | | +| `meta` | `⌘ command` key on MacOS | +| `shift` | | +| `mod` | `⌘ command` (Meta) key on MacOS, `ctrl` key on Windows | ### Global Events diff --git a/examples/views/slideshow.ejs b/examples/views/slideshow.ejs index c404a9a8..23a0c8be 100644 --- a/examples/views/slideshow.ejs +++ b/examples/views/slideshow.ejs @@ -1,6 +1,6 @@ <%- include("layout/head") %> -
+
@@ -8,6 +8,10 @@
🙈
🙉
🙊
+ +

+ Hint: Use keyboard shortcuts to navigate the slideshow: mod + j & mod + k +

<%- include("layout/tail") %> diff --git a/src/core/action.ts b/src/core/action.ts index e009399b..54f34e6c 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -4,7 +4,7 @@ import { Schema } from "./schema" import { camelize } from "./string_helpers" import { hasProperty } from "./utils" -const allModifiers = ["meta", "ctrl", "alt", "shift"] +const allModifiers = ["meta", "ctrl", "alt", "shift", "mod"] export class Action { readonly element: Element @@ -98,9 +98,14 @@ export class Action { } private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filters: Array): boolean { - const [meta, ctrl, alt, shift] = allModifiers.map((modifier) => filters.includes(modifier)) - - return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift + const [meta, ctrl, alt, shift, mod] = allModifiers.map((modifier) => filters.includes(modifier)) + const modKey = mod && this.keyMappings.mod + return ( + event.metaKey !== (meta || modKey === "Meta") || + event.ctrlKey !== (ctrl || modKey === "Control") || + event.altKey !== alt || + event.shiftKey !== shift + ) } } diff --git a/src/core/schema.ts b/src/core/schema.ts index bbc5b24a..754a6830 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -1,3 +1,5 @@ +const isMac = typeof window !== "undefined" && /Mac|iPod|iPhone|iPad/.test(window.navigator?.platform || "") + export interface Schema { controllerAttribute: string actionAttribute: string @@ -14,6 +16,7 @@ export const defaultSchema: Schema = { targetAttributeForScope: (identifier) => `data-${identifier}-target`, outletAttributeForScope: (identifier, outlet) => `data-${identifier}-${outlet}-outlet`, keyMappings: { + mod: isMac ? "Meta" : "Control", enter: "Enter", tab: "Tab", esc: "Escape", diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index 64a9303c..dfe95a93 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -3,7 +3,10 @@ import { LogControllerTestCase } from "../../cases/log_controller_test_case" import { Schema, defaultSchema } from "../../../core/schema" import { Application } from "../../../core/application" -const customSchema = { ...defaultSchema, keyMappings: { ...defaultSchema.keyMappings, a: "a", b: "b" } } +const customSchema = { + ...defaultSchema, + keyMappings: { ...defaultSchema.keyMappings, a: "a", b: "b" } as { [key: string]: string }, +} export default class ActionKeyboardFilterTests extends LogControllerTestCase { schema: Schema = customSchema @@ -22,6 +25,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase {
` @@ -177,7 +181,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { this.assertActions({ name: "log2", identifier: "a", eventType: "keydown", currentTarget: button }) } - async "test ignore event handlers associated with modifiers other than ctrol+shift+a"() { + async "test ignore event handlers associated with modifiers other than ctrl+shift+a"() { const button = this.findElement("#button9") await this.nextFrame await this.triggerKeyboardEvent(button, "keydown", { key: "A", ctrlKey: true, shiftKey: true }) @@ -197,4 +201,53 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { await this.triggerEvent(button, "jquery.a") this.assertActions({ name: "log2", identifier: "a", eventType: "jquery.a", currentTarget: button }) } + + async "test that the default schema keyMappings.mod value is based on the platform"() { + const expectedKeyMapping = navigator.platform?.match(/Mac|iPod|iPhone|iPad/) ? "Meta" : "Control" + this.assert.equal(defaultSchema.keyMappings.mod, expectedKeyMapping) + } + + async "test ignore event handlers associated with modifiers mod+ (dynamic based on platform)"() { + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true }) + await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true }) + // We should only see one event using `mod` (which is dynamic based on platform) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + + customSchema.keyMappings.mod = "Control" // set up for next test + } + + async "test ignore event handlers associated with modifiers mod+ (set to 'Control')"() { + // see .mod setting in previous test (mocking Windows) + this.schema = { + ...this.application.schema, + keyMappings: { ...this.application.schema.keyMappings, mod: "Control" }, + } + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true }) + this.assertNoActions() + await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + + customSchema.keyMappings.mod = "Meta" // set up for next test + } + + async "test ignore event handlers associated with modifiers mod+ (set to 'Meta')"() { + // see .mod setting in previous test (mocking Windows) + this.schema = { + ...this.application.schema, + keyMappings: { ...this.application.schema.keyMappings, mod: "Meta" }, + } + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true }) + this.assertNoActions() + await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + + // Reset to default for any subsequent tests + this.schema = customSchema + } }