@tai42/studio-sdk 17.1.2 → 18.0.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.
Files changed (77) hide show
  1. package/dist/components/execution-key-picker.d.ts +37 -0
  2. package/dist/components/execution-key-picker.d.ts.map +1 -0
  3. package/dist/components/execution-key-picker.js +49 -0
  4. package/dist/components/execution-key-picker.js.map +1 -0
  5. package/dist/components/json-tree-nodes.d.ts.map +1 -1
  6. package/dist/components/json-tree-nodes.js +24 -9
  7. package/dist/components/json-tree-nodes.js.map +1 -1
  8. package/dist/components/overflow-measure.d.ts +11 -3
  9. package/dist/components/overflow-measure.d.ts.map +1 -1
  10. package/dist/components/overflow-measure.js +8 -5
  11. package/dist/components/overflow-measure.js.map +1 -1
  12. package/dist/components/scroll-region.d.ts +12 -2
  13. package/dist/components/scroll-region.d.ts.map +1 -1
  14. package/dist/components/scroll-region.js +7 -5
  15. package/dist/components/scroll-region.js.map +1 -1
  16. package/dist/components/subject-section.d.ts +78 -0
  17. package/dist/components/subject-section.d.ts.map +1 -0
  18. package/dist/components/subject-section.js +66 -0
  19. package/dist/components/subject-section.js.map +1 -0
  20. package/dist/components/tool-picker.d.ts.map +1 -1
  21. package/dist/components/tool-picker.js +11 -1
  22. package/dist/components/tool-picker.js.map +1 -1
  23. package/dist/hooks/useApi.d.ts +96 -45
  24. package/dist/hooks/useApi.d.ts.map +1 -1
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/schema-form/array-field.d.ts.map +1 -1
  30. package/dist/schema-form/array-field.js +3 -3
  31. package/dist/schema-form/array-field.js.map +1 -1
  32. package/dist/schema-form/context.d.ts +19 -0
  33. package/dist/schema-form/context.d.ts.map +1 -1
  34. package/dist/schema-form/context.js.map +1 -1
  35. package/dist/schema-form/field-model.d.ts +19 -0
  36. package/dist/schema-form/field-model.d.ts.map +1 -1
  37. package/dist/schema-form/field-node.d.ts +10 -3
  38. package/dist/schema-form/field-node.d.ts.map +1 -1
  39. package/dist/schema-form/field-node.js +16 -2
  40. package/dist/schema-form/field-node.js.map +1 -1
  41. package/dist/schema-form/keyed-callbacks.d.ts +2 -0
  42. package/dist/schema-form/keyed-callbacks.d.ts.map +1 -0
  43. package/dist/schema-form/keyed-callbacks.js +45 -0
  44. package/dist/schema-form/keyed-callbacks.js.map +1 -0
  45. package/dist/schema-form/object-fields.d.ts.map +1 -1
  46. package/dist/schema-form/object-fields.js +5 -3
  47. package/dist/schema-form/object-fields.js.map +1 -1
  48. package/dist/schema-form/record-field.d.ts.map +1 -1
  49. package/dist/schema-form/record-field.js +4 -6
  50. package/dist/schema-form/record-field.js.map +1 -1
  51. package/dist/schema-form/string-annotations.d.ts.map +1 -1
  52. package/dist/schema-form/string-annotations.js +26 -0
  53. package/dist/schema-form/string-annotations.js.map +1 -1
  54. package/dist/schema-form/string-field.js +22 -6
  55. package/dist/schema-form/string-field.js.map +1 -1
  56. package/dist/testing.d.ts +9 -7
  57. package/dist/testing.d.ts.map +1 -1
  58. package/dist/testing.js +16 -13
  59. package/dist/testing.js.map +1 -1
  60. package/package.json +3 -3
  61. package/src/components/execution-key-picker.tsx +116 -0
  62. package/src/components/json-tree-nodes.tsx +36 -18
  63. package/src/components/overflow-measure.ts +16 -5
  64. package/src/components/scroll-region.tsx +16 -3
  65. package/src/components/subject-section.tsx +177 -0
  66. package/src/components/tool-picker.tsx +14 -2
  67. package/src/index.ts +11 -0
  68. package/src/schema-form/array-field.tsx +3 -3
  69. package/src/schema-form/context.ts +20 -0
  70. package/src/schema-form/field-model.ts +20 -0
  71. package/src/schema-form/field-node.tsx +11 -3
  72. package/src/schema-form/keyed-callbacks.ts +46 -0
  73. package/src/schema-form/object-fields.tsx +8 -3
  74. package/src/schema-form/record-field.tsx +7 -6
  75. package/src/schema-form/string-annotations.ts +32 -1
  76. package/src/schema-form/string-field.tsx +25 -6
  77. package/src/testing.ts +21 -13
package/src/index.ts CHANGED
@@ -221,6 +221,7 @@ export { Button, Card, EmptyState, ErrorState, Skeleton, Spinner } from './compo
221
221
  export type { RadioGroupProps, RadioOption } from './components/radio-group';
222
222
  export { RadioGroup } from './components/radio-group';
223
223
  export type {
224
+ OverflowAxis,
224
225
  OverflowRegionAttributes,
225
226
  ProseScrollLabels,
226
227
  ScrollRegionProps,
@@ -295,6 +296,8 @@ export { openTargetProps } from './components/open-target';
295
296
  // hands back the parsed URL, so a caller that needs the URL does not re-parse it.
296
297
  export type { CopyFieldProps } from './components/copy-field';
297
298
  export { CopyField } from './components/copy-field';
299
+ export type { ExecutionKeyPickerProps, ExecutionKeyQuery } from './components/execution-key-picker';
300
+ export { ExecutionKeyPicker, isExecutionKeyListEmpty } from './components/execution-key-picker';
298
301
  export type { ExtensionComboBuilderProps } from './components/extension-combo-builder';
299
302
  export { ExtensionComboBuilder } from './components/extension-combo-builder';
300
303
  export type { ExtensionFamily, ExtensionKindGroup } from './components/extension-grouping';
@@ -311,6 +314,14 @@ export type { ExternalLinkButtonProps } from './components/external-link';
311
314
  export { safeHttpUrl } from './components/primitives';
312
315
  export type { RevealInputProps } from './components/reveal-input';
313
316
  export { RevealInput } from './components/reveal-input';
317
+ export type { SubjectSectionProps, SubjectTargetOption } from './components/subject-section';
318
+ export {
319
+ buildSubject,
320
+ SUBJECT_INCOMPLETE_MESSAGE,
321
+ SubjectSection,
322
+ toSubjectTargetOptions,
323
+ useSubjectFields,
324
+ } from './components/subject-section';
314
325
  export type { TokenName } from './components/tokens';
315
326
  export { TOKEN_NAMES } from './components/tokens';
316
327
  export type { ToolPickerProps } from './components/tool-picker';
@@ -11,6 +11,7 @@ import { classifySchema } from './classify';
11
11
  import { defaultValueForSchema } from './default-value';
12
12
  import { FieldGroup } from './field-group';
13
13
  import { FieldNode } from './field-node';
14
+ import { useKeyedCallbacks } from './keyed-callbacks';
14
15
  import type { JsonSchema, SchemaFormErrors } from './types';
15
16
 
16
17
  export function ArrayField({
@@ -46,6 +47,7 @@ export function ArrayField({
46
47
  const setItem = (index: number, next: unknown): void => {
47
48
  onChange(list.map((item, position) => (position === index ? next : item)));
48
49
  };
50
+ const changeFor = useKeyedCallbacks<number, unknown>(setItem, list.keys());
49
51
 
50
52
  return (
51
53
  <FieldGroup heading={heading} description={description} error={error} atRoot={false}>
@@ -63,9 +65,7 @@ export function ArrayField({
63
65
  schema={items}
64
66
  root={root}
65
67
  value={item}
66
- onChange={(next) => {
67
- setItem(index, next);
68
- }}
68
+ onChange={changeFor(index)}
69
69
  path={`${path}[${String(index)}]`}
70
70
  label={`Item ${String(index + 1)}`}
71
71
  required
@@ -52,6 +52,23 @@ export interface ExpressionInputKey {
52
52
  readonly gloss: string;
53
53
  }
54
54
 
55
+ /**
56
+ * One named jq variable the form hands the door beside `.`, mirroring jq-studio's
57
+ * variable descriptor without importing it. The door lists it in the legend, offers
58
+ * `$name` as a path root, and seeds the Test panel with its `sample`.
59
+ */
60
+ export interface ExpressionInputVariable {
61
+ /** The variable name WITHOUT the leading `$` (referenced as `$name`). */
62
+ readonly name: string;
63
+ /** One sentence: what this variable holds. */
64
+ readonly blurb: string;
65
+ /** The top-level keys of the variable's value; empty when it is a scalar or
66
+ * the annotation glosses none. */
67
+ readonly keys: readonly ExpressionInputKey[];
68
+ /** A static skeleton of the variable's value for the door's Test panel. */
69
+ readonly sample?: unknown;
70
+ }
71
+
55
72
  /**
56
73
  * What `.` IS for an expression-annotated field, as the form hands it to the
57
74
  * injected door. Built by the renderer from the schema's `x-tai42-expression`
@@ -77,6 +94,9 @@ export interface ExpressionInputShape {
77
94
  readonly caveats?: readonly string[];
78
95
  /** A static skeleton of `.` for the door's test surface. */
79
96
  readonly sample?: unknown;
97
+ /** The named variables the expression reads as `$name` beside `.`; omitted when
98
+ * the field declares none. */
99
+ readonly variables?: readonly ExpressionInputVariable[];
80
100
  }
81
101
 
82
102
  /**
@@ -34,6 +34,23 @@ export interface ExpressionAnnotationKey {
34
34
  readonly gloss: string;
35
35
  }
36
36
 
37
+ /**
38
+ * One named jq variable the expression may read as `$name` beside its `.` input:
39
+ * its blurb and a representative sample the editor's Test panel binds it to. The
40
+ * `x-tai42-expression` annotation carries these under `variables`; a field whose
41
+ * `.` is its only input carries none.
42
+ */
43
+ export interface ExpressionAnnotationVariable {
44
+ readonly name: string;
45
+ readonly blurb: string;
46
+ /** True when the annotation carries a `sample` for this variable — tracked
47
+ * separately because a legitimate sample may be JSON `null`. */
48
+ readonly hasSample: boolean;
49
+ /** The sample value the Test panel binds `$name` to. Meaningful only when
50
+ * {@link hasSample} is true. */
51
+ readonly sample: unknown;
52
+ }
53
+
37
54
  /**
38
55
  * The `x-tai42-expression` annotation on a string schema: the server declares
39
56
  * that the field's value is authored in a pipeline expression language (only jq
@@ -53,6 +70,9 @@ export interface ExpressionAnnotation {
53
70
  readonly blurb: string | undefined;
54
71
  /** The top-level keys of `.`, each with a one-liner. */
55
72
  readonly keys: readonly ExpressionAnnotationKey[] | undefined;
73
+ /** The named variables the expression reads as `$name` beside `.`. Absent when
74
+ * the annotation declares none (the field's `.` is its only input). */
75
+ readonly variables: readonly ExpressionAnnotationVariable[] | undefined;
56
76
  /** What the expression must return (e.g. "true or false"). */
57
77
  readonly returns: string | undefined;
58
78
  /** Per-field caveats. */
@@ -4,7 +4,7 @@
4
4
  * inline; every other kind delegates to a dedicated field component). Object,
5
5
  * array, and union renderers recurse back through here for their children.
6
6
  */
7
- import type { ReactNode } from 'react';
7
+ import { memo, type ReactNode } from 'react';
8
8
 
9
9
  import { Checkbox } from '../components/checkbox';
10
10
  import { Field } from '../components/field';
@@ -45,8 +45,14 @@ function labelText(explicit: string | undefined, label: string | undefined): str
45
45
  return explicit ?? label ?? 'Value';
46
46
  }
47
47
 
48
- /** A single schema node, dispatched on its classified model kind. */
49
- export function FieldNode(props: FieldNodeProps): ReactNode {
48
+ /**
49
+ * A single schema node, dispatched on its classified model kind. Wrapped in
50
+ * `memo` so a field whose props are unchanged is skipped: the containers hand
51
+ * each child a referentially-stable `onChange` (see {@link useKeyedCallbacks})
52
+ * and the controlled value keeps unedited branches identity-equal, so editing
53
+ * one field re-renders only that field's subtree, not every sibling.
54
+ */
55
+ function FieldNodeImpl(props: FieldNodeProps): ReactNode {
50
56
  const { root, value, onChange, path, required, idPrefix, errors } = props;
51
57
  const classified = classifySchema(props.schema, root);
52
58
  const base = labelText(classified.title, props.label);
@@ -187,6 +193,8 @@ export function FieldNode(props: FieldNodeProps): ReactNode {
187
193
  }
188
194
  }
189
195
 
196
+ export const FieldNode = memo(FieldNodeImpl);
197
+
190
198
  /** The label/description/error chrome every scalar leaf field shares. */
191
199
  interface ScalarFieldChrome {
192
200
  readonly heading: string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A stable per-key callback factory. A container (object, array, record) renders
3
+ * one child per key and folds that child's edits back into the container value.
4
+ * Written inline, each child's `onChange` is a fresh closure every render, which
5
+ * defeats {@link FieldNode}'s `memo`: an untouched sibling re-renders whenever any
6
+ * field changes. This returns the SAME function identity for a given key across
7
+ * renders — so a memoized child keyed on that identity is skipped when a sibling
8
+ * changes — while each call reads the latest folding logic through a ref, so the
9
+ * callback never closes over a stale container value.
10
+ *
11
+ * The cache is bounded by `liveKeys`, the container's current key set: every
12
+ * render evicts each cached handler whose key is not live, so a mounted form that
13
+ * adds and drops entries holds at most one handler per live key (the record
14
+ * field keys by a monotonic row id, so without eviction it would leak one dead
15
+ * handler per id ever created). Eviction runs during render on the ref-held Map —
16
+ * no effect, no extra render — and never touches a live key's handler, so
17
+ * identity stays stable for every key that survives the render.
18
+ */
19
+ import { useRef } from 'react';
20
+
21
+ export function useKeyedCallbacks<K, A>(
22
+ fold: (key: K, arg: A) => void,
23
+ liveKeys: Iterable<K>,
24
+ ): (key: K) => (arg: A) => void {
25
+ const foldRef = useRef(fold);
26
+ foldRef.current = fold;
27
+ // The cache lives in a ref, lazily initialised, so its identity is HARD-stable
28
+ // for the component's whole life: a `useMemo(() => new Map(), [])` cache is only
29
+ // a hint React may discard, which would drop every cached handler and re-render
30
+ // every sibling — the regression the memo is here to prevent.
31
+ const cacheRef = useRef<Map<K, (arg: A) => void> | undefined>(undefined);
32
+ const cache = (cacheRef.current ??= new Map<K, (arg: A) => void>());
33
+ const live = new Set<K>(liveKeys);
34
+ for (const key of cache.keys()) {
35
+ if (!live.has(key)) cache.delete(key);
36
+ }
37
+ return (key: K) => {
38
+ const cached = cache.get(key);
39
+ if (cached !== undefined) return cached;
40
+ const handler = (arg: A): void => {
41
+ foldRef.current(key, arg);
42
+ };
43
+ cache.set(key, handler);
44
+ return handler;
45
+ };
46
+ }
@@ -7,6 +7,7 @@
7
7
  import type { ReactNode } from 'react';
8
8
 
9
9
  import { FieldNode } from './field-node';
10
+ import { useKeyedCallbacks } from './keyed-callbacks';
10
11
  import type { JsonSchema, SchemaFormErrors } from './types';
11
12
 
12
13
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -38,6 +39,12 @@ export function ObjectFields({
38
39
  markRequired?: boolean;
39
40
  }): ReactNode {
40
41
  const obj = isPlainObject(value) ? value : {};
42
+ const changeFor = useKeyedCallbacks<string, unknown>(
43
+ (name, next) => {
44
+ onChange(setKey(obj, name, next));
45
+ },
46
+ properties.map(([name]) => name),
47
+ );
41
48
  return (
42
49
  <>
43
50
  {properties.map(([name, propSchema]) => {
@@ -49,9 +56,7 @@ export function ObjectFields({
49
56
  schema={propSchema}
50
57
  root={root}
51
58
  value={obj[name]}
52
- onChange={(next) => {
53
- onChange(setKey(obj, name, next));
54
- }}
59
+ onChange={changeFor(name)}
55
60
  path={childPath}
56
61
  label={name}
57
62
  required={requiredKeys.has(name)}
@@ -22,6 +22,7 @@ import { RecordEntryRendererContext } from './context';
22
22
  import { defaultValueForSchema } from './default-value';
23
23
  import { FieldGroup } from './field-group';
24
24
  import { FieldNode } from './field-node';
25
+ import { useKeyedCallbacks } from './keyed-callbacks';
25
26
  import type { JsonSchema, SchemaFormErrors } from './types';
26
27
 
27
28
  interface Row {
@@ -119,6 +120,10 @@ export function RecordField({
119
120
  const setValue = (id: number, next: unknown): void => {
120
121
  commit(rows.map((row) => (row.id === id ? { ...row, value: next } : row)));
121
122
  };
123
+ const valueChangeFor = useKeyedCallbacks<number, unknown>(
124
+ setValue,
125
+ rows.map((row) => row.id),
126
+ );
122
127
  const addRow = (): void => {
123
128
  commit([...rows, { id: nextId.current++, key: '', value: newEntryValue(values, root) }]);
124
129
  };
@@ -146,9 +151,7 @@ export function RecordField({
146
151
  schema={values}
147
152
  root={root}
148
153
  value={row.value}
149
- onChange={(next) => {
150
- setValue(row.id, next);
151
- }}
154
+ onChange={valueChangeFor(row.id)}
152
155
  path={entryPath}
153
156
  label={undefined}
154
157
  required
@@ -179,9 +182,7 @@ export function RecordField({
179
182
  path: entryPath,
180
183
  valueSchema: values,
181
184
  value: row.value,
182
- onChange: (next) => {
183
- setValue(row.id, next);
184
- },
185
+ onChange: valueChangeFor(row.id),
185
186
  defaultField,
186
187
  })}
187
188
  </div>
@@ -6,7 +6,12 @@
6
6
  * renders differently from the plain string it falls back to.
7
7
  */
8
8
  import { isRecord } from '../guards';
9
- import type { ExpressionAnnotation, ExpressionAnnotationKey, MediaUpload } from './field-model';
9
+ import type {
10
+ ExpressionAnnotation,
11
+ ExpressionAnnotationKey,
12
+ ExpressionAnnotationVariable,
13
+ MediaUpload,
14
+ } from './field-model';
10
15
  import type { JsonSchema } from './types';
11
16
 
12
17
  const EXPRESSION_KEYWORD = 'x-tai42-expression';
@@ -58,6 +63,29 @@ function parseAnnotationKeys(
58
63
  return keys;
59
64
  }
60
65
 
66
+ /** Parse the optional `variables` array: absent → `undefined`, malformed → `null`
67
+ * (reject). Each entry needs a string `name` and `blurb`; `sample` is any JSON
68
+ * (including `null`), so its presence is tracked rather than typed. */
69
+ function parseAnnotationVariables(
70
+ value: unknown,
71
+ ): readonly ExpressionAnnotationVariable[] | undefined | null {
72
+ if (value === undefined) return undefined;
73
+ if (!Array.isArray(value)) return null;
74
+ const variables: ExpressionAnnotationVariable[] = [];
75
+ for (const entry of value as readonly unknown[]) {
76
+ if (!isRecord(entry) || typeof entry.name !== 'string' || typeof entry.blurb !== 'string') {
77
+ return null;
78
+ }
79
+ variables.push({
80
+ name: entry.name,
81
+ blurb: entry.blurb,
82
+ hasSample: 'sample' in entry,
83
+ sample: entry.sample,
84
+ });
85
+ }
86
+ return variables;
87
+ }
88
+
61
89
  /** Parse the optional `caveats` array: absent → `undefined`, malformed → `null` (reject). */
62
90
  function parseCaveatList(value: unknown): readonly string[] | undefined | null {
63
91
  if (value === undefined) return undefined;
@@ -94,6 +122,8 @@ export function expressionAnnotation(schema: JsonSchema): ExpressionAnnotation |
94
122
 
95
123
  const keys = parseAnnotationKeys(raw.keys);
96
124
  if (keys === null) return undefined;
125
+ const variables = parseAnnotationVariables(raw.variables);
126
+ if (variables === null) return undefined;
97
127
  const caveats = parseCaveatList(raw.caveats);
98
128
  if (caveats === null) return undefined;
99
129
 
@@ -103,6 +133,7 @@ export function expressionAnnotation(schema: JsonSchema): ExpressionAnnotation |
103
133
  label: label.value,
104
134
  blurb: blurb.value,
105
135
  keys,
136
+ variables,
106
137
  returns: returns.value,
107
138
  caveats,
108
139
  hasSample: 'sample' in raw,
@@ -296,12 +296,14 @@ function expressionShape(
296
296
  argName: string,
297
297
  ): ExpressionInputShape | undefined {
298
298
  const declared =
299
- expression.label !== undefined ||
300
- expression.blurb !== undefined ||
301
- expression.keys !== undefined ||
302
- expression.returns !== undefined ||
303
- expression.caveats !== undefined ||
304
- expression.hasSample;
299
+ [
300
+ expression.label,
301
+ expression.blurb,
302
+ expression.keys,
303
+ expression.variables,
304
+ expression.returns,
305
+ expression.caveats,
306
+ ].some((field) => field !== undefined) || expression.hasSample;
305
307
  if (!declared) return undefined;
306
308
  return {
307
309
  // Opaque, host-namespaced, stable per field (memoisation/telemetry only).
@@ -312,6 +314,23 @@ function expressionShape(
312
314
  returns: expression.returns ?? '',
313
315
  ...(expression.caveats === undefined ? {} : { caveats: expression.caveats }),
314
316
  ...(expression.hasSample ? { sample: expression.sample } : {}),
317
+ ...(expression.variables === undefined
318
+ ? {}
319
+ : { variables: expression.variables.map(expressionVariable) }),
320
+ };
321
+ }
322
+
323
+ /** Map one classified annotation variable onto the door's variable descriptor. */
324
+ function expressionVariable(
325
+ variable: NonNullable<ExpressionAnnotation['variables']>[number],
326
+ ): NonNullable<ExpressionInputShape['variables']>[number] {
327
+ return {
328
+ name: variable.name,
329
+ blurb: variable.blurb,
330
+ // The annotation glosses no sub-keys of a variable's value, so the door gets
331
+ // the variable's shape as untyped (empty keys).
332
+ keys: [],
333
+ ...(variable.hasSample ? { sample: variable.sample } : {}),
315
334
  };
316
335
  }
317
336
 
package/src/testing.ts CHANGED
@@ -109,25 +109,33 @@ export function flushResizeObserversFor(target: Element): void {
109
109
  }
110
110
  }
111
111
 
112
- /** The faked widths: equal reads as "fits", unequal as "scrolls". */
113
- const FITTING_WIDTH = 100;
114
- const OVERFLOWING_SCROLL_WIDTH = 400;
112
+ /** The faked extents: equal reads as "fits", unequal as "scrolls". */
113
+ const FITTING_EXTENT = 100;
114
+ const OVERFLOWING_SCROLL_EXTENT = 400;
115
115
 
116
116
  /**
117
- * Makes `element` report itself as horizontally overflowing (or not). jsdom runs
118
- * no layout, so `scrollWidth` and `clientWidth` are both 0 and every
119
- * overflow test reads as "fits"; this shadows the prototype getters with own,
120
- * configurable values so a test can drive either state. Call it again with the
121
- * opposite flag to flip the element back.
117
+ * Makes `element` report itself as overflowing (or not) on `axis`. jsdom runs no
118
+ * layout, so `scroll*` and `client*` are both 0 and every overflow test reads as
119
+ * "fits"; this shadows the prototype getters with own, configurable values so a
120
+ * test can drive either state. Call it again with the opposite flag to flip the
121
+ * element back.
122
122
  *
123
123
  * @param element - the element whose scroll metrics to fake.
124
- * @param overflowing - true to report content wider than the box.
124
+ * @param overflowing - true to report content larger than the box on `axis`.
125
+ * @param axis - the dimension to fake: `horizontal` (width, the default) or
126
+ * `vertical` (height).
125
127
  */
126
- export function setElementOverflow(element: HTMLElement, overflowing: boolean): void {
127
- Object.defineProperty(element, 'clientWidth', { configurable: true, value: FITTING_WIDTH });
128
- Object.defineProperty(element, 'scrollWidth', {
128
+ export function setElementOverflow(
129
+ element: HTMLElement,
130
+ overflowing: boolean,
131
+ axis: 'horizontal' | 'vertical' = 'horizontal',
132
+ ): void {
133
+ const [client, scroll] =
134
+ axis === 'vertical' ? ['clientHeight', 'scrollHeight'] : ['clientWidth', 'scrollWidth'];
135
+ Object.defineProperty(element, client, { configurable: true, value: FITTING_EXTENT });
136
+ Object.defineProperty(element, scroll, {
129
137
  configurable: true,
130
- value: overflowing ? OVERFLOWING_SCROLL_WIDTH : FITTING_WIDTH,
138
+ value: overflowing ? OVERFLOWING_SCROLL_EXTENT : FITTING_EXTENT,
131
139
  });
132
140
  }
133
141