@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/index.css +164 -341
- package/package.json +1 -1
- package/src/View/Components/AdaptableInput/AdaptableDateInlineInput.js +1 -1
- package/src/View/Components/AdaptableInput/index.d.ts +1 -0
- package/src/View/Components/AdaptableInput/index.js +2 -2
- package/src/View/Components/ColumnFilter/ColumnFilter.js +1 -1
- package/src/View/Components/ColumnFilter/components/ColumnFilterInput.d.ts +1 -2
- package/src/View/Components/ColumnFilter/components/FloatingFilterInputList.js +3 -3
- package/src/components/Combobox/VirtualizedList.js +7 -1
- package/src/components/Combobox/comboboxUtils.d.ts +8 -0
- package/src/components/Combobox/index.js +9 -7
- package/src/components/Datepicker/index.js +5 -0
- package/src/components/NewSelect/index.js +4 -4
- package/src/components/Tree/TreeDropdown/index.js +1 -0
- package/src/components/WindowModal/WindowModal.d.ts +6 -0
- package/src/components/WindowModal/WindowModal.js +232 -31
- package/src/components/ui/combobox.js +45 -15
- package/src/components/utils/useDraggable.d.ts +3 -1
- package/src/components/utils/useDraggable.js +67 -44
- package/src/env.js +2 -2
- package/tsconfig.esm.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -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-
|
|
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
|
|
27
|
+
inputProps?: AdaptableInputProps;
|
|
29
28
|
} & ColumnFilterEditorChangeProps) => React.JSX.Element;
|
|
30
29
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
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(
|
|
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
|
-
|
|
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 +
|
|
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' &&
|
|
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))`,
|
|
@@ -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) =>
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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:
|
|
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
|
|
30
|
-
const
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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, ...
|
|
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>;
|