Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
13fa598
[#448] Bugfix: Make Forever buttons accessible with Tab
omargfh Oct 9, 2025
e33b5ab
[#448] Bugfix: Make switches accessible with Tab and usable with Enter
omargfh Oct 9, 2025
200d195
[#448] Feat: Add Keyboard Listener class to manage keybinds
omargfh Oct 9, 2025
725baf9
[#448] Feat: Add keybinds help component
omargfh Oct 9, 2025
cd90046
[#448] Feat: Add utility selector for focus-able elements
omargfh Oct 9, 2025
aca4a63
[#448] Feat: Add keybinds and hook them into Skeleton, hook keybind h…
omargfh Oct 9, 2025
4654f2c
[#448, #docs] Handle Shift+digit KBD combos edge case in KeyboardList…
omargfh Oct 10, 2025
7da81f7
[#448] Add focus API to Code and Info tab editors
omargfh Oct 10, 2025
be59570
[#448] Refactor tabs into accessible TabWidget component
omargfh Oct 10, 2025
58d92b7
[#448] Update keybinds
omargfh Oct 10, 2025
6df5ec0
Editor: Update .vscode
omargfh Oct 10, 2025
32a0ca5
[#448] Add utility function to sort widgets by position in row-major …
omargfh Oct 10, 2025
6aff7ad
[#448] Refactor KeyboardListener for Mousetrap
omargfh Oct 13, 2025
926a243
[#448] Prevent pointer events on widgets when overlay is present
omargfh Oct 14, 2025
786f9e3
[#448] Rename platformCmdKey to modKey and change ctrl+t to alt+t
omargfh Oct 14, 2025
d687567
[#448] Add tabindex, aria-label, inheritable attrs
omargfh Oct 14, 2025
fef06e9
[#448] Set up events to update sorting keys on widgets
omargfh Oct 14, 2025
394a8ed
[#448] Lint: Nit types, if/else, returns, gaurds, template syntax, et…
omargfh Oct 21, 2025
d62e52a
[#448] Bugfix: Add keyboard listener for Enter/Space to Forever Button
omargfh Oct 29, 2025
f6bd6de
[#448] Minor: Add `id` field to RactiveCustomSlider
omargfh Oct 29, 2025
105e53d
[#448] Minor: Add Mod+I to switch input mode for slider widget
omargfh Oct 29, 2025
fce2878
[#448] Minor(refactor): Swap focus/toggle keybind shortcuts
omargfh Oct 29, 2025
50c351a
[#448] Minor: use canonical mousetrap spelling for escape key
omargfh Oct 29, 2025
351a309
[#448] Lint/Minor: Lint export keyword and add new helper to accessib…
omargfh Oct 29, 2025
12f9c91
[#448] Major: Add global on-activateClick.
omargfh Oct 29, 2025
4998447
[#448] Minor: Add per-component local config to Code Container
omargfh Oct 29, 2025
1a7da40
[#448] Minor: Pass tabindex to CodeEditor directly in input widgets
omargfh Oct 29, 2025
1765753
[#448] Minor(Bugfix): Documentation top-bar link is focusable and nav…
omargfh Oct 29, 2025
a07050f
[#448] Minor: Add global on-copy event.
omargfh Oct 29, 2025
f70feb5
[#448] Minor: Bind Ctrl+C to copy current value
omargfh Oct 29, 2025
2cbd460
[#448] Minor: Add Ctrl+0 to focus first widget in model
omargfh Oct 29, 2025
2c8499c
[#448] Minor(fix): Consolidate Help/KeyboardHelp into one modal
omargfh Oct 30, 2025
efac9c6
[#448] Minor(refactor): Store canonical reference to the tabindex val…
omargfh Oct 30, 2025
1ed53ec
[#448] Minor(ux): Focus edit form's first input on appearance
omargfh Oct 30, 2025
295edd3
[#448] Minor(feat): Context menu kbd navigable & tabindex order
omargfh Oct 30, 2025
73ecd1f
[#448] Minor(feature, refactor): Add KBD for adding widget and widget…
omargfh Oct 30, 2025
4694ae8
[#448] Minor(refactor): Create new accessibility events source file f…
omargfh Oct 30, 2025
f350913
[#448] Minor(feature, refactor) Add paste event to sliders; refactor …
omargfh Oct 30, 2025
62ce3fb
[#448] Minor(feature): Add KBD to nudge widgets a far distance
omargfh Oct 30, 2025
0a7024b
[#448] Minor(fix): Better implementation for selecting first widget
omargfh Oct 30, 2025
ae31195
[#448] Minor(a11y): Better ARIA.
omargfh Oct 30, 2025
00c9ee4
[#448] Forward keybinds from main window to iframe for keyhandling an…
omargfh Oct 31, 2025
2315ba6
[#448] Add help shortcut to main window
omargfh Oct 31, 2025
0364066
[#448] Minor(bugfix): Proper tab wrapping
omargfh Oct 31, 2025
319a980
[#448] Minor(a11y): Fix tabindex and refocus after compilation relate…
omargfh Oct 31, 2025
300ecc7
[#448] Minor(UX): better hint label for keyboard shortcuts
omargfh Dec 4, 2025
00633d0
[#448] Minor(UX): more prominent focus ring for widgets
omargfh Dec 4, 2025
04dac05
[#448] Minor(UX): override tab to use specialized positional + tabind…
omargfh Dec 4, 2025
3da3704
Feature: Add Toast notifications
omargfh Dec 4, 2025
5894750
[#448] Minor(UX): Change copy alert to toast
omargfh Dec 4, 2025
c21aa60
[#448] Fix(UX): Output values copy event uses internal variable
omargfh Dec 4, 2025
1f033b2
[#448] Fix(UX): Input variables copy internal value on Mod+C
omargfh Dec 4, 2025
e41d0b8
Lint: stylistic consistency for [#448]
omargfh Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"files.watcherExclude": {
"**/target": true
}
},

"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"editor.rulers": [80, 100, 120],
"editor.trimAutoWhitespace": true,
"editor.renderFinalNewline": "on",
"editor.renderWhitespace": "all",
}
62 changes: 62 additions & 0 deletions app/assets/javascripts/beak/widgets/accessibility/events.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

import { isMac, isToggleKeydownEvent } from "./utils.js"

# (HTMLElement, (Object) => Void) => Void
ractiveAccessibleClickEvent = (node, fire) ->
clickHandler = (event) ->
fire({ node: node, original: event })

keydownHandler = (event) ->
if isToggleKeydownEvent(event)
event.preventDefault()
fire({ node: node, original: event })

node.addEventListener('click', clickHandler, false)
node.addEventListener('keydown', keydownHandler, false)

return {
teardown: ->
node.removeEventListener('click', clickHandler, false)
node.removeEventListener('keydown', keydownHandler, false)
}

# (HTMLElement, (Object) => Void) => Void
ractiveCopyEvent = (node, fire) ->
keydownHandler = (event) ->
modKey = if isMac then event.metaKey else event.ctrlKey
copyKey = event.key is 'c'
matchKey = modKey and copyKey

if matchKey
fire({ node: node, original: event })

node.addEventListener('keydown', keydownHandler, false)

return {
teardown: ->
node.removeEventListener('keydown', keydownHandler, false)
}

# (HTMLElement, (Object) => Void) => Void
ractivePasteEvent = (node, fire) ->
keydownHandler = (event) ->
modKey = if isMac then event.metaKey else event.ctrlKey
pasteKey = event.key is 'v'
matchKey = modKey and pasteKey

if matchKey
fire({ node: node, original: event })

node.addEventListener('keydown', keydownHandler, false)

return {
teardown: ->
node.removeEventListener('keydown', keydownHandler, false)
}


export {
ractiveAccessibleClickEvent,
ractiveCopyEvent,
ractivePasteEvent
}
45 changes: 45 additions & 0 deletions app/assets/javascripts/beak/widgets/accessibility/key-combo.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isMac } from "./utils.js"

class KeyCombo
# (String) => Unit
constructor: (comboStr) ->
@comboStr = comboStr
[@key, parts...] = comboStr.toLowerCase().split('+').map((x) => x.trim()).reverse()
@modifiers = {
ctrl: parts.includes('ctrl') or parts.includes('control')
, alt: parts.includes('alt') or parts.includes('option')
, shift: parts.includes('shift')
, meta: parts.includes('meta') or parts.includes('cmd') or parts.includes('command') or parts.includes('mod')
}

# { [key: String]: String }
keyStringTable: {
up: "↑",
down: "↓",
left: "←",
right: "→",
escape: "Esc",
}

# () => Array[String]
getKeys: ->
modifierKeys = Object.entries(@modifiers).filter(([_, v]) => v).map(([mod, _]) =>
switch mod
when 'ctrl' then if isMac then 'Control' else 'Ctrl'
when 'alt' then if isMac then '⌥' else 'Alt'
when 'shift' then '⇧'
when 'meta' then if isMac then '⌘' else 'Meta'
)

mainKey = if Object.prototype.hasOwnProperty.call(@keyStringTable, @key)
@keyStringTable[@key]
else
@key[0].toUpperCase() + @key.slice(1).toLowerCase()

[...modifierKeys, mainKey]

# () => String
toString: ->
@getKeys().join(' + ')

export { KeyCombo }
68 changes: 68 additions & 0 deletions app/assets/javascripts/beak/widgets/accessibility/keybind.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { isMac } from "./utils.js"
import { KeyCombo } from "./key-combo.js"

# Keybind[mousetrap, ractive]
# mousetrap: Mousetrap
# ractive: Ractive
export class Keybind
id: undefined # String
cb: undefined # (ractive, KeyboardEvent, combo) => Boolean | Unit
combos: undefined # Array[KeyCombo]
metadata: {} # { description: String, docs: String, hidden?: Boolean }
options: {} # { type: "keydown" | "keyup" | "keypress", bind: Boolean, preventDefault: Boolean }

# parameters:
# id: String
# cb: (ractive, KeyboardEvent, combo) => Boolean | Unit
# comboStrs: Array[String] (see https://craig.is/killing/mice; excluding sequences)
# metadata: { description: String, docs: String } | undefined
# options: { type: "keydown" | "keyup" | "keypress", bind: Boolean, preventDefault: Boolean } | undefined
constructor: (@id, @cb, comboStrs, @metadata, options) ->
@options = { type: "keydown", bind: true, preventDefault: false, ...options }
@combos = comboStrs.map((comboStr) -> new KeyCombo(comboStr))
Object.defineProperty(this, 'comboStrs', {
get: => @combos.map((combo) -> combo.comboStr)
})

# (mousetrap, ractive, ((ractive) => Boolean) | undefined) => Unit
bind: (mousetrap, ractive, check) ->
if @options.bind is true
mousetrap.bind(@comboStrs, (e, combo) =>
if not check or check(ractive) and @cb?
if @options.preventDefault and e.preventDefault?
e.preventDefault()
return @cb(ractive, e, combo)
, @options.type)

# (mousetrap) => Unit
unbind: (mousetrap) ->
if @options.bind is true
mousetrap.unbind(@comboStrs, @options.type)

# KeybindGroup[mousetrap, ractive]
# mousetrap: Mousetrap
# ractive: Ractive
export class KeybindGroup
# name: String
# description: String | undefined
# conditions: Array[((ractive) => Boolean)]
# keybinds (keybinds): Array[Keybind]
constructor: (name, description, conditions, keybinds) ->
@name = name
@description = description
@conditions = conditions
@keybinds = keybinds

# ractive => Boolean
meetsConditions: (ractive) ->
return @conditions.every((condition) => condition(ractive))

# (mousetrap, ractive) => Unit
bind: (mousetrap, ractive) ->
@keybinds.forEach((keybind) =>
keybind.bind(mousetrap, ractive, @meetsConditions.bind(this))
)

# (mousetrap) => Unit
unbind: (mousetrap) ->
@keybinds.forEach((keybind) -> keybind.unbind(mousetrap))
Loading