@olenbetong/appframe-ds 1.9.0 → 1.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 1.9.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 2f1516a: AfGrid and the lookup grid take their row height from `--ds-size-11` instead of `--ds-size-10`, so the text no longer crowds the row (about 34px at sm, 28px at xs). AfGrid's text is one step below the size mode's body text, as MUI's compact grid was, except at xs, and inputs in the grid follow it.
8
+
9
+ ## 1.9.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 0f57479: Lookups: the dialog lines up with the input instead of the label beside or above it, grows upwards when it is placed above the field, and follows the field's size mode also when opened with ArrowDown or F4. `GridEditLookup` takes `editable`, so a value can be typed into the cell.
14
+
3
15
  ## 1.9.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
@@ -1,27 +1,27 @@
1
- /*
2
- * tsc emits the JavaScript into es/ but not the stylesheets the components import next to
3
- * themselves. Copy every .css under src/ to the same path under es/, so a new component's
4
- * stylesheet is published without anyone remembering a list.
5
- */
6
- import { cpSync, mkdirSync, readdirSync } from "node:fs";
7
- import { dirname, join, relative } from "node:path";
8
-
9
- function* cssFiles(dir) {
10
- for (let entry of readdirSync(dir, { withFileTypes: true })) {
11
- let path = join(dir, entry.name);
12
- if (entry.isDirectory()) {
13
- yield* cssFiles(path);
14
- } else if (entry.name.endsWith(".css")) {
15
- yield path;
16
- }
17
- }
18
- }
19
-
20
- let count = 0;
21
- for (let src of cssFiles("src")) {
22
- let dest = join("es", relative("src", src));
23
- mkdirSync(dirname(dest), { recursive: true });
24
- cpSync(src, dest);
25
- count++;
26
- }
27
- console.log(`Copied ${count} stylesheets from src/ to es/`);
1
+ /*
2
+ * tsc emits the JavaScript into es/ but not the stylesheets the components import next to
3
+ * themselves. Copy every .css under src/ to the same path under es/, so a new component's
4
+ * stylesheet is published without anyone remembering a list.
5
+ */
6
+ import { cpSync, mkdirSync, readdirSync } from "node:fs";
7
+ import { dirname, join, relative } from "node:path";
8
+
9
+ function* cssFiles(dir) {
10
+ for (let entry of readdirSync(dir, { withFileTypes: true })) {
11
+ let path = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ yield* cssFiles(path);
14
+ } else if (entry.name.endsWith(".css")) {
15
+ yield path;
16
+ }
17
+ }
18
+ }
19
+
20
+ let count = 0;
21
+ for (let src of cssFiles("src")) {
22
+ let dest = join("es", relative("src", src));
23
+ mkdirSync(dirname(dest), { recursive: true });
24
+ cpSync(src, dest);
25
+ count++;
26
+ }
27
+ console.log(`Copied ${count} stylesheets from src/ to es/`);
@@ -3,6 +3,8 @@
3
3
 
4
4
  .ds-input {
5
5
  padding-inline-end: var(--ObLookup-buttons-width, var(--ds-size-12, 3rem));
6
+ /* Set by AfLookup and AfLookupEdit; the dialog lines up with the input, not its label. */
7
+ anchor-name: var(--ObLookup-anchor);
6
8
  }
7
9
  }
8
10
 
@@ -144,6 +146,11 @@
144
146
  }
145
147
  }
146
148
 
