Skip to content

Commit cd03c7e

Browse files
committed
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 - Allows `mod` to be overridden at the schema keyMappings level if needed and used as a standalone key - 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
1 parent 8cbca6d commit cd03c7e

File tree

5 files changed

+54
-13
lines changed

5 files changed

+54
-13
lines changed

docs/reference/actions.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,13 @@ If you want to subscribe to a compound filter using a modifier key, you can writ
120120

121121
The list of supported modifier keys is shown below.
122122

123-
| Modifier | Notes |
124-
| -------- | ------------------ |
125-
| `alt` | `option` on MacOS |
126-
| `ctrl` | |
127-
| `meta` | Command key on MacOS |
128-
| `shift` | |
123+
| Modifier | Notes |
124+
| -------- | ------------------------------------------------------ |
125+
| `alt` | `option` on MacOS |
126+
| `ctrl` | |
127+
| `meta` | `⌘ command` key on MacOS |
128+
| `shift` | |
129+
| `mod` | `⌘ command` (Meta) key on MacOS, `ctrl` key on Windows |
129130

130131
### Global Events
131132

examples/views/slideshow.ejs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
<%- include("layout/head") %>
22

3-
<div data-controller="slideshow" data-slideshow-current-slide-class="slide--current">
3+
<div data-controller="slideshow" data-slideshow-current-slide-class="slide--current" data-action="keydown.mod+j@document->slideshow#previous keydown.mod+k@document->slideshow#next">
44
<button data-action="slideshow#previous">←</button>
55
<button data-action="slideshow#next">→</button>
66

77
<div data-slideshow-target="slide" class="slide">🐵</div>
88
<div data-slideshow-target="slide" class="slide">🙈</div>
99
<div data-slideshow-target="slide" class="slide">🙉</div>
1010
<div data-slideshow-target="slide" class="slide">🙊</div>
11+
12+
<p>
13+
Hint: Use keyboard shortcuts to navigate the slideshow: <kbd>mod + j</kbd> & <kbd>mod + k</kbd>
14+
</p>
1115
</div>
1216

1317
<%- include("layout/tail") %>

src/core/action.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Schema } from "./schema"
44
import { camelize } from "./string_helpers"
55
import { hasProperty } from "./utils"
66

7-
const allModifiers = ["meta", "ctrl", "alt", "shift"]
7+
const allModifiers = ["meta", "ctrl", "alt", "shift", "mod"]
88

99
export class Action {
1010
readonly element: Element
@@ -98,9 +98,14 @@ export class Action {
9898
}
9999

100100
private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filters: Array<string>): boolean {
101-
const [meta, ctrl, alt, shift] = allModifiers.map((modifier) => filters.includes(modifier))
102-
103-
return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift
101+
const [meta, ctrl, alt, shift, mod] = allModifiers.map((modifier) => filters.includes(modifier))
102+
const modKey = mod && this.keyMappings.mod
103+
return (
104+
event.metaKey !== (meta || modKey === "Meta") ||
105+
event.ctrlKey !== (ctrl || modKey === "Control") ||
106+
event.altKey !== alt ||
107+
event.shiftKey !== shift
108+
)
104109
}
105110
}
106111

src/core/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const isMac = typeof window !== "undefined" && /Mac|iPod|iPhone|iPad/.test(window.navigator?.platform || "")
2+
13
export interface Schema {
24
controllerAttribute: string
35
actionAttribute: string
@@ -14,6 +16,7 @@ export const defaultSchema: Schema = {
1416
targetAttributeForScope: (identifier) => `data-${identifier}-target`,
1517
outletAttributeForScope: (identifier, outlet) => `data-${identifier}-${outlet}-outlet`,
1618
keyMappings: {
19+
mod: isMac ? "Meta" : "Control",
1720
enter: "Enter",
1821
tab: "Tab",
1922
esc: "Escape",

src/tests/modules/core/action_keyboard_filter_tests.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { LogControllerTestCase } from "../../cases/log_controller_test_case"
33
import { Schema, defaultSchema } from "../../../core/schema"
44
import { Application } from "../../../core/application"
55

6-
const customSchema = { ...defaultSchema, keyMappings: { ...defaultSchema.keyMappings, a: "a", b: "b" } }
6+
const customSchema = {
7+
...defaultSchema,
8+
keyMappings: { ...defaultSchema.keyMappings, a: "a", b: "b" } as { [key: string]: string },
9+
}
710

811
export default class ActionKeyboardFilterTests extends LogControllerTestCase {
912
schema: Schema = customSchema
@@ -22,6 +25,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase {
2225
<button id="button8" data-action="keydown.a->a#log keydown.b->a#log2"></button>
2326
<button id="button9" data-action="keydown.shift+a->a#log keydown.a->a#log2 keydown.ctrl+shift+a->a#log3">
2427
<button id="button10" data-action="jquery.custom.event->a#log jquery.a->a#log2">
28+
<button id="button11" data-action="keydown.mod+s->a#log">
2529
</div>
2630
`
2731

@@ -177,7 +181,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase {
177181
this.assertActions({ name: "log2", identifier: "a", eventType: "keydown", currentTarget: button })
178182
}
179183

180-
async "test ignore event handlers associated with modifiers other than ctrol+shift+a"() {
184+
async "test ignore event handlers associated with modifiers other than ctrl+shift+a"() {
181185
const button = this.findElement("#button9")
182186
await this.nextFrame
183187
await this.triggerKeyboardEvent(button, "keydown", { key: "A", ctrlKey: true, shiftKey: true })
@@ -197,4 +201,28 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase {
197201
await this.triggerEvent(button, "jquery.a")
198202
this.assertActions({ name: "log2", identifier: "a", eventType: "jquery.a", currentTarget: button })
199203
}
204+
205+
async "test ignore event handlers associated with modifiers mod+s (macOS = Meta)"() {
206+
const button = this.findElement("#button11")
207+
await this.nextFrame
208+
await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true })
209+
this.assertNoActions()
210+
await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true })
211+
this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button })
212+
customSchema.keyMappings.mod = "Control" // set up for next test
213+
}
214+
215+
async "test ignore event handlers associated with modifiers mod+s (Windows = Ctrl)"() {
216+
// see .mod setting in previous test (mocking Windows)
217+
this.schema = {
218+
...this.application.schema,
219+
keyMappings: { ...this.application.schema.keyMappings, mod: "Control" },
220+
}
221+
const button = this.findElement("#button11")
222+
await this.nextFrame
223+
await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true })
224+
this.assertNoActions()
225+
await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true })
226+
this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button })
227+
}
200228
}

0 commit comments

Comments
 (0)