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
+ }
}