@domicile-desktop/component-library 0.0.0-alpha-c079b224b5da → 0.0.0-alpha-7aa450be0ffd

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.
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "dependencies": {
3
3
  "@base-ui/react": "^1.8.0",
4
4
  "@cprussin/option-result": "^2.0.1",
5
- "@domicile-desktop/sdk": "0.0.0-alpha-c079b224b5da",
6
- "@domicile-desktop/system-apps": "0.0.0-alpha-c079b224b5da",
5
+ "@domicile-desktop/sdk": "0.0.0-alpha-7aa450be0ffd",
6
+ "@domicile-desktop/system-apps": "0.0.0-alpha-7aa450be0ffd",
7
7
  "@pandacss/dev": "^1.12.1",
8
8
  "@phosphor-icons/react": "^2.1.10"
9
9
  },
@@ -162,5 +162,5 @@
162
162
  "test:unit": "bun test"
163
163
  },
164
164
  "type": "module",
165
- "version": "0.0.0-alpha-c079b224b5da"
165
+ "version": "0.0.0-alpha-7aa450be0ffd"
166
166
  }
@@ -5,7 +5,7 @@ import { useId, useState } from "react";
5
5
  import { css } from "../../styled-system/css";
6
6
  import { flex, hstack } from "../../styled-system/patterns";
7
7
  import { Button } from "../Button/Button";
8
- import { keepInView, stepOf, steppedTo } from "../list-walk";
8
+ import { keepInView, pageOf, stepOf, steppedTo } from "../list-walk";
9
9
  import { ModalDialog } from "../ModalDialog/ModalDialog";
10
10
  import { preselected } from "./app-choices";
11
11
  import { useDescribedChoices } from "./useDescribedChoices";
