Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/_includes/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,17 @@

<div class="s-topbar--searchbar w100 wmx3 sm:wmx-initial js-search">
<div class="s-topbar--searchbar--input-group">
<input id="searchbox" type="text" placeholder="Search Stacks…" value="" autocomplete="off" class="s-input s-input__search" />
<input
id="searchbox"
type="text"
placeholder="Search Stacks…"
value=""
autocomplete="off"
class="s-input s-input__search"
data-controller="s-keyboard-shortcut"
data-s-keyboard-shortcut-ctrl-value="true"
data-s-keyboard-shortcut-key-value="/"
/>
{% icon "Search", "s-input-icon s-input-icon__search" %}
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion lib/ts/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// export all controllers *with helpers* so they can be bulk re-exported by the package entry point
export { ExpandableController } from './s-expandable-control';
export { KeyboardShortcutController } from './s-keyboard-shortcut';
export { hideModal, ModalController, showModal } from './s-modal';
export { TabListController } from './s-navigation-tablist';
export { attachPopover, detachPopover, hidePopover, BasePopoverController, PopoverController, showPopover } from './s-popover';
export { TableController } from './s-table';
export { setTooltipHtml, setTooltipText, TooltipController } from './s-tooltip';
export { UploaderController } from './s-uploader';
export { UploaderController } from './s-uploader';
117 changes: 117 additions & 0 deletions lib/ts/controllers/s-keyboard-shortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { StacksController } from "../stacks";
import { shallowEquals } from "../shared/utilities";

interface Shortcut {
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
meta?: boolean;
key: string;
}

type ClickableElement = HTMLAnchorElement | HTMLButtonElement | HTMLDetailsElement;
const clickableElements = ['a', 'button', 'details'];

type FocusableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
const focusableElements = ['input', 'select', 'textarea'];

export class KeyboardShortcutController extends StacksController {
declare ctrlValue: boolean;
declare shiftValue: boolean;
declare altValue: boolean;
declare metaValue: boolean;
declare keyValue: string;

static values = {
ctrl: Boolean,
meta: Boolean,
shift: Boolean,
alt: Boolean,
key: String,
};

private cachedShortcut: null | Shortcut = null;

get shortcut(): Shortcut {
if (this.cachedShortcut) {
return this.cachedShortcut;
}

return this.cachedShortcut = {
key: this.keyValue.toUpperCase(),
...(this.ctrlValue ? { ctrl: true } : {}),
...(this.metaValue ? { meta: true } : {}),
...(this.shiftValue ? { shift: true } : {}),
...(this.altValue ? { alt: true } : {}),
};
}

connect() {
window.addEventListener('keydown', this.handleKeyPress);
}

disconnect() {
window.removeEventListener('keydown', this.handleKeyPress);
}

//
// Rebuild our shortcut cache if our shortcut definition changes
//

ctrlValueChanged() {
this.cachedShortcut = null;
}

shiftValueChanged() {
this.cachedShortcut = null;
}

altValueChanged() {
this.cachedShortcut = null;
}

metaValueChanged() {
this.cachedShortcut = null;
}

keyValueChanged() {
this.cachedShortcut = null;
}

private handleKeyPress = (event: KeyboardEvent) => {
// If we're inside a text field, ignore any custom keyboard shortcuts
if (this.isInputInFocus()) {
return;
}

const keyPress = {
key: event.key.toUpperCase(),
...(event.ctrlKey ? { ctrl: true } : {}),
...(event.metaKey ? { meta: true } : {}),
...(event.shiftKey ? { shift: true } : {}),
...(event.altKey ? { alt: true } : {}),
};

if (shallowEquals(this.shortcut, keyPress)) {
event.preventDefault();

const tag = this.element.tagName.toLowerCase();

if (clickableElements.indexOf(tag) >= 0) {
(this.element as ClickableElement).click();
} else if (focusableElements.indexOf(tag) >= 0) {
(this.element as FocusableElement).focus();
}
}
};

private isInputInFocus = (): boolean => {
const nodeName = document.activeElement?.nodeName.toLowerCase();

if (!nodeName) {
return false;
}

return ['input', 'textarea', 'select'].includes(nodeName);
}
}
12 changes: 11 additions & 1 deletion lib/ts/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import '../css/stacks.less';
import { ExpandableController, ModalController, PopoverController, TableController, TabListController, TooltipController, UploaderController } from './controllers';
import {
ExpandableController,
KeyboardShortcutController,
ModalController,
PopoverController,
TableController,
TabListController,
TooltipController,
UploaderController
} from './controllers';
import { application, StacksApplication } from './stacks';

// register all built-in controllers
application.register("s-expandable-control", ExpandableController);
application.register("s-keyboard-shortcut", KeyboardShortcutController);
application.register("s-modal", ModalController);
application.register("s-navigation-tablist", TabListController);
application.register("s-popover", PopoverController);
Expand Down
8 changes: 8 additions & 0 deletions lib/ts/shared/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type Indexable = Record<string | number | symbol, any>;

export function shallowEquals(obj1: Indexable, obj2: Indexable) {
return (
Object.keys(obj1).length === Object.keys(obj2).length &&
Object.keys(obj1).every(key => obj1[key] === obj2[key])
);
}