@franzzemen/brokenstock-shell 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1865,6 +1865,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
1865
1865
  args: [{ providedIn: 'root' }]
1866
1866
  }], ctorParameters: () => [] });
1867
1867
 
1868
+ /**
1869
+ * Elements that mean "the user is typing", where an unmodified shortcut must not fire.
1870
+ *
1871
+ * Kept as a set of tag names plus a `contenteditable` check rather than a
1872
+ * selector string, so it is greppable and so the reasoning stays attached to it.
1873
+ */
1874
+ const TEXT_ENTRY_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
1868
1875
  class KeyboardShortcutService {
1869
1876
  #groups;
1870
1877
  #activeGroupId;
@@ -1880,6 +1887,18 @@ class KeyboardShortcutService {
1880
1887
  this.#focusService = inject(TerminalFocusService);
1881
1888
  this.helpVisible = signal(false, /* @ts-ignore */
1882
1889
  ...(ngDevMode ? [{ debugName: "helpVisible" }] : /* istanbul ignore next */ []));
1890
+ /**
1891
+ * True while unmodified shortcuts are being suppressed — the user is typing,
1892
+ * or a dialog is open.
1893
+ *
1894
+ * Exposed because a screen binding BARE keys needs to tell the user whether the
1895
+ * next keystroke acts or types, and that indicator must read the service's own
1896
+ * dispatch state. A screen keeping its own boolean will eventually disagree
1897
+ * with the service, and an indicator that lies about whether a keystroke sells
1898
+ * a thousand shares is worse than no indicator at all.
1899
+ */
1900
+ this.suppressed = signal(false, /* @ts-ignore */
1901
+ ...(ngDevMode ? [{ debugName: "suppressed" }] : /* istanbul ignore next */ []));
1883
1902
  this.activeShortcuts = computed(() => {
1884
1903
  const groupId = this.#activeGroupId();
1885
1904
  if (!groupId)
@@ -1894,11 +1913,19 @@ class KeyboardShortcutService {
1894
1913
  this.#document.addEventListener('keydown', (event) => {
1895
1914
  this.#handleKeydown(event);
1896
1915
  });
1916
+ // Focus moves are what change `suppressed`, and they are frequent — kept
1917
+ // out of the zone, with the signal written directly. A signal write marks
1918
+ // its consumers dirty on its own; it does not need a change-detection pass
1919
+ // of its own to be scheduled here.
1920
+ const refresh = () => this.suppressed.set(this.#isSuppressed());
1921
+ this.#document.addEventListener('focusin', refresh, true);
1922
+ this.#document.addEventListener('focusout', refresh, true);
1897
1923
  });
1898
1924
  }
1899
1925
  registerGroup(groupId, shortcuts) {
1900
1926
  this.#groups.set(groupId, shortcuts);
1901
1927
  this.#activeGroupId.set(groupId);
1928
+ this.suppressed.set(this.#isSuppressed());
1902
1929
  }
1903
1930
  unregisterGroup(groupId) {
1904
1931
  this.#groups.delete(groupId);
@@ -1910,17 +1937,61 @@ class KeyboardShortcutService {
1910
1937
  toggleHelp() {
1911
1938
  this.helpVisible.update(v => !v);
1912
1939
  }
1940
+ /**
1941
+ * Would an unmodified shortcut be ignored right now?
1942
+ *
1943
+ * Two cases: a text-entry element has focus, or a dialog is open. Alt
1944
+ * combinations are exempt — they do not collide with typing, which is why
1945
+ * every binding in the app before this guard existed used one.
1946
+ */
1947
+ #isSuppressed() {
1948
+ const active = this.#document.activeElement;
1949
+ if (active) {
1950
+ if (TEXT_ENTRY_TAGS.has(active.tagName))
1951
+ return true;
1952
+ if (active.isContentEditable)
1953
+ return true;
1954
+ }
1955
+ // Angular Material puts every dialog in this container. A shortcut firing
1956
+ // behind an open dialog acts on a screen the user cannot currently see.
1957
+ return this.#document.querySelector('.cdk-overlay-container .cdk-overlay-pane') !== null;
1958
+ }
1959
+ #matches(shortcut, event) {
1960
+ // The physical key when `code` is given, the produced character otherwise.
1961
+ const hit = shortcut.code ? shortcut.code === event.code : shortcut.key === event.key;
1962
+ if (!hit)
1963
+ return false;
1964
+ if (shortcut.modifier === 'Alt')
1965
+ return event.altKey;
1966
+ if (shortcut.modifier === 'Shift') {
1967
+ return event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
1968
+ }
1969
+ // Unmodified. `ctrl`/`alt`/`meta` are excluded because the page still
1970
+ // receives a keystroke carrying the digit when the browser handles
1971
+ // `ctrl`+1 as a tab switch — without this, switching tabs would also fire
1972
+ // whatever a bare `1` is bound to.
1973
+ return !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
1974
+ }
1913
1975
  #handleKeydown(event) {
1914
1976
  const activeId = this.#activeGroupId();
1915
1977
  if (activeId) {
1916
1978
  const shortcuts = this.#groups.get(activeId);
1917
1979
  if (shortcuts) {
1918
- const match = shortcuts.find(s => {
1919
- if (s.modifier === 'Alt')
1920
- return event.altKey && s.key === event.key;
1921
- return !event.altKey && !event.ctrlKey && !event.metaKey && s.key === event.key;
1922
- });
1980
+ const match = shortcuts.find(s => this.#matches(s, event));
1923
1981
  if (match) {
1982
+ const alt = match.modifier === 'Alt';
1983
+ // Alt bindings are exempt from both guards below, so nothing that
1984
+ // existed before them changes behaviour: Alt does not collide with
1985
+ // typing, and holding Alt+ArrowDown to walk a list is wanted.
1986
+ if (!alt) {
1987
+ // A key held down repeats at roughly thirty a second. On a screen
1988
+ // where a bare key places an order, that is fifteen orders in half a
1989
+ // second, and a synchronous action path will run every one of them.
1990
+ if (event.repeat)
1991
+ return false;
1992
+ if (this.#isSuppressed())
1993
+ return false;
1994
+ }
1924
1995
  event.preventDefault();
1925
1996
  this.#zone.run(() => match.action());
1926
1997
  return true;