149
+ /* Placed above the field (see useLookup): grow upwards from it. */
150
+ &[data-above] {
151
+ transform-origin: bottom left;
152
+ }
153
+
147
154
  &[data-anchor="manual"] {
148
155
  inset: unset;
149
156
  position: fixed;
@@ -78,12 +78,38 @@ export function useLookup<T extends Record<string, unknown>>({
78
78
  // mounts with the search value already set.
79
79
  setInitialSearchValue(searchValue);
80
80
  openDialog();
81
+ setGrowDirection();
82
+ // A manually anchored dialog (in a virtualized grid) is placed in the next frame.
83
+ requestAnimationFrame(setGrowDirection);
84
+ }
85
+
86
+ // When there is no room below the field, the dialog is placed above it; it then grows upwards
87
+ // from the field instead of down towards it. Its bottom edge tells, whatever the opening scale.
88
+ function setGrowDirection() {
89
+ let $dialog = dialogRef.current;
90
+ let anchor = document.getElementById(inputId) ?? getAnchorElement?.();
91
+ if (!$dialog || !anchor) return;
92
+
93
+ let above = $dialog.getBoundingClientRect().bottom <= anchor.getBoundingClientRect().top + 1;
94
+ $dialog.toggleAttribute("data-above", above);
95
+ }
96
+
97
+ // useLookupState opens the dialog on ArrowDown and F4 itself, which would skip the size mode and
98
+ // the grow direction above; every way of opening it goes through handleOpenDialog instead.
99
+ function handleKeyDown(event: React.KeyboardEvent<HTMLElement>) {
100
+ if (event.key === "ArrowDown" || event.key === "F4") {
101
+ event.preventDefault();
102
+ event.stopPropagation();
103
+ handleOpenDialog();
104
+ return;
105
+ }
106
+ onKeyDown(event);
81
107
  }
82
108
 
83
109
  return {
84
110
  open,
85
111
  openDialog: handleOpenDialog,
86
- onKeyDown,
112
+ onKeyDown: handleKeyDown,
87
113
  onChange: handleChange,
88
114
  dialogRef,
89
115
  anchorName,
@@ -135,7 +161,8 @@ export function AfLookup<T extends Record<string, unknown>>({
135
161
  }
136
162
 
137
163
  return (
138
- <div style={{ anchorName } as React.CSSProperties}>
164
+ // The dialog anchors to the input itself (see Lookup.css), not to the label beside or above it.
165
+ <div style={{ "--ObLookup-anchor": anchorName } as React.CSSProperties}>
139
166
  <div
140
167
  className="ObLookup-inputWrap"
141
168
  style={
@@ -73,7 +73,8 @@ export function AfLookupEdit<T extends Record<string, unknown>>({
73
73
  let hasClear = !!(nullable && value && onChange);
74
74
 
75
75
  return (
76
- <div style={{ anchorName } as React.CSSProperties}>
76
+ // The dialog anchors to the input itself (see Lookup.css), not to the label beside or above it.
77
+ <div style={{ "--ObLookup-anchor": anchorName } as React.CSSProperties}>
77
78
  <div
78
79
  className="ObLookup-inputWrap"
79
80
  style={
@@ -9,7 +9,7 @@ import { List, type RowComponentProps, useListRef } from "react-window";
9
9
  import { getLocalizedString } from "../i18n.js";
10
10
  import { useDsSize } from "../theme/sizeMode.js";
11
11
 
12
- /** The row and header height until `--ds-size-10` is measured, which follows the size mode like AfGrid's rows. */
12
+ /** The row and header height until `--ds-size-11` is measured, which follows the size mode like AfGrid's rows. */
13
13
  const ROW_HEIGHT = 40;
14
14
  /** Narrowest a column can be made, and what a flexible column counts for in the minimum width */
15
15
  const MIN_COLUMN_WIDTH = 80;
@@ -192,7 +192,7 @@ export function LookupGrid<T extends Record<string, unknown>>({
192
192
  let listRef = useListRef(null);
193
193
  let headerRowRef = useRef<HTMLDivElement>(null);
194
194
  let rootRef = useRef<HTMLDivElement>(null);
195
- let rowHeight = useDsSize(rootRef, 10, ROW_HEIGHT);
195
+ let rowHeight = useDsSize(rootRef, 11, ROW_HEIGHT);
196
196
  let [resizedWidths, setResizedWidths] = useState<Partial<Record<string, number>>>({});
197
197
  let [resizing, setResizing] = useState<{ field: string; startWidth: number } | null>(null);
198
198
  let [listInnerWidth, setListInnerWidth] = useState<number>();
@@ -22,7 +22,8 @@ export type GridEditLookupProps<
22
22
 
23
23
  /**
24
24
  * A lookup as a grid cell editor: a read-only input with a search button that opens the
25
- * lookup dialog anchored to the cell, and a clear button when the field is nullable. Give it
25
+ * lookup dialog anchored to the cell, and a clear button when the field is nullable. With
26
+ * `editable` the value can be typed into the cell as well, such as a username. Give it
26
27
  * to a column's `renderEditCell`. Equivalent of `GridEditLookup` in `@olenbetong/appframe-mui`.
27
28
  */
28
29
  export function GridEditLookup<
@@ -77,7 +78,7 @@ export function GridEditLookup<
77
78
  slotProps={{
78
79
  root: {
79
80
  // @ts-expect-error: typed as HTMLElement, but it is the input
80
- readOnly: true,
81
+ readOnly: !props.editable,
81
82
  },
82
83
  }}
83
84
  onKeyDown={onKeyDown}
@@ -4,11 +4,33 @@
4
4
 
5
5
  /*
6
6
  * The grid follows the Designsystemet size mode it sits in instead of MUI's density and type
7
- * scale: its text is the size mode's body text, like the inputs around it, and its row and header
8
- * heights come from the same mode (`useAfSizeMode`).
7
+ * scale, and its row and header heights come from the same mode (`useAfSizeMode`). Its text is
8
+ * one step below the mode's body text - at sm that is MUI's compact grid text - except at xs,
9
+ * where the body text is already that small. The size is picked on the element that sets the
10
+ * mode, so the closest one wins; the components that use `data-size` for a variant of their own
11
+ * are left out, as in `getSizeMode`. Without a size mode the grid inherits the page's text.
9
12
  */
13
+ [data-size]:not(.ds-heading, .ds-paragraph, .ds-spinner, .ds-avatar) {
14
+ --AfGrid-font-size: var(--ds-body-sm-font-size);
15
+ }
16
+
17
+ [data-size="xs"]:not(.ds-heading, .ds-paragraph, .ds-spinner, .ds-avatar) {
18
+ --AfGrid-font-size: var(--ds-body-md-font-size);
19
+ }
20
+
10
21
  .AfGrid-root .MuiDataGrid-root {
11
- font-size: inherit;
22
+ font-size: var(--AfGrid-font-size, inherit);
23
+ }
24
+
25
+ /*
26
+ * Inputs in the grid - cell editors, header filters - take the grid's text size rather than the
27
+ * mode's body text. Only where there is a mouse, so a touch device keeps Designsystemet's floor
28
+ * that stops iOS zooming in on a focused field (see the site's xs rule).
29
+ */
30
+ @media (hover: hover) and (pointer: fine) {
31
+ .AfGrid-root .MuiDataGrid-root .ds-input {
32
+ --dsc-input-font-size: 1em;
33
+ }
12
34
  }
13
35
 
14
36
  /*
@@ -30,13 +52,13 @@
30
52
 
31
53
  /*
32
54
  * Controls in a data cell - editors, lookup buttons, the row's action buttons - fit the row, which
33
- * is `--ds-size-10` tall, rather than taking Designsystemet's `--ds-size-12` control height, which
55
+ * is `--ds-size-11` tall, rather than taking Designsystemet's `--ds-size-12` control height, which
34
56
  * would overflow it at every size. The custom properties are set on the components themselves,
35
57
  * so they are overridden there and not on the cell.
36
58
  */
37
59
  .AfGrid-root .MuiDataGrid-cell {
38
60
  .ds-input {
39
- --dsc-input-size: var(--ds-size-10, 2.25rem);
61
+ --dsc-input-size: var(--ds-size-11, 2.25rem);
40
62
  --dsc-input-padding-block: 0;
41
63
  }
42
64
 
@@ -7,13 +7,14 @@ const FALLBACK_HEIGHT = 36;
7
7
 
8
8
  /**
9
9
  * Row and header heights from the Designsystemet size mode the grid sits in, in place of MUI's
10
- * density: `--ds-size-10`, which is 25px at xs, 31px at sm, 35px at md and 41px at lg with the
11
- * site's 14px root. The grid renders at the standard density, so these are the heights as they
12
- * are. The text, toolbar and footer follow the same size mode through the stylesheet and by
10
+ * density: `--ds-size-11`, which is about 28px at xs, 34px at sm, 39px at md and 45px at lg with
11
+ * the site's 14px root - close to Designsystemet's own table at each size. `--ds-size-10` left the
12
+ * text crowded in its row. The grid renders at the standard density, so these are the heights as
13
+ * they are. The text, toolbar and footer follow the same size mode through the stylesheet and by
13
14
  * inheriting `data-size`.
14
15
  */
15
16
  export function useAfSizeMode(rootRef: RefObject<HTMLElement | null>) {
16
- let height = useDsSize(rootRef, 10, FALLBACK_HEIGHT);
17
+ let height = useDsSize(rootRef, 11, FALLBACK_HEIGHT);
17
18
 
18
19
  return { rowHeight: height, columnHeaderHeight: height };
19
20
  }
@@ -1,9 +1,9 @@
1
- {
2
- "extends": "../tsconfig.build.base.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./es"
6
- },
7
- "include": ["src"],
8
- "exclude": ["es", "node_modules"]
9
- }
1
+ {
2
+ "extends": "../tsconfig.build.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./es"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["es", "node_modules"]
9
+ }
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
- {
2
- "extends": "../tsconfig.build.base.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./es"
6
- },
7
- "include": ["src"]
8
- }
1
+ {
2
+ "extends": "../tsconfig.build.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./es"
6
+ },
7
+ "include": ["src"]
8
+ }
package/vitest.config.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
6
- environment: "happy-dom",
7
- setupFiles: ["src/test/setup.ts"],
8
- env: { NODE_ENV: "test" },
9
- },
10
- });
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
6
+ environment: "happy-dom",
7
+ setupFiles: ["src/test/setup.ts"],
8
+ env: { NODE_ENV: "test" },
9
+ },
10
+ });