@@ -124,7 +124,8 @@ export const AppChooser = ({
124
124
  aria-label="Applications"
125
125
  className={listStyles}
126
126
  onKeyDown={(event) => {
127
- const step = stepOf(event);
127
+ // The listbox is its own scroller.
128
+ const step = stepOf(event, pageOf(event.currentTarget));
128
129
  const next = apps[steppedTo(at, step ?? 0, apps.length)];
129
130
  if (event.key === "Enter") {
130
131
  event.preventDefault();
@@ -33,7 +33,7 @@ import { PencilSimpleIcon } from "@phosphor-icons/react/dist/ssr/PencilSimple";
33
33
  import { UsersIcon } from "@phosphor-icons/react/dist/ssr/Users";
34
34
  import { WarningIcon } from "@phosphor-icons/react/dist/ssr/Warning";
35
35
  import type { FocusEvent, KeyboardEvent, ReactNode, Ref } from "react";
36
- import { useId, useState } from "react";
36
+ import { useId, useRef, useState } from "react";
37
37
 
38
38
  import { css } from "../../styled-system/css";
39
39
  import { flex, grid, hstack, vstack } from "../../styled-system/patterns";
@@ -41,7 +41,13 @@ import { focusOnAttach } from "../_control/focusOnAttach";
41
41
  import { useStableRef } from "../_control/useStableRef";
42
42
  import { Button } from "../Button/Button";
43
43
  import { Input } from "../Input/Input";
44
- import { highlightIn, keepInView, stepOf, steppedTo } from "../list-walk";
44
+ import {
45
+ highlightIn,
46
+ keepInView,
47
+ pageOf,
48
+ stepOf,
49
+ steppedTo,
50
+ } from "../list-walk";
45
51
  import { Select } from "../Select/Select";
46
52
  import { Clash, clashOf } from "./clash";
47
53
  import { FileKind, fileKindOf, kindLabel } from "./file-kind";
@@ -114,6 +120,7 @@ type NewFolder = { failure: string | undefined; name: string };
114
120
  export const FilePicker = ({ ref, request }: Props) => {
115
121
  const titleId = useId();
116
122
  const listId = useId();
123
+ const results = useRef<HTMLDivElement>(null);
117
124
  const replaceTitleId = useId();
118
125
  const replaceNoteId = useId();
119
126
  const failureId = useId();
@@ -475,7 +482,7 @@ export const FilePicker = ({ ref, request }: Props) => {
475
482
  }}
476
483
  onFocus={saving ? selectStem : undefined}
477
484
  onKeyDown={(event) => {
478
- const step = stepOf(event);
485
+ const step = stepOf(event, pageOf(results.current));
479
486
  const up = parentOf(directory);
480
487
  if (step !== undefined) {
481
488
  event.preventDefault();
@@ -569,7 +576,7 @@ export const FilePicker = ({ ref, request }: Props) => {
569
576
  )}
570
577
  </div>
571
578
  )}
572
- <div className={resultsStyles}>
579
+ <div className={resultsStyles} ref={results}>
573
580
  <div aria-hidden className={columnsStyles}>
574
581
  <span>Name</span>
575
582
  <span>Kind</span>
package/src/list-walk.ts CHANGED
@@ -1,18 +1,37 @@
1
1
  // Keyboard movement of a highlight through a list, while focus stays in the
2
- // input above it. Used by the launcher and the file picker.
2
+ // input above it. Used by the launcher, the clipboard history and the
3
+ // pickers.
3
4
 
4
5
  /**
5
6
  * How far a key press moves the highlight, or `undefined` if it does not move
6
- * it. Handles the arrows and Emacs-style `ctrl+n` / `ctrl+p`.
7
+ * it. Handles the arrows, Emacs-style `ctrl+n` / `ctrl+p`, Page Up / Page Down
8
+ * by `page` rows, and Vim-style `ctrl+d` / `ctrl+u` by half that.
9
+ *
10
+ * `ctrl+u` takes the key from the box, which would otherwise clear the line.
7
11
  */
8
- export const stepOf = ({
9
- ctrlKey,
10
- key,
11
- }: {
12
- ctrlKey: boolean;
13
- key: string;
14
- }): number | undefined => {
12
+ export const stepOf = (
13
+ {
14
+ ctrlKey,
15
+ key,
16
+ }: {
17
+ ctrlKey: boolean;
18
+ key: string;
19
+ },
20
+ page: number,
21
+ ): number | undefined => {
15
22
  switch (key) {
23
+ case "PageDown": {
24
+ return page;
25
+ }
26
+ case "PageUp": {
27
+ return -page;
28
+ }
29
+ case "d": {
30
+ return ctrlKey ? halfOf(page) : undefined;
31
+ }
32
+ case "u": {
33
+ return ctrlKey ? -halfOf(page) : undefined;
34
+ }
16
35
  case "ArrowDown": {
17
36
  return 1;
18
37
  }
@@ -31,6 +50,30 @@ export const stepOf = ({
31
50
  }
32
51
  };
33
52
 
53
+ /** Half a page, rounded down, but at least one row so the key always moves. */
54
+ const halfOf = (page: number): number => Math.max(Math.floor(page / 2), 1);
55
+
56
+ /**
57
+ * How many rows Page Down moves in `scroller`: the rows that fit in it whole.
58
+ *
59
+ * At least one, so the keys still move a list that isn't laid out, or whose
60
+ * scroller is shorter than a row. Takes a ref's `current`, which is `null`
61
+ * only before the list mounts, when no key can reach it.
62
+ */
63
+ export const pageOf = (scroller: Scroller | null): number => {
64
+ if (scroller === null) {
65
+ throw new Error("Paged a list that is not mounted");
66
+ } else {
67
+ const row = scroller.querySelector('[role="option"]')?.offsetHeight ?? 0;
68
+ return row === 0 ? 1 : Math.max(Math.floor(scroller.clientHeight / row), 1);
69
+ }
70
+ };
71
+
72
+ /** The parts of a scrolling list element that `pageOf` measures. */
73
+ type Scroller = Pick<HTMLElement, "clientHeight"> & {
74
+ querySelector: (selector: string) => Pick<HTMLElement, "offsetHeight"> | null;
75
+ };
76
+
34
77
  /**
35
78
  * The highlighted row index, kept within the list, or none for an empty list.
36
79
  * With no key pressed it is the first row, so Enter picks it.