Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mod key to be dynamic based on Mac/Windows #715

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/reference/actions.md
Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion examples/views/slideshow.ejs
@@ -1,13 +1,17 @@
<%- include("layout/head") %>

<div data-controller="slideshow" data-slideshow-current-slide-class="slide--current">
<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">
<button data-action="slideshow#previous">←</button>
<button data-action="slideshow#next">→</button>

<div data-slideshow-target="slide" class="slide">🐵</div>
<div data-slideshow-target="slide" class="slide">🙈</div>
<div data-slideshow-target="slide" class="slide">🙉</div>
<div data-slideshow-target="slide" class="slide">🙊</div>

<p>
Hint: Use keyboard shortcuts to navigate the slideshow: <kbd>mod + j</kbd> & <kbd>mod + k</kbd>
</p>
</div>

<%- include("layout/tail") %>
13 changes: 9 additions & 4 deletions src/core/action.ts
Expand Up @@ -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
Expand Down Expand Up @@ -98,9 +98,14 @@ export class Action {
}

private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filters: Array<string>): 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
)
}
}

Expand Down
3 changes: 3 additions & 0 deletions 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
Expand All @@ -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",
Expand Down
57 changes: 55 additions & 2 deletions src/tests/modules/core/action_keyboard_filter_tests.ts
Expand Up @@ -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
Expand All @@ -22,6 +25,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase {
<button id="button8" data-action="keydown.a->a#log keydown.b->a#log2"></button>
<button id="button9" data-action="keydown.shift+a->a#log keydown.a->a#log2 keydown.ctrl+shift+a->a#log3">
<button id="button10" data-action="jquery.custom.event->a#log jquery.a->a#log2">
<button id="button11" data-action="keydown.mod+s->a#log">
</div>
`

Expand Down Expand Up @@ -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 })
Expand All @@ -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+<key> (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+<key> (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+<key> (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
}
}