@adaptabletools/adaptable 23.0.10 → 23.0.12

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptabletools/adaptable",
3
- "version": "23.0.10",
3
+ "version": "23.0.12",
4
4
  "description": "Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements",
5
5
  "keywords": [
6
6
  "web-components",
@@ -5,7 +5,7 @@ import { useState } from 'react';
5
5
  import Input from '../../../components/Input';
6
6
  import { useAdaptable } from '../../AdaptableContext';
7
7
  import { getDefaultAdaptableOptions } from '../../../AdaptableOptions/DefaultAdaptableOptions';
8
- export const AdaptableDateInlineInput = React.forwardRef((props, ref) => {
8
+ export const AdaptableDateInlineInput = React.forwardRef(function AdaptableDateInlineInputCmp(props, ref) {
9
9
  const [hasFocus, setHasFocus] = useState(false);
10
10
  const adaptable = useAdaptable();
11
11
  const dateInputOptions = adaptable?.adaptableOptions.userInterfaceOptions.dateInputOptions ??
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
  import { InputProps } from '../../../components/Input';
3
3
  export type AdaptableInputProps = InputProps & {
4
4
  showClearButton?: boolean;
5
+ showTriggerButton?: boolean;
5
6
  };
6
7
  declare const AdaptableInput: React.ForwardRefExoticComponent<Omit<AdaptableInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
7
8
  export default AdaptableInput;
@@ -5,9 +5,9 @@ import { useAdaptable } from '../../AdaptableContext';
5
5
  import AdaptableDateInput from '../AdaptableDateInput';
6
6
  const AdaptableInput = React.forwardRef((props, ref) => {
7
7
  const useNativeInputDate = useAdaptable().adaptableOptions.userInterfaceOptions?.dateInputOptions?.useNativeInput;
8
- const { showClearButton, ...inputProps } = props;
8
+ const { showClearButton, showTriggerButton, ...inputProps } = props;
9
9
  if (props.type === 'date' && !useNativeInputDate) {
10
- return (_jsx(AdaptableDateInput, { showClearButton: showClearButton, ...inputProps, ref: ref }));
10
+ return (_jsx(AdaptableDateInput, { showClearButton: showClearButton, showTriggerButton: showTriggerButton, ...inputProps, ref: ref }));
11
11
  }
12
12
  else {
13
13
  return _jsx(Input, { ...inputProps, ref: ref });
@@ -32,7 +32,7 @@ const ColumnFilterPredicateDropdown = (props) => {
32
32
  }, items: options, placeholder: "Add Condition", value: isAndOr ? null : props.predicate?.operator }) }));
33
33
  };
34
34
  const ColumnFilterEditor = (props) => {
35
- return (_jsxs(Box, { className: "twa:mb-3 twa:overflow-x-hidden twa:p-0.5", children: [_jsxs(Flex, { className: "twa:mb-2", children: [_jsx(ColumnFilterPredicateDropdown, { columnId: props.columnId, disabled: props.disabled, predicate: props.predicate, predicateDefs: props.predicateDefs, onPredicateChange: props.onPredicateChange }), _jsx(Flex, { alignItems: "center", className: "twa:ml-2", children: _jsx(SimpleButton, { disabled: props.deleteDisabled, onClick: props.onDelete, variant: "text", icon: "delete" }) })] }), _jsx(ColumnFilterInputList, { columnId: props.columnId, disabled: props.disabled, predicate: props.predicate, onPredicateChange: props.onPredicateChange, predicateDefs: props.predicateDefs })] }));
35
+ return (_jsxs(Box, { className: "twa:mb-3 twa:overflow-x-hidden twa:p-0.5 twa:gap-1 twa:flex twa:flex-col", children: [_jsxs(Flex, { className: "twa:mb-1", children: [_jsx(ColumnFilterPredicateDropdown, { columnId: props.columnId, disabled: props.disabled, predicate: props.predicate, predicateDefs: props.predicateDefs, onPredicateChange: props.onPredicateChange }), _jsx(Flex, { alignItems: "center", className: "twa:ml-2", children: _jsx(SimpleButton, { disabled: props.deleteDisabled, onClick: props.onDelete, variant: "text", icon: "delete" }) })] }), _jsx(ColumnFilterInputList, { columnId: props.columnId, disabled: props.disabled, predicate: props.predicate, onPredicateChange: props.onPredicateChange, predicateDefs: props.predicateDefs })] }));
36
36
  };
37
37
  const AndOrInput = (props) => {
38
38
  return (_jsxs(Flex, { className: "twa:flex-1", children: [_jsx(Radio, { className: "twa:mr-2", onClick: () => props.onChange('AND'), checked: props.operator === 'AND', children: "AND" }), _jsx(Radio, { className: "twa:mr-2", onClick: () => props.onChange('OR'), checked: props.operator === 'OR', children: "OR" })] }));
@@ -1,6 +1,5 @@
1
1
  import * as React from 'react';
2
2
  import { AdaptableInputProps } from '../../AdaptableInput';
3
- import { AdaptableDateInputProps } from '../../AdaptableDateInput';
4
3
  type ColumnFilterEditorChangeProps = {
5
4
  type: 'text';
6
5
  value: string;
@@ -25,6 +24,6 @@ export declare const ColumnFilterInput: (props: {
25
24
  filterType?: "floating" | "default";
26
25
  usePrimitiveInputs?: boolean;
27
26
  columnId: string;
28
- inputProps?: AdaptableInputProps | AdaptableDateInputProps;
27
+ inputProps?: AdaptableInputProps;
29
28
  } & ColumnFilterEditorChangeProps) => React.JSX.Element;
30
29
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { debounce } from '../../../../Utilities/Helpers/TimingHelper';
3
3
  import * as React from 'react';
4
4
  import { getOperatorMatchingInputs } from '../../../../components/ExpressionEditor/QueryBuilder/utils';
@@ -51,7 +51,7 @@ export const FloatingFilterInputList = (props) => {
51
51
  'twa:*:[input]:border-none twa:*:[[role=button]]:border-none': hasMultipleInputs,
52
52
  }), children: (matchingInputs ?? []).map((input, index) => {
53
53
  const value = props?.predicate?.args?.[index];
54
- return (_jsxs(_Fragment, { children: [index > 0 && _jsx("div", { className: "twa:border-l twa:border-input-border" }), _jsx(ColumnFilterInput, { inputProps: {
54
+ return (_jsxs(React.Fragment, { children: [index > 0 && (_jsx("div", { className: "twa:border-l twa:border-input-border" }, `${props.columnId}-${index}-divider`)), _jsx(ColumnFilterInput, { inputProps: {
55
55
  showTriggerButton: hasMultipleInputs ? false : undefined,
56
56
  }, filterType: "floating", columnId: props.columnId, usePrimitiveInputs: true, disabled: props.disabled, type: input, value: value, onKeyDown: props.onKeyDown, onClear: () => {
57
57
  const args = [...props.predicate.args];
@@ -68,6 +68,6 @@ export const FloatingFilterInputList = (props) => {
68
68
  };
69
69
  newPredicate.args[index] = value;
70
70
  debouncedPredicateChange(newPredicate);
71
- } }, index)] }));
71
+ } }, `${props.columnId}-${index}`)] }, `${props.columnId}-${index}`));
72
72
  }) }));
73
73
  };
@@ -42,7 +42,13 @@ export function VirtualizedList({ open, virtualizerRef, showItemTooltip, renderC
42
42
  ? `Create "${itemText}"`
43
43
  : itemText
44
44
  : undefined;
45
- const label = isUncreatedCreatable ? `Create "${item.label}"` : item.label;
45
+ let label = isUncreatedCreatable ? `Create "${item.label}"` : item.label;
46
+ if (label && typeof label === 'object') {
47
+ if (Object.prototype.hasOwnProperty.call(label, 'toString') &&
48
+ typeof label.toString === 'function') {
49
+ label = label.toString();
50
+ }
51
+ }
46
52
  const itemLabel = renderItemLabel ? (renderItemLabel(label, item)) : (_jsx("span", { className: "twa:min-w-0 twa:flex-1 twa:truncate", children: label }));
47
53
  const itemContent = isUncreatedCreatable ? (_jsxs(_Fragment, { children: [_jsx(PlusIcon, {}), itemLabel] })) : (itemLabel);
48
54
  return (_jsx(ComboboxItem, { index: virtualItem.index, "data-index": virtualItem.index, ref: virtualizer.measureElement, value: item, renderCheckboxIndicator: renderCheckboxIndicator, "aria-setsize": filteredItems.length, "aria-posinset": virtualItem.index + 1, "aria-label": itemText ?? undefined, style: {
@@ -49,6 +49,14 @@ export type ComboboxCommonProps<T extends ComboboxItemType> = {
49
49
  showClear?: boolean;
50
50
  clearOnEscape?: boolean;
51
51
  searchable?: boolean | 'inline' | 'menulist';
52
+ /**
53
+ * Only applies when `searchable === 'menulist'` and the search input is
54
+ * uncontrolled (no `inputValue` prop): when true, the text typed in the
55
+ * in-popup search input survives closing the popup, so reopening it shows
56
+ * the list filtered by the previous query. Defaults to false — the filter
57
+ * is cleared on close and every open starts from the full list.
58
+ */
59
+ persistFilterOnReopen?: boolean;
52
60
  /**
53
61
  * Custom filter function, or `null` to disable built-in filtering
54
62
  * (e.g. when options are already filtered server-side).
@@ -10,9 +10,9 @@ import { VirtualizedList } from './VirtualizedList';
10
10
  import { CheckBox } from '../CheckBox';
11
11
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
12
12
  const getContentStyle = ({ maxLabelLength }) => {
13
- const labelWidth = `${maxLabelLength + 3}ch`;
13
+ const labelWidth = `${maxLabelLength + 6}ch`;
14
14
  return {
15
- width: labelWidth,
15
+ width: `max(${labelWidth}, calc(var(--ab-base-space) * 25), var(--anchor-width))`,
16
16
  minWidth: `min(var(--anchor-width), var(--available-width), 80vw)`,
17
17
  };
18
18
  };
@@ -104,7 +104,9 @@ const AdaptableCombobox_Root = (props) => {
104
104
  eventDetails.cancel?.();
105
105
  return;
106
106
  }
107
- if (props.searchable === 'menulist' && eventDetails?.reason === 'input-clear' && props.open) {
107
+ if (props.searchable === 'menulist' &&
108
+ eventDetails?.reason === 'input-clear' &&
109
+ (props.open || props.persistFilterOnReopen)) {
108
110
  eventDetails.cancel?.();
109
111
  return;
110
112
  }
@@ -164,10 +166,10 @@ const useGetAllItems = (props) => {
164
166
  onInputValueChange,
165
167
  onValueChange,
166
168
  allItems,
167
- maxLabelLength: allItems.reduce((max, item) => Math.max(max, typeof item.label === 'string'
168
- ? item.label.length
169
- : typeof item.textLabel === 'string'
170
- ? item.textLabel.length
169
+ maxLabelLength: allItems.reduce((max, item) => Math.max(max, typeof item.label === 'string' || typeof item.label === 'number'
170
+ ? String(item.label).length
171
+ : typeof item.textLabel === 'string' || typeof item.textLabel === 'number'
172
+ ? String(item.textLabel).length
171
173
  : 0), 0),
172
174
  };
173
175
  };
@@ -188,6 +188,11 @@ export const Datepicker = React.forwardRef((props, ref) => {
188
188
  if (isValid(date)) {
189
189
  updateValue(date);
190
190
  }
191
+ }, onKeyDown: (e) => {
192
+ if (e.key === 'Enter' && !open && !disabled) {
193
+ e.preventDefault();
194
+ setPopoverOpen(true);
195
+ }
191
196
  }, className: "twa:h-auto twa:min-w-0 twa:outline-none twa:min-h-auto twa:py-0 twa:flex-1 twa:rounded-none twa:border-0 twa:bg-transparent twa:shadow-none twa:ring-0 twa:focus-visible:ring-0 twa:dark:bg-transparent", style: style, disabled: disabled }), _jsxs(InputGroupAddon, { align: "inline-end", children: [inputValue ? clearButton : null, calendarButton] })] }) }), _jsx(PopoverContent, { className: "twa:w-auto twa:p-0", align: "start", sideOffset: 8, children: _jsx("div", { ref: popupRefCallback, tabIndex: -1, role: "region", "aria-label": "Calendar", className: cn('twa:relative twa:isolate twa:overflow-visible twa:outline-none twa:rounded-standard', targetOwn.focusWithinOutline), onKeyDown: () => {
192
197
  }, children: _jsx(DatepickerSelectPortalContext.Provider, { value: selectPortalRoot, children: _jsx(Calendar, { fixedWeeks: true, autoFocus: !!autoFocus, showWeekNumber: showWeekNumber, showOutsideDays: showOutsideDays, mode: "single", captionLayout: "dropdown", month: month && !isNaN(month.getTime()) ? month : new Date(), selected: value, onMonthChange: setMonth, onSelect: updateValue, startMonth: START_MONTH, endMonth: END_MONTH, locale: dateProps.locale, components: captionComponents, footer: _jsx(Flex, { justifyContent: "space-between", className: "twa:mt-2", flexWrap: 'wrap', children: footerButtons }) }) }) }) })] }));
193
198
  });
@@ -30,10 +30,10 @@ const NewSelect = (props) => {
30
30
  ];
31
31
  allItems = props.items;
32
32
  }
33
- const maxLabelLength = allItems.reduce((max, item) => Math.max(max, typeof item.label === 'string'
34
- ? item.label.length
35
- : typeof item.tooltip === 'string'
36
- ? item.tooltip.length
33
+ const maxLabelLength = allItems.reduce((max, item) => Math.max(max, typeof item.label === 'string' || typeof item.label === 'number'
34
+ ? String(item.label).length
35
+ : typeof item.tooltip === 'string' || typeof item.tooltip === 'number'
36
+ ? String(item.tooltip).length
37
37
  : 0), 0);
38
38
  const contentStyle = {
39
39
  width: `max(${maxLabelLength + (props.extraWidthChars ?? 3)}ch, calc(var(--ab-base-space) * 20), var(--anchor-width))`,
@@ -362,6 +362,7 @@ export function TreeDropdown(props) {
362
362
  return defaultLabel;
363
363
  },
364
364
  filter,
365
+ inputValue: searchQuery,
365
366
  onInputValueChange: handleSearchInputChange,
366
367
  onOpenChange: (open) => {
367
368
  if (!open) {
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ export declare const WINDOW_MODAL_VIEWPORT_EDGE_OFFSET = 10;
2
3
  export type WindowModalSize = {
3
4
  width: number;
4
5
  height: number;
@@ -7,6 +8,11 @@ export type WindowModalPosition = {
7
8
  x: number;
8
9
  y: number;
9
10
  };
11
+ export declare const clampWindowModalToViewport: (position: WindowModalPosition, size: WindowModalSize, minWidth?: number, minHeight?: number, edgeOffset?: number) => {
12
+ position: WindowModalPosition;
13
+ size: WindowModalSize;
14
+ };
15
+ export declare const clampWindowModalPositionToViewport: (position: WindowModalPosition, size: WindowModalSize, edgeOffset?: number) => WindowModalPosition;
10
16
  export interface WindowModalProps {
11
17
  handleSelector: string;
12
18
  size: WindowModalSize;
@@ -5,6 +5,7 @@ import useDraggable from '../utils/useDraggable';
5
5
  import { isBrowserDocumentAvailable } from '../../View/UIHelper';
6
6
  import { createPortal } from 'react-dom';
7
7
  import { useStacking } from './useStacking';
8
+ import { clamp } from '../../Utilities/Extensions/NumberExtensions';
8
9
  let portalElement;
9
10
  const ensurePortalElement = () => {
10
11
  if (!isBrowserDocumentAvailable()) {
@@ -17,7 +18,55 @@ const ensurePortalElement = () => {
17
18
  portalElement.classList.add('ab-cmp-modal-window');
18
19
  document.body.appendChild(portalElement);
19
20
  };
20
- const normalizeTopLeft = (position) => (position < 0 ? 0 : position);
21
+ const normalizeTopLeft = (position, edgeOffset) => position < edgeOffset ? edgeOffset : position;
22
+ export const WINDOW_MODAL_VIEWPORT_EDGE_OFFSET = 10;
23
+ const getViewportSize = () => {
24
+ if (!isBrowserDocumentAvailable()) {
25
+ return { width: Infinity, height: Infinity };
26
+ }
27
+ return { width: window.innerWidth, height: window.innerHeight };
28
+ };
29
+ const parsePxConstraint = (value) => {
30
+ if (!value) {
31
+ return undefined;
32
+ }
33
+ const match = /^(-?\d+(?:\.\d+)?)px$/.exec(value.trim());
34
+ if (!match) {
35
+ return undefined;
36
+ }
37
+ const parsed = Number(match[1]);
38
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
39
+ };
40
+ const readDomSizeConstraints = (element) => {
41
+ if (!element || !isBrowserDocumentAvailable()) {
42
+ return {};
43
+ }
44
+ const computed = window.getComputedStyle(element);
45
+ return {
46
+ minWidth: parsePxConstraint(computed.minWidth),
47
+ minHeight: parsePxConstraint(computed.minHeight),
48
+ maxWidth: parsePxConstraint(computed.maxWidth),
49
+ maxHeight: parsePxConstraint(computed.maxHeight),
50
+ };
51
+ };
52
+ export const clampWindowModalToViewport = (position, size, minWidth = 0, minHeight = 0, edgeOffset = WINDOW_MODAL_VIEWPORT_EDGE_OFFSET) => {
53
+ const { width: viewportWidth, height: viewportHeight } = getViewportSize();
54
+ const width = clamp(size.width, minWidth, Math.max(minWidth, viewportWidth - 2 * edgeOffset));
55
+ const height = clamp(size.height, minHeight, Math.max(minHeight, viewportHeight - 2 * edgeOffset));
56
+ const x = clamp(position.x, edgeOffset, Math.max(edgeOffset, viewportWidth - edgeOffset - width));
57
+ const y = clamp(position.y, edgeOffset, Math.max(edgeOffset, viewportHeight - edgeOffset - height));
58
+ return {
59
+ position: { x, y },
60
+ size: { width, height },
61
+ };
62
+ };
63
+ export const clampWindowModalPositionToViewport = (position, size, edgeOffset = WINDOW_MODAL_VIEWPORT_EDGE_OFFSET) => {
64
+ const { width: viewportWidth, height: viewportHeight } = getViewportSize();
65
+ return {
66
+ x: clamp(position.x, edgeOffset, Math.max(edgeOffset, viewportWidth - edgeOffset - size.width)),
67
+ y: clamp(position.y, edgeOffset, Math.max(edgeOffset, viewportHeight - edgeOffset - size.height)),
68
+ };
69
+ };
21
70
  const getResizePositionDelta = (direction, delta) => {
22
71
  const positionDelta = { x: 0, y: 0 };
23
72
  const left = -delta.width;
@@ -38,16 +87,126 @@ const getResizePositionDelta = (direction, delta) => {
38
87
  }
39
88
  return null;
40
89
  };
90
+ const resizeDirectionAffects = (direction) => ({
91
+ left: ['left', 'topLeft', 'bottomLeft'].includes(direction),
92
+ top: ['top', 'topLeft', 'topRight'].includes(direction),
93
+ right: ['right', 'topRight', 'bottomRight'].includes(direction),
94
+ bottom: ['bottom', 'bottomLeft', 'bottomRight'].includes(direction),
95
+ });
96
+ const getClampedResizeState = (direction, delta, basePosition, baseSize, minWidth, minHeight, maxWidth, maxHeight, edgeOffset = WINDOW_MODAL_VIEWPORT_EDGE_OFFSET) => {
97
+ const { width: viewportWidth, height: viewportHeight } = getViewportSize();
98
+ const affects = resizeDirectionAffects(direction);
99
+ let deltaWidth = delta.width;
100
+ let deltaHeight = delta.height;
101
+ if (affects.left) {
102
+ deltaWidth = Math.min(deltaWidth, Math.max(0, basePosition.x - edgeOffset));
103
+ }
104
+ if (affects.top) {
105
+ deltaHeight = Math.min(deltaHeight, Math.max(0, basePosition.y - edgeOffset));
106
+ }
107
+ if (affects.right) {
108
+ deltaWidth = Math.min(deltaWidth, viewportWidth - edgeOffset - basePosition.x - baseSize.width);
109
+ }
110
+ if (affects.bottom) {
111
+ deltaHeight = Math.min(deltaHeight, viewportHeight - edgeOffset - basePosition.y - baseSize.height);
112
+ }
113
+ if (affects.left || affects.right) {
114
+ const maxDeltaWidth = maxWidth != null ? maxWidth - baseSize.width : Infinity;
115
+ const minDeltaWidth = minWidth - baseSize.width;
116
+ if (minDeltaWidth <= maxDeltaWidth) {
117
+ deltaWidth = clamp(deltaWidth, minDeltaWidth, maxDeltaWidth);
118
+ }
119
+ }
120
+ if (affects.top || affects.bottom) {
121
+ const maxDeltaHeight = maxHeight != null ? maxHeight - baseSize.height : Infinity;
122
+ const minDeltaHeight = minHeight - baseSize.height;
123
+ if (minDeltaHeight <= maxDeltaHeight) {
124
+ deltaHeight = clamp(deltaHeight, minDeltaHeight, maxDeltaHeight);
125
+ }
126
+ }
127
+ const adjustedDelta = { width: deltaWidth, height: deltaHeight };
128
+ const positionDelta = getResizePositionDelta(direction, adjustedDelta);
129
+ const newPosition = {
130
+ x: basePosition.x + (positionDelta?.x ?? 0),
131
+ y: basePosition.y + (positionDelta?.y ?? 0),
132
+ };
133
+ const newSize = {
134
+ width: baseSize.width + deltaWidth,
135
+ height: baseSize.height + deltaHeight,
136
+ };
137
+ const clamped = clampWindowModalToViewport(newPosition, newSize, minWidth, minHeight, edgeOffset);
138
+ return {
139
+ ...clamped,
140
+ positionDelta: positionDelta
141
+ ? {
142
+ x: clamped.position.x - basePosition.x,
143
+ y: clamped.position.y - basePosition.y,
144
+ }
145
+ : null,
146
+ };
147
+ };
148
+ const RESIZE_HANDLE_CLASS_PREFIX = 'ab-WindowModal__resize-handle';
149
+ const RESIZE_EDGE_HANDLES = ['left', 'right', 'top', 'bottom'];
150
+ const RESIZE_HANDLE_CLASSES = {
151
+ left: `${RESIZE_HANDLE_CLASS_PREFIX}--left`,
152
+ right: `${RESIZE_HANDLE_CLASS_PREFIX}--right`,
153
+ top: `${RESIZE_HANDLE_CLASS_PREFIX}--top`,
154
+ bottom: `${RESIZE_HANDLE_CLASS_PREFIX}--bottom`,
155
+ };
156
+ const AT_CONSTRAINT_EPSILON = 0.5;
157
+ const GROW_CURSOR = {
158
+ left: 'w-resize',
159
+ right: 'e-resize',
160
+ top: 'n-resize',
161
+ bottom: 's-resize',
162
+ };
163
+ const SHRINK_CURSOR = {
164
+ left: 'e-resize',
165
+ right: 'w-resize',
166
+ top: 's-resize',
167
+ bottom: 'n-resize',
168
+ };
169
+ const getEdgeResizeCursor = (edge, size, constraints) => {
170
+ const isHorizontal = edge === 'left' || edge === 'right';
171
+ const value = isHorizontal ? size.width : size.height;
172
+ const min = isHorizontal ? constraints.minWidth : constraints.minHeight;
173
+ const max = isHorizontal ? constraints.maxWidth : constraints.maxHeight;
174
+ const atMax = max != null && value >= max - AT_CONSTRAINT_EPSILON;
175
+ const atMin = value <= min + AT_CONSTRAINT_EPSILON;
176
+ if (atMax && !atMin) {
177
+ return SHRINK_CURSOR[edge];
178
+ }
179
+ if (atMin && !atMax) {
180
+ return GROW_CURSOR[edge];
181
+ }
182
+ return isHorizontal ? 'ew-resize' : 'ns-resize';
183
+ };
184
+ const updateResizeHandleCursors = (container, size, constraints) => {
185
+ if (!container) {
186
+ return;
187
+ }
188
+ RESIZE_EDGE_HANDLES.forEach((edge) => {
189
+ const handle = container.querySelector(`.${RESIZE_HANDLE_CLASSES[edge]}`);
190
+ if (handle) {
191
+ handle.style.cursor = getEdgeResizeCursor(edge, size, constraints);
192
+ }
193
+ });
194
+ };
41
195
  export const WindowModal = (props) => {
42
196
  ensurePortalElement();
43
197
  const positionDeltaRef = React.useRef(null);
198
+ const lastClampedRef = React.useRef(null);
44
199
  const normalizedPosition = {
45
- x: normalizeTopLeft(props.position?.x),
46
- y: normalizeTopLeft(props.position?.y),
200
+ x: normalizeTopLeft(props.position?.x ?? 0, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET),
201
+ y: normalizeTopLeft(props.position?.y ?? 0, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET),
47
202
  };
48
203
  const positionRef = React.useRef(normalizedPosition);
204
+ const sizeRef = React.useRef(props.size);
49
205
  const stacking = useStacking();
50
206
  positionRef.current = normalizedPosition;
207
+ sizeRef.current = props.size;
208
+ const minWidth = props.minWidth ?? 0;
209
+ const minHeight = props.minHeight ?? 0;
51
210
  const style = {
52
211
  zIndex: stacking.zIndex,
53
212
  position: 'absolute',
@@ -59,41 +218,83 @@ export const WindowModal = (props) => {
59
218
  x: positionRef.current.x + dx,
60
219
  y: positionRef.current.y + dy,
61
220
  };
62
- props.onChange({
63
- position: newPosition,
64
- size: props.size,
65
- });
221
+ const size = sizeRef.current;
222
+ const position = clampWindowModalPositionToViewport(newPosition, size, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET);
223
+ props.onChange({ position, size });
66
224
  };
67
225
  const { targetRef, applyTransform } = useDraggable({
68
226
  handleSelector: props.handleSelector,
69
227
  onDrop: handleDrop,
228
+ edgeOffset: WINDOW_MODAL_VIEWPORT_EDGE_OFFSET,
70
229
  });
71
- const handleResizeStop = (event, direction, elementRef, delta) => {
72
- let newPosition = normalizedPosition;
73
- if (positionDeltaRef.current) {
74
- newPosition = {
75
- x: normalizedPosition.x + positionDeltaRef.current.x,
76
- y: normalizedPosition.y + positionDeltaRef.current.y,
77
- };
78
- positionDeltaRef.current = null;
79
- applyTransform(0, 0);
80
- }
81
- const newSize = {
82
- width: props.size.width + delta.width,
83
- height: props.size.height + delta.height,
230
+ const constraintsRef = React.useRef({ minWidth, minHeight });
231
+ const getContentElement = () => {
232
+ const resizableEl = targetRef.current?.firstElementChild;
233
+ return resizableEl?.firstElementChild ?? null;
234
+ };
235
+ const computeEffectiveConstraints = () => {
236
+ const dom = readDomSizeConstraints(getContentElement());
237
+ return {
238
+ minWidth: Math.max(minWidth, dom.minWidth ?? 0),
239
+ minHeight: Math.max(minHeight, dom.minHeight ?? 0),
240
+ maxWidth: dom.maxWidth,
241
+ maxHeight: dom.maxHeight,
84
242
  };
85
- props.onChange({
86
- position: newPosition,
87
- size: newSize,
88
- });
89
243
  };
90
- const handleResize = React.useCallback((event, direction, elementRef, delta) => {
91
- const positionDelta = getResizePositionDelta(direction, delta);
92
- if (positionDelta) {
93
- positionDeltaRef.current = positionDelta;
94
- applyTransform(positionDelta.x, positionDelta.y);
244
+ const handleResizeStart = () => {
245
+ constraintsRef.current = computeEffectiveConstraints();
246
+ updateResizeHandleCursors(targetRef.current, sizeRef.current, constraintsRef.current);
247
+ };
248
+ const handleResize = (_event, direction, elementRef, delta) => {
249
+ const constraints = constraintsRef.current;
250
+ const clamped = getClampedResizeState(direction, delta, positionRef.current, sizeRef.current, constraints.minWidth, constraints.minHeight, constraints.maxWidth, constraints.maxHeight);
251
+ lastClampedRef.current = clamped;
252
+ if (clamped.positionDelta) {
253
+ positionDeltaRef.current = clamped.positionDelta;
254
+ applyTransform(clamped.positionDelta.x, clamped.positionDelta.y);
255
+ }
256
+ elementRef.style.width = `${clamped.size.width}px`;
257
+ elementRef.style.height = `${clamped.size.height}px`;
258
+ updateResizeHandleCursors(targetRef.current, clamped.size, constraints);
259
+ if (isBrowserDocumentAvailable() &&
260
+ RESIZE_EDGE_HANDLES.includes(direction)) {
261
+ document.body.style.cursor = getEdgeResizeCursor(direction, clamped.size, constraints);
95
262
  }
96
- }, []);
263
+ };
264
+ React.useEffect(() => {
265
+ updateResizeHandleCursors(targetRef.current, sizeRef.current, computeEffectiveConstraints());
266
+ }, [props.size.width, props.size.height, minWidth, minHeight]);
267
+ const handleResizeStop = () => {
268
+ applyTransform(0, 0);
269
+ positionDeltaRef.current = null;
270
+ if (isBrowserDocumentAvailable()) {
271
+ document.body.style.cursor = '';
272
+ }
273
+ const clamped = lastClampedRef.current ??
274
+ clampWindowModalToViewport(positionRef.current, sizeRef.current, minWidth, minHeight, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET);
275
+ lastClampedRef.current = null;
276
+ props.onChange(clamped);
277
+ };
278
+ const onChangeRef = React.useRef(props.onChange);
279
+ onChangeRef.current = props.onChange;
280
+ React.useEffect(() => {
281
+ if (!isBrowserDocumentAvailable()) {
282
+ return;
283
+ }
284
+ const handleViewportResize = () => {
285
+ const current = positionRef.current;
286
+ const currentSize = sizeRef.current;
287
+ const clamped = clampWindowModalToViewport(current, currentSize, minWidth, minHeight, WINDOW_MODAL_VIEWPORT_EDGE_OFFSET);
288
+ if (clamped.position.x !== current.x ||
289
+ clamped.position.y !== current.y ||
290
+ clamped.size.width !== currentSize.width ||
291
+ clamped.size.height !== currentSize.height) {
292
+ onChangeRef.current(clamped);
293
+ }
294
+ };
295
+ window.addEventListener('resize', handleViewportResize);
296
+ return () => window.removeEventListener('resize', handleViewportResize);
297
+ }, [minWidth, minHeight]);
97
298
  const ResizableCmp = Resizable;
98
299
  const handleMouseDown = (event) => {
99
300
  const node = targetRef.current;
@@ -102,7 +303,7 @@ export const WindowModal = (props) => {
102
303
  }
103
304
  stacking.bringInFront();
104
305
  };
105
- return createPortal(_jsx("div", { style: style, ref: targetRef, onMouseDown: handleMouseDown, children: _jsx(ResizableCmp, { minWidth: props.minWidth, minHeight: props.minHeight, onResizeStop: handleResizeStop, onResize: handleResize, bounds: "window", defaultSize: {
306
+ return createPortal(_jsx("div", { style: style, ref: targetRef, onMouseDown: handleMouseDown, children: _jsx(ResizableCmp, { minWidth: minWidth, minHeight: minHeight, onResizeStart: handleResizeStart, onResizeStop: handleResizeStop, onResize: handleResize, handleClasses: RESIZE_HANDLE_CLASSES, size: {
106
307
  width: props.size.width,
107
308
  height: props.size.height,
108
309
  }, children: props.children }) }), portalElement);
@@ -26,28 +26,58 @@ const ComboboxInput = React.forwardRef(function ComboboxInput({ className, child
26
26
  return (_jsxs(InputGroup, { ref: groupRef, className: cn('twa:w-auto', className), "aria-label": ariaLabel, children: [_jsx(ComboboxPrimitive.Input, { ref: ref, render: _jsx(InputGroupInput, { disabled: disabled }), ...rest }), _jsxs(InputGroupAddon, { align: "inline-end", children: [isLoading && (_jsx(Loader2Icon, { className: "twa:size-4 twa:text-muted-foreground twa:animate-spin" })), showTrigger && (_jsx(InputGroupButton, { size: "icon-xs", variant: "ghost", render: _jsx(ComboboxTrigger, {}), "data-slot": "combobox-toggle", className: "twa:group-has-data-[slot=combobox-clear]/input-group:hidden twa:data-pressed:bg-transparent twa:min-h-auto!", disabled: disabled })), showClear && _jsx(ComboboxClear, { disabled: disabled })] }), children] }));
27
27
  });
28
28
  function ComboboxContent({ className, side = 'bottom', sideOffset = 6, align = 'start', alignOffset = 0, anchor, resizable = false, contentRef, style, ...props }) {
29
- const sizeRef = React.useRef(null);
30
- const popupElRef = React.useRef(null);
29
+ const [userSize, setUserSize] = React.useState(null);
30
+ const styleObject = typeof style === 'function' ? undefined : style;
31
+ const expectedSizeRef = React.useRef({});
32
+ expectedSizeRef.current = {
33
+ width: userSize?.width ?? styleObject?.width,
34
+ height: userSize?.height ?? styleObject?.height,
35
+ };
36
+ const styleObserverRef = React.useRef(null);
37
+ const pendingUserSizeRef = React.useRef(null);
38
+ const pinTimerRef = React.useRef(null);
39
+ const flushPendingUserSize = React.useCallback(() => {
40
+ if (pinTimerRef.current) {
41
+ clearTimeout(pinTimerRef.current);
42
+ pinTimerRef.current = null;
43
+ }
44
+ if (pendingUserSizeRef.current) {
45
+ setUserSize(pendingUserSizeRef.current);
46
+ pendingUserSizeRef.current = null;
47
+ }
48
+ }, []);
31
49
  const popupRef = React.useCallback((el) => {
32
- if (!el && popupElRef.current && resizable) {
33
- const prev = popupElRef.current;
34
- const w = prev.style.width;
35
- const h = prev.style.height;
36
- if (w && h) {
37
- sizeRef.current = { width: w, height: h };
38
- }
50
+ styleObserverRef.current?.disconnect();
51
+ styleObserverRef.current = null;
52
+ if (!el) {
53
+ flushPendingUserSize();
39
54
  }
40
- popupElRef.current = el;
41
55
  contentRef?.(el);
42
- if (el && resizable && sizeRef.current) {
43
- el.style.width = sizeRef.current.width;
44
- el.style.height = sizeRef.current.height;
56
+ if (el && resizable) {
57
+ const observer = new MutationObserver(() => {
58
+ const { width, height } = el.style;
59
+ const expected = expectedSizeRef.current;
60
+ const isUserSize = (actual, expectedValue) => !!actual && actual !== expectedValue && actual.endsWith('px');
61
+ if (!isUserSize(width, expected.width) && !isUserSize(height, expected.height)) {
62
+ return;
63
+ }
64
+ pendingUserSizeRef.current = {
65
+ width: width || `${el.offsetWidth}px`,
66
+ height: height || `${el.offsetHeight}px`,
67
+ };
68
+ if (pinTimerRef.current) {
69
+ clearTimeout(pinTimerRef.current);
70
+ }
71
+ pinTimerRef.current = setTimeout(flushPendingUserSize, 150);
72
+ });
73
+ observer.observe(el, { attributes: true, attributeFilter: ['style'] });
74
+ styleObserverRef.current = observer;
45
75
  }
46
- }, [resizable, contentRef]);
76
+ }, [resizable, contentRef, flushPendingUserSize]);
47
77
  const resizableStyle = resizable
48
78
  ? { resize: 'both' }
49
79
  : undefined;
50
- return (_jsx(ComboboxPrimitive.Portal, { children: _jsx(ComboboxPrimitive.Positioner, { side: side, sideOffset: sideOffset, align: align, alignOffset: alignOffset, anchor: anchor, className: `twa:isolate ${POPUP_Z_INDEX}`, children: _jsx(ComboboxPrimitive.Popup, { ref: popupRef, "data-slot": "combobox-content", "data-chips": !!anchor, "data-resizable": resizable || undefined, style: { ...resizableStyle, ...style }, className: cn('twa:group/combobox-content twa:relative twa:origin-(--transform-origin) twa:overflow-hidden twa:rounded-input twa:bg-popover twa:text-popover-foreground twa:shadow-md twa:ring-1 twa:ring-foreground/10 twa:duration-100 twa:data-[side=bottom]:slide-in-from-top-2 twa:data-[side=inline-end]:slide-in-from-left-2 twa:data-[side=inline-start]:slide-in-from-right-2 twa:data-[side=left]:slide-in-from-right-2 twa:data-[side=right]:slide-in-from-left-2 twa:data-[side=top]:slide-in-from-bottom-2 twa:*:data-[slot=input-group]:m-1 twa:*:data-[slot=input-group]:mb-0 twa:*:data-[slot=input-group]:h-8 twa:*:data-[slot=input-group]:border-input/30 twa:*:data-[slot=input-group]:bg-input/30 twa:*:data-[slot=input-group]:shadow-none twa:data-open:animate-in twa:data-open:fade-in-0 twa:data-open:zoom-in-95 twa:data-closed:animate-out twa:data-closed:fade-out-0 twa:data-closed:zoom-out-95', 'twa:max-h-[min(var(--available-height),80vh)]', 'twa:max-w-[min(var(--available-width),80vw)]', 'twa:min-w-[min(calc(var(--anchor-width)+--spacing(7)),var(--available-width),80vw)]', resizable ? 'twa:flex twa:flex-col twa:min-h-24' : 'twa:w-(--anchor-width)', className), ...props }) }) }));
80
+ return (_jsx(ComboboxPrimitive.Portal, { children: _jsx(ComboboxPrimitive.Positioner, { side: side, sideOffset: sideOffset, align: align, alignOffset: alignOffset, anchor: anchor, className: `twa:isolate ${POPUP_Z_INDEX}`, children: _jsx(ComboboxPrimitive.Popup, { ref: popupRef, "data-slot": "combobox-content", "data-chips": !!anchor, "data-resizable": resizable || undefined, style: { ...resizableStyle, ...styleObject, ...userSize }, className: cn('twa:group/combobox-content twa:relative twa:origin-(--transform-origin) twa:overflow-hidden twa:rounded-input twa:bg-popover twa:text-popover-foreground twa:shadow-md twa:ring-1 twa:ring-foreground/10 twa:duration-100 twa:data-[side=bottom]:slide-in-from-top-2 twa:data-[side=inline-end]:slide-in-from-left-2 twa:data-[side=inline-start]:slide-in-from-right-2 twa:data-[side=left]:slide-in-from-right-2 twa:data-[side=right]:slide-in-from-left-2 twa:data-[side=top]:slide-in-from-bottom-2 twa:*:data-[slot=input-group]:m-1 twa:*:data-[slot=input-group]:mb-0 twa:*:data-[slot=input-group]:h-8 twa:*:data-[slot=input-group]:border-input/30 twa:*:data-[slot=input-group]:bg-input/30 twa:*:data-[slot=input-group]:shadow-none twa:data-open:animate-in twa:data-open:fade-in-0 twa:data-open:zoom-in-95 twa:data-closed:animate-out twa:data-closed:fade-out-0 twa:data-closed:zoom-out-95', 'twa:max-h-[min(var(--available-height,80vh),80vh)]', 'twa:max-w-[min(var(--available-width,80vw),80vw)]', 'twa:min-w-[min(calc(var(--anchor-width)+--spacing(7)),var(--available-width),80vw)]', resizable ? 'twa:flex twa:flex-col twa:min-h-24' : 'twa:w-(--anchor-width)', className), ...props }) }) }));
51
81
  }
52
82
  const ComboboxList = React.forwardRef(function ComboboxList({ className, ...props }, ref) {
53
83
  return (_jsx(ComboboxPrimitive.List, { "data-slot": "combobox-list", className: cn('twa:no-scrollbar twa:max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] twa:scroll-py-1 twa:overflow-y-auto twa:overscroll-contain twa:p-1 twa:data-empty:p-0', RESIZABLE_LIST_CLASS, className), ref: ref, ...props }));
@@ -1,8 +1,10 @@
1
- export default function useDraggable({ onMove, onDrop, getBoundingRect, handleSelector, }: {
1
+ export default function useDraggable({ onMove, onDrop, getBoundingRect, handleSelector, edgeOffset, }: {
2
2
  onMove?: (event: MouseEvent) => void;
3
3
  onDrop?: (dx: number, dy: number) => void;
4
4
  getBoundingRect?: () => DOMRect;
5
5
  handleSelector?: string;
6
+ /** Minimum distance to keep from the constrain region edges while dragging. */
7
+ edgeOffset?: number;
6
8
  }): {
7
9
  handleRef: (newNode: HTMLElement) => void;
8
10
  targetRef: import("react").RefObject<HTMLElement>;