@olenbetong/appframe-ds 0.6.0 → 0.7.0

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,15 @@
1
1
  # @olenbetong/appframe-ds
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 52e2e10: Add `Autocomplete` and `Combobox`, built on the Base UI components of the same name and styled with Designsystemet tokens. Both expose an API close to MUI's `Autocomplete` (`options`, `value`/`onChange`, `inputValue`/`onInputChange`, `getOptionLabel`, `isOptionEqualToValue`, `filterOptions`, `renderOption`, `groupBy`, `multiple`), plus Designsystemet field props (`label`, `description`, `error`, `data-size`). Use `Combobox` when the user must pick one of the options and `Autocomplete` when free text is allowed (`freeSolo`). Multiple selection renders the selected options as chips above the input, like Designsystemet's `Suggestion`. `createFilterOptions` is exported for custom filtering.
8
+
9
+ **Breaking:** the data object bound `Combobox` used internally by `AfLookup` is renamed to `LookupCombobox` (`ComboboxProps` → `LookupComboboxProps`) to free up the `Combobox` name.
10
+
11
+ - 77e96aa: Add `SortableItemList`, an `ItemList` variant bound to a data object where items can be reordered by dragging or through a menu on the drag handle. The building blocks are exported as well (`useSortableList`, `useSortableItem`, `useSortableContainer`, `DropIndicator`, `reorderSortOrder`, `getNextSortOrder`, `ensureNonZeroSortOrder`) for lists that need custom item markup.
12
+
3
13
  ## 0.6.0
4
14
 
5
15
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/appframe-ds",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Components that bind Designsystemet components to Appframe data objects",
5
5
  "type": "module",
6
6
  "types": "./es/index.d.ts",
@@ -23,18 +23,21 @@
23
23
  "react-router": ">=8.3.0"
24
24
  },
25
25
  "dependencies": {
26
+ "@atlaskit/pragmatic-drag-and-drop": "2.0.1",
27
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "2.0.0",
28
+ "@base-ui/react": "1.7.0",
26
29
  "@olenbetong/appframe-core": "2.11.10",
27
30
  "@olenbetong/appframe-data": "1.5.0",
28
31
  "@olenbetong/appframe-react": "2.0.0",
29
- "react-window": "^1.8.11",
32
+ "clsx": "^2.1.1",
30
33
  "react-virtualized-auto-sizer": "^1.0.26",
31
- "clsx": "^2.1.1"
34
+ "react-window": "^1.8.11"
32
35
  },
33
36
  "devDependencies": {
34
37
  "@digdir/designsystemet-react": "1.18.0",
35
38
  "@navikt/aksel-icons": "8.16.1",
36
- "@types/react": "19.2.17",
37
- "@types/react-dom": "19.2.3",
39
+ "@types/react": "19.2.18",
40
+ "@types/react-dom": "19.2.4",
38
41
  "@types/react-window": "^1.8.8",
39
42
  "react": "19.2.8",
40
43
  "react-dom": "19.2.8",
@@ -42,7 +45,6 @@
42
45
  "react-router": "8.3.0",
43
46
  "typescript": "7.0.2"
44
47
  },
45
- "optionalDependencies": {},
46
48
  "scripts": {
47
49
  "build": "pnpm run build:tsc && pnpm run build:css",
48
50
  "build:tsc": "node --no-warnings ../../scripts/clean.ts ./es tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
@@ -3,7 +3,7 @@ import { cpSync, mkdirSync } from "node:fs";
3
3
  const files = [
4
4
  ["src/AfLookup/Lookup.css", "es/AfLookup/Lookup.css"],
5
5
  ["src/AfLookup/LookupGrid.css", "es/AfLookup/LookupGrid.css"],
6
- ["src/AfLookup/Combobox.css", "es/AfLookup/Combobox.css"],
6
+ ["src/AfLookup/LookupCombobox.css", "es/AfLookup/LookupCombobox.css"],
7
7
  ["src/layout/AppMenu.css", "es/layout/AppMenu.css"],
8
8
  ["src/layout/AppMenuItem.css", "es/layout/AppMenuItem.css"],
9
9
  ["src/layout/AppMenuSubheader.css", "es/layout/AppMenuSubheader.css"],
@@ -19,6 +19,9 @@ const files = [
19
19
  ["src/input/InputAdornments.css", "es/input/InputAdornments.css"],
20
20
  ["src/paper/Paper.css", "es/paper/Paper.css"],
21
21
  ["src/progress/LinearProgress.css", "es/progress/LinearProgress.css"],
22
+ ["src/sortable/DropIndicator.css", "es/sortable/DropIndicator.css"],
23
+ ["src/sortable/SortableItemList.css", "es/sortable/SortableItemList.css"],
24
+ ["src/autocomplete/Autocomplete.css", "es/autocomplete/Autocomplete.css"],
22
25
  ];
23
26
 
24
27
  mkdirSync("es/layout", { recursive: true });
@@ -27,6 +30,8 @@ mkdirSync("es/list", { recursive: true });
27
30
  mkdirSync("es/page", { recursive: true });
28
31
  mkdirSync("es/paper", { recursive: true });
29
32
  mkdirSync("es/progress", { recursive: true });
33
+ mkdirSync("es/sortable", { recursive: true });
34
+ mkdirSync("es/autocomplete", { recursive: true });
30
35
 
31
36
  for (let [src, dest] of files) {
32
37
  cpSync(src, dest);
@@ -9,9 +9,9 @@ import clsx from "clsx";
9
9
  import { useRef } from "react";
10
10
  import { createPortal } from "react-dom";
11
11
 
12
- import { Combobox, type ComboboxProps } from "./Combobox.js";
12
+ import { LookupCombobox, type LookupComboboxProps } from "./LookupCombobox.js";
13
13
 
14
- export type AfLookupProps<T extends Record<string, unknown>> = DistributiveOmit<ComboboxProps<T>, "value"> & {
14
+ export type AfLookupProps<T extends Record<string, unknown>> = DistributiveOmit<LookupComboboxProps<T>, "value"> & {
15
15
  /**
16
16
  * Allows the user to enter a value manually that might not be in the list.
17
17
  */
@@ -26,7 +26,7 @@ export type AfLookupProps<T extends Record<string, unknown>> = DistributiveOmit<
26
26
  };
27
27
  };
28
28
 
29
- export type LookupProps<T extends Record<string, unknown>> = DistributiveOmit<ComboboxProps<T>, "value"> & {
29
+ export type LookupProps<T extends Record<string, unknown>> = DistributiveOmit<LookupComboboxProps<T>, "value"> & {
30
30
  slotProps?: {
31
31
  dialog?: Partial<React.HTMLProps<HTMLDialogElement>>;
32
32
  };
@@ -70,7 +70,7 @@ export function useLookup<T extends Record<string, unknown>>({
70
70
  style={{ positionAnchor: anchorName, ...slotProps?.dialog?.style } as React.CSSProperties}
71
71
  >
72
72
  {open && (
73
- <Combobox
73
+ <LookupCombobox
74
74
  {...props}
75
75
  initialSearchValue={initialSearchValueRef.current}
76
76
  onClose={() => dialogRef.current?.close()}
@@ -1,4 +1,4 @@
1
- import "./Combobox.css";
1
+ import "./LookupCombobox.css";
2
2
 
3
3
  import { Button, Textfield } from "@digdir/designsystemet-react";
4
4
  import { getLocalizedString } from "@olenbetong/appframe-core";
@@ -11,7 +11,7 @@ import { VariableSizeList as VirtualList } from "react-window";
11
11
 
12
12
  import { LookupGrid } from "./LookupGrid.js";
13
13
 
14
- export interface ComboboxBaseProps<T extends Record<string, unknown>> {
14
+ export interface LookupComboboxBaseProps<T extends Record<string, unknown>> {
15
15
  nullable?: boolean;
16
16
  dataObject: DataObject<T>;
17
17
  uniqueIdField?: string;
@@ -22,7 +22,7 @@ export interface ComboboxBaseProps<T extends Record<string, unknown>> {
22
22
  onClose?: () => void;
23
23
  }
24
24
 
25
- export interface ComboboxListProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
25
+ export interface LookupComboboxListProps<T extends Record<string, unknown>> extends LookupComboboxBaseProps<T> {
26
26
  /**
27
27
  * Renders the popup content as a list (default).
28
28
  */
@@ -30,7 +30,7 @@ export interface ComboboxListProps<T extends Record<string, unknown>> extends Co
30
30
  renderItem: (props: React.ComponentPropsWithRef<"li"> & { item: T }) => React.ReactNode;
31
31
  }
32
32
 
33
- export interface ComboboxGridProps<T extends Record<string, unknown>> extends ComboboxBaseProps<T> {
33
+ export interface LookupComboboxGridProps<T extends Record<string, unknown>> extends LookupComboboxBaseProps<T> {
34
34
  /**
35
35
  * Renders the popup content as a grid with a header row and one column per
36
36
  * entry in `columns`.
@@ -39,9 +39,9 @@ export interface ComboboxGridProps<T extends Record<string, unknown>> extends Co
39
39
  columns: LookupColumn<T>[];
40
40
  }
41
41
 
42
- export type ComboboxProps<T extends Record<string, unknown>> = ComboboxListProps<T> | ComboboxGridProps<T>;
42
+ export type LookupComboboxProps<T extends Record<string, unknown>> = LookupComboboxListProps<T> | LookupComboboxGridProps<T>;
43
43
 
44
- export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps<T>) {
44
+ export function LookupCombobox<T extends Record<string, unknown>>(props: LookupComboboxProps<T>) {
45
45
  let {
46
46
  nullable,
47
47
  dataObject,
@@ -52,7 +52,7 @@ export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps
52
52
  onClose,
53
53
  } = props;
54
54
  let isGrid = props.variant === "grid";
55
- let columns = isGrid ? (props as ComboboxGridProps<T>).columns : undefined;
55
+ let columns = isGrid ? (props as LookupComboboxGridProps<T>).columns : undefined;
56
56
  let listRef = useRef<VirtualList>(null);
57
57
 
58
58
  let [isDesktop, setIsDesktop] = useState(
@@ -121,7 +121,7 @@ export function Combobox<T extends Record<string, unknown>>(props: ComboboxProps
121
121
  let item = data[index];
122
122
  let itemId = getItemId(item);
123
123
  let active = index === currentIndex;
124
- let ItemComponent = (props as ComboboxListProps<T>).renderItem;
124
+ let ItemComponent = (props as LookupComboboxListProps<T>).renderItem;
125
125
 
126
126
  return (
127
127
  <ItemComponent
@@ -8,11 +8,11 @@ import type { DistributiveOmit } from "@olenbetong/appframe-react";
8
8
  import clsx from "clsx";
9
9
  import type React from "react";
10
10
 
11
- import type { ComboboxProps } from "./Combobox.js";
11
+ import type { LookupComboboxProps } from "./LookupCombobox.js";
12
12
  import { useLookup } from "./Lookup.js";
13
13
 
14
14
  export type AfLookupEditProps<T extends Record<string, unknown>> = DistributiveOmit<
15
- ComboboxProps<T>,
15
+ LookupComboboxProps<T>,
16
16
  "value" | "onChange"
17
17
  > & {
18
18
  /**
@@ -1,5 +1,5 @@
1
1
  export * from "./BoundLookup.js";
2
- export * from "./Combobox.js";
2
+ export * from "./LookupCombobox.js";
3
3
  export * from "./Lookup.js";
4
4
  export * from "./LookupEdit.js";
5
5
  export * from "./LookupGrid.js";
@@ -0,0 +1,222 @@
1
+ .ObAutocomplete-root {
2
+ --obAutocomplete-padding: 0.25rem;
3
+ --obAutocomplete-gap: 0.25rem;
4
+ --obAutocomplete-fontSize: var(--ds-font-size-3, 1rem);
5
+
6
+ display: inline-grid;
7
+ }
8
+
9
+ .ObAutocomplete-root[data-full-width="true"] {
10
+ display: grid;
11
+ inline-size: 100%;
12
+ }
13
+
14
+ .ObAutocomplete-root[data-size="xs"] {
15
+ --obAutocomplete-padding: 0.125rem;
16
+ --obAutocomplete-fontSize: var(--ds-font-size-1, 0.75rem);
17
+ }
18
+
19
+ .ObAutocomplete-root[data-size="sm"] {
20
+ --obAutocomplete-padding: 0.25rem;
21
+ --obAutocomplete-fontSize: var(--ds-font-size-2, 0.875rem);
22
+ }
23
+
24
+ .ObAutocomplete-root[data-size="md"] {
25
+ --obAutocomplete-padding: 0.25rem;
26
+ --obAutocomplete-fontSize: var(--ds-font-size-3, 1rem);
27
+ }
28
+
29
+ .ObAutocomplete-root[data-size="lg"] {
30
+ --obAutocomplete-padding: 0.375rem;
31
+ --obAutocomplete-fontSize: var(--ds-font-size-4, 1.125rem);
32
+ }
33
+
34
+ /*
35
+ * The control is the visual input: it owns the border, background, hover and
36
+ * focus styling, so the chips, the bare input and the buttons read as a single
37
+ * control. Mirrors how Designsystemet's Suggestion renders selected chips
38
+ * above the input.
39
+ */
40
+ .ObAutocomplete-control {
41
+ display: flex;
42
+ flex-direction: column;
43
+ box-sizing: border-box;
44
+ gap: var(--obAutocomplete-gap);
45
+ padding: var(--obAutocomplete-padding);
46
+ inline-size: 100%;
47
+ font-size: var(--obAutocomplete-fontSize);
48
+ color: var(--ds-color-neutral-text-default, #1e2b3c);
49
+ background-color: var(--ds-color-neutral-surface-default, #fff);
50
+ border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-default, #b3b3b3);
51
+ border-radius: var(--ds-border-radius-md, 0.375rem);
52
+ }
53
+
54
+ .ObAutocomplete-control:hover:not(:focus-within) {
55
+ border-color: var(--ds-color-neutral-border-strong, #767676);
56
+ }
57
+
58
+ .ObAutocomplete-control:has(.ObAutocomplete-input:focus-visible) {
59
+ box-shadow: var(--dsc-focus-boxShadow);
60
+ outline: var(--dsc-focus-outline);
61
+ outline-offset: var(--ds-border-width-focus, 3px);
62
+ }
63
+
64
+ .ObAutocomplete-control[data-invalid="true"] {
65
+ border-color: var(--ds-color-danger-border-default, #b3253a);
66
+ }
67
+
68
+ .ObAutocomplete-control[data-disabled="true"] {
69
+ cursor: not-allowed;
70
+ opacity: var(--ds-disabled-opacity, 0.3);
71
+ }
72
+
73
+ .ObAutocomplete-chips {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ gap: var(--obAutocomplete-gap);
77
+ padding: var(--obAutocomplete-padding);
78
+ padding-block-end: 0;
79
+ }
80
+
81
+ .ObAutocomplete-inputGroup {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: var(--obAutocomplete-gap);
85
+ min-inline-size: 0;
86
+ }
87
+
88
+ .ObAutocomplete-input {
89
+ flex: 1;
90
+ min-inline-size: 0;
91
+ padding: var(--ds-size-1, 0.25rem);
92
+ font: inherit;
93
+ color: inherit;
94
+ background: transparent;
95
+ border: none;
96
+ outline: none;
97
+ }
98
+
99
+ .ObAutocomplete-input:disabled {
100
+ cursor: not-allowed;
101
+ }
102
+
103
+ .ObAutocomplete-button {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ flex: none;
108
+ padding: var(--ds-size-1, 0.25rem);
109
+ font-size: var(--obAutocomplete-fontSize);
110
+ color: inherit;
111
+ background: transparent;
112
+ border: none;
113
+ border-radius: var(--ds-border-radius-sm, 0.25rem);
114
+ cursor: pointer;
115
+ }
116
+
117
+ .ObAutocomplete-button:hover:not(:disabled) {
118
+ background-color: var(--ds-color-neutral-surface-hover, #e6e6e6);
119
+ }
120
+
121
+ .ObAutocomplete-button:disabled {
122
+ cursor: not-allowed;
123
+ }
124
+
125
+ .ObAutocomplete-button[data-visible="false"] {
126
+ display: none;
127
+ }
128
+
129
+ .ObAutocomplete-trigger[data-popup-open] svg {
130
+ rotate: 180deg;
131
+ }
132
+
133
+ .ObAutocomplete-spinner {
134
+ flex: none;
135
+ display: inline-flex;
136
+ align-items: center;
137
+ padding-inline: var(--ds-size-1, 0.25rem);
138
+ }
139
+
140
+ .ObAutocomplete-positioner {
141
+ z-index: 1500;
142
+ /* Base UI renders the positioner even while closed */
143
+ outline: none;
144
+ }
145
+
146
+ .ObAutocomplete-popup {
147
+ box-sizing: border-box;
148
+ inline-size: var(--anchor-width);
149
+ max-block-size: min(24rem, var(--available-height));
150
+ overflow-y: auto;
151
+ overscroll-behavior: contain;
152
+ padding: var(--ds-size-1, 0.25rem);
153
+ font-size: var(--obAutocomplete-fontSize, var(--ds-font-size-3, 1rem));
154
+ color: var(--ds-color-neutral-text-default, #1e2b3c);
155
+ background-color: var(--ds-color-neutral-background-default, #fff);
156
+ border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #e0e0e0);
157
+ border-radius: var(--ds-border-radius-md, 0.375rem);
158
+ box-shadow: var(--ds-shadow-md, 0 2px 8px 0 rgb(0 0 0 / 0.12));
159
+ transform-origin: var(--transform-origin);
160
+ transition:
161
+ opacity 0.12s ease-out,
162
+ scale 0.12s ease-out;
163
+ }
164
+
165
+ .ObAutocomplete-popup[data-starting-style],
166
+ .ObAutocomplete-popup[data-ending-style] {
167
+ opacity: 0;
168
+ scale: 0.98;
169
+ }
170
+
171
+ .ObAutocomplete-list {
172
+ display: flex;
173
+ flex-direction: column;
174
+ }
175
+
176
+ .ObAutocomplete-item {
177
+ display: flex;
178
+ align-items: center;
179
+ gap: var(--ds-size-2, 0.5rem);
180
+ padding: var(--ds-size-2, 0.5rem);
181
+ border-radius: var(--ds-border-radius-sm, 0.25rem);
182
+ cursor: default;
183
+ user-select: none;
184
+ scroll-margin-block: var(--ds-size-1, 0.25rem);
185
+ }
186
+
187
+ .ObAutocomplete-item[data-highlighted] {
188
+ background-color: var(--ds-color-accent-surface-hover, #d6e2f2);
189
+ }
190
+
191
+ .ObAutocomplete-item[data-selected] {
192
+ font-weight: var(--ds-font-weight-medium, 500);
193
+ background-color: var(--ds-color-accent-surface-default, #e6effa);
194
+ }
195
+
196
+ .ObAutocomplete-item[data-selected][data-highlighted] {
197
+ background-color: var(--ds-color-accent-surface-active, #c2d5ec);
198
+ }
199
+
200
+ .ObAutocomplete-item[data-disabled] {
201
+ cursor: not-allowed;
202
+ opacity: var(--ds-disabled-opacity, 0.3);
203
+ }
204
+
205
+ .ObAutocomplete-itemIndicator {
206
+ display: inline-flex;
207
+ flex: none;
208
+ inline-size: 1em;
209
+ }
210
+
211
+ .ObAutocomplete-groupLabel {
212
+ padding: var(--ds-size-2, 0.5rem);
213
+ font-size: var(--ds-font-size-1, 0.75rem);
214
+ font-weight: var(--ds-font-weight-semibold, 600);
215
+ color: var(--ds-color-neutral-text-subtle, #4c5866);
216
+ text-transform: uppercase;
217
+ }
218
+
219
+ .ObAutocomplete-empty:not(:empty) {
220
+ padding: var(--ds-size-2, 0.5rem);
221
+ color: var(--ds-color-neutral-text-subtle, #4c5866);
222
+ }