@db-ux/react-core-components 2.1.2 → 2.2.1

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.
@@ -2,7 +2,7 @@
2
2
  import * as React from "react";
3
3
  import { filterPassingProps, getRootProps } from "../../utils/react";
4
4
  import { useState, useRef, useEffect, forwardRef } from "react";
5
- import { cls, delay, getBoolean, getBooleanAsString, getHideProp, getSearchInput, handleDataOutside, hasVoiceOver, stringPropVisible, uuid, } from "../../utils";
5
+ import { cls, delay, getBoolean, getBooleanAsString, getHideProp, getOptionKey, getSearchInput, hasVoiceOver, stringPropVisible, uuid, } from "../../utils";
6
6
  import { DEFAULT_CLOSE_BUTTON, DEFAULT_INVALID_MESSAGE, DEFAULT_INVALID_MESSAGE_ID_SUFFIX, DEFAULT_LABEL, DEFAULT_LABEL_ID_SUFFIX, DEFAULT_MESSAGE, DEFAULT_MESSAGE_ID_SUFFIX, DEFAULT_PLACEHOLDER_ID_SUFFIX, DEFAULT_REMOVE, DEFAULT_SELECT_ID_SUFFIX, DEFAULT_SELECTED, DEFAULT_VALID_MESSAGE, DEFAULT_VALID_MESSAGE_ID_SUFFIX, } from "../../shared/constants";
7
7
  import DBCustomSelectList from "../custom-select-list/custom-select-list";
8
8
  import DBCustomSelectListItem from "../custom-select-list-item/custom-select-list-item";
@@ -13,6 +13,8 @@ import DBButton from "../button/button";
13
13
  import DBTooltip from "../tooltip/tooltip";
14
14
  import DBInput from "../input/input";
15
15
  import { DocumentClickListener } from "../../utils/document-click-listener";
16
+ import { DocumentScrollListener } from "../../utils/document-scroll-listener";
17
+ import { handleFixedDropdown } from "../../utils/floating-components";
16
18
  function DBCustomSelectFn(props, component) {
17
19
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
18
20
  props = Object.assign({ clearSelectionText: "Clear selection", showClearSelection: true }, props);
@@ -45,6 +47,15 @@ function DBCustomSelectFn(props, component) {
45
47
  const [_hasNoOptions, set_hasNoOptions] = useState(() => false);
46
48
  const [_documentClickListenerCallbackId, set_documentClickListenerCallbackId,] = useState(() => undefined);
47
49
  const [_internalChangeTimestamp, set_internalChangeTimestamp] = useState(() => 0);
50
+ const [_documentScrollListenerCallbackId, set_documentScrollListenerCallbackId,] = useState(() => undefined);
51
+ const [_observer, set_observer] = useState(() => undefined);
52
+ function handleDocumentScroll(event) {
53
+ var _a, _b;
54
+ if (((_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a.contains) &&
55
+ ((_b = event === null || event === void 0 ? void 0 : event.target) === null || _b === void 0 ? void 0 : _b.contains(detailsRef.current))) {
56
+ handleAutoPlacement();
57
+ }
58
+ }
48
59
  const [_searchValue, set_searchValue] = useState(() => undefined);
49
60
  function hasValidState() {
50
61
  var _a;
@@ -93,7 +104,9 @@ function DBCustomSelectFn(props, component) {
93
104
  }
94
105
  if (event.target.open) {
95
106
  set_documentClickListenerCallbackId(new DocumentClickListener().addCallback((event) => handleDocumentClose(event)));
107
+ set_documentScrollListenerCallbackId(new DocumentScrollListener().addCallback((event) => handleDocumentScroll(event)));
96
108
  handleAutoPlacement();
109
+ _observer === null || _observer === void 0 ? void 0 : _observer.observe(detailsRef.current);
97
110
  if (!event.target.dataset.test) {
98
111
  // We need this workaround for snapshot testing
99
112
  handleOpenByKeyboardFocus();
@@ -103,6 +116,10 @@ function DBCustomSelectFn(props, component) {
103
116
  if (_documentClickListenerCallbackId) {
104
117
  new DocumentClickListener().removeCallback(_documentClickListenerCallbackId);
105
118
  }
119
+ if (_documentScrollListenerCallbackId) {
120
+ new DocumentScrollListener().removeCallback(_documentScrollListenerCallbackId);
121
+ }
122
+ _observer === null || _observer === void 0 ? void 0 : _observer.unobserve(detailsRef.current);
106
123
  }
107
124
  }
108
125
  function getNativeSelectValue() {
@@ -136,10 +153,6 @@ function DBCustomSelectFn(props, component) {
136
153
  }
137
154
  return false;
138
155
  }
139
- function getOptionKey(option) {
140
- var _a, _b;
141
- return ((_b = (_a = option.id) !== null && _a !== void 0 ? _a : option.value) !== null && _b !== void 0 ? _b : uuid()).toString();
142
- }
143
156
  function getTagRemoveLabel(index) {
144
157
  if (props.removeTagsTexts && props.removeTagsTexts.length > index) {
145
158
  return props.removeTagsTexts.at(index);
@@ -157,8 +170,10 @@ function DBCustomSelectFn(props, component) {
157
170
  if (detailsRef.current) {
158
171
  const dropdown = detailsRef.current.querySelector("article");
159
172
  if (dropdown) {
173
+ // This is a workaround for Angular
160
174
  delay(() => {
161
- handleDataOutside(dropdown);
175
+ var _a;
176
+ handleFixedDropdown(dropdown, detailsRef.current, (_a = props.placement) !== null && _a !== void 0 ? _a : "bottom");
162
177
  }, 1);
163
178
  }
164
179
  }
@@ -414,6 +429,14 @@ function DBCustomSelectFn(props, component) {
414
429
  set_selectedLabelsId(mId + "-selected-labels");
415
430
  set_infoTextId(mId + "-info");
416
431
  set_invalidMessage(props.invalidMessage || DEFAULT_INVALID_MESSAGE);
432
+ set_observer(new IntersectionObserver((payload) => {
433
+ if (detailsRef.current) {
434
+ const entry = payload.find(({ target }) => target === detailsRef.current);
435
+ if (entry && !entry.isIntersecting && detailsRef.current.open) {
436
+ detailsRef.current.open = false;
437
+ }
438
+ }
439
+ }));
417
440
  }, []);
418
441
  useEffect(() => {
419
442
  if (detailsRef.current) {
@@ -566,13 +589,13 @@ function DBCustomSelectFn(props, component) {
566
589
  ? "above"
567
590
  : props.variant, "data-required": getBooleanAsString(props.required), "data-placement": props.placement, "data-selected-type": props.multiple ? props.selectedType : "text", "data-hide-label": getHideProp(props.showLabel), "data-icon": props.icon, "data-hide-icon": getHideProp(props.showIcon) }),
568
591
  React.createElement("label", { id: _labelId }, (_a = props.label) !== null && _a !== void 0 ? _a : DEFAULT_LABEL,
569
- React.createElement("select", { role: "none", hidden: true, id: _selectId, tabIndex: -1, ref: selectRef, form: props.form, name: props.name, multiple: getBoolean(props.multiple, "multiple"), disabled: getBoolean(props.disabled, "disabled"), required: getBoolean(props.required, "required"), onChange: (event) => satisfyReact(event) }, ((_b = props.options) === null || _b === void 0 ? void 0 : _b.length) ? (React.createElement(React.Fragment, null, (_c = props.options) === null || _c === void 0 ? void 0 : _c.map((option) => (React.createElement("option", { disabled: option.disabled, value: option.value, key: "native-select-option-" + getOptionKey(option) }, getOptionLabel(option)))))) : null)),
592
+ React.createElement("select", { role: "none", hidden: true, id: _selectId, tabIndex: -1, ref: selectRef, form: props.form, name: props.name, multiple: getBoolean(props.multiple, "multiple"), disabled: getBoolean(props.disabled, "disabled"), required: getBoolean(props.required, "required"), onChange: (event) => satisfyReact(event) }, ((_b = props.options) === null || _b === void 0 ? void 0 : _b.length) ? (React.createElement(React.Fragment, null, (_c = props.options) === null || _c === void 0 ? void 0 : _c.map((option) => (React.createElement("option", { disabled: option.disabled, value: option.value, key: getOptionKey(option, "native-select-option-") }, getOptionLabel(option)))))) : null)),
570
593
  React.createElement("details", { ref: detailsRef, open: props.open, onToggle: (event) => handleDropdownToggle(event), onKeyDown: (event) => handleKeyboardPress(event) },
571
594
  props.children,
572
595
  props.options ? (React.createElement(React.Fragment, null,
573
596
  React.createElement("summary", { className: "db-custom-select-form-field", id: _summaryId, "aria-disabled": getBooleanAsString(props.disabled), "aria-labelledby": _labelId },
574
597
  (_selectedLabels === null || _selectedLabels === void 0 ? void 0 : _selectedLabels.length) ? (React.createElement("span", { "data-visually-hidden": getBooleanAsString(props.selectedType === "tag"), id: _selectedLabelsId }, _selectedLabels)) : null,
575
- props.selectedType === "tag" ? (React.createElement("div", null, _selectedOptions === null || _selectedOptions === void 0 ? void 0 : _selectedOptions.map((option, index) => (React.createElement(DBTag, { emphasis: "strong", behavior: "removable", removeButton: getTagRemoveLabel(index), onRemove: (event) => handleTagRemove(option, event), key: "tag-" + getOptionKey(option) }, getOptionLabel(option)))))) : null),
598
+ props.selectedType === "tag" ? (React.createElement("div", null, _selectedOptions === null || _selectedOptions === void 0 ? void 0 : _selectedOptions.map((option, index) => (React.createElement(DBTag, { emphasis: "strong", behavior: "removable", removeButton: getTagRemoveLabel(index), onRemove: (event) => handleTagRemove(option, event), key: getOptionKey(option, "tag-") }, getOptionLabel(option)))))) : null),
576
599
  React.createElement(DBCustomSelectDropdown, { width: props.dropdownWidth },
577
600
  searchEnabled ? (React.createElement("div", null,
578
601
  React.createElement(DBInput, { type: "search", ref: searchInputRef, name: _id, form: _id, showLabel: false, value: _searchValue, label: (_d = props.searchLabel) !== null && _d !== void 0 ? _d : DEFAULT_LABEL, placeholder: (_e = props.searchPlaceholder) !== null && _e !== void 0 ? _e : props.searchLabel, ariaDescribedBy: _hasNoOptions || props.showLoading
@@ -585,7 +608,7 @@ function DBCustomSelectFn(props, component) {
585
608
  React.createElement("label", null,
586
609
  React.createElement("input", { type: "checkbox", value: "select-all", ref: selectAllRef, form: _id, checked: selectAllChecked, onChange: (event) => handleSelectAll(event) }),
587
610
  getSelectAllLabel())))) : null,
588
- React.createElement(DBCustomSelectList, { multiple: getBoolean(props.multiple, "multiple"), label: (_h = (_g = props.ariaListLabel) !== null && _g !== void 0 ? _g : props.label) !== null && _h !== void 0 ? _h : DEFAULT_LABEL }, _options === null || _options === void 0 ? void 0 : _options.map((option) => (React.createElement(DBCustomSelectListItem, { type: props.multiple ? "checkbox" : "radio", showDivider: option.showDivider, icon: option.icon, isGroupTitle: option.isGroupTitle, groupTitle: getOptionLabel(option), name: _id, checked: getOptionChecked(option.value), disabled: option.disabled, value: option.value, onChange: (event) => handleSelect(option.value), key: "custom-select-list-item-" + getOptionKey(option) }, !option.isGroupTitle ? (React.createElement(React.Fragment, null, getOptionLabel(option))) : null))))))),
611
+ React.createElement(DBCustomSelectList, { multiple: getBoolean(props.multiple, "multiple"), label: (_h = (_g = props.ariaListLabel) !== null && _g !== void 0 ? _g : props.label) !== null && _h !== void 0 ? _h : DEFAULT_LABEL }, _options === null || _options === void 0 ? void 0 : _options.map((option) => (React.createElement(DBCustomSelectListItem, { type: props.multiple ? "checkbox" : "radio", showDivider: option.showDivider, icon: option.icon, isGroupTitle: option.isGroupTitle, groupTitle: getOptionLabel(option), name: _id, checked: getOptionChecked(option.value), disabled: option.disabled, value: option.value, onChange: (event) => handleSelect(option.value), key: getOptionKey(option, "custom-select-list-item-") }, !option.isGroupTitle ? (React.createElement(React.Fragment, null, getOptionLabel(option))) : null))))))),
589
612
  React.createElement("div", null,
590
613
  React.createElement(DBButton, { variant: "ghost", width: "full", icon: "cross", size: "small", name: _id, form: _id, onClick: (event) => handleClose("close") }, (_j = props.mobileCloseButtonText) !== null && _j !== void 0 ? _j : DEFAULT_CLOSE_BUTTON))))) : null),
591
614
  ((_k = props.showClearSelection) !== null && _k !== void 0 ? _k : true) && (_values === null || _values === void 0 ? void 0 : _values.length) ? (React.createElement(DBButton, { icon: "cross", variant: "ghost", size: "small", noText: true, name: _id, form: _id, onClick: (event) => handleClearAll(event) },
@@ -1,4 +1,4 @@
1
- import { BaseFormProps, CloseEventState, CustomFormProps, FormMessageProps, FormState, FromValidState, GlobalProps, GlobalState, IconProps, PlacementVerticalType, PopoverState, RequiredProps, ShowIconProps, ShowLabelProps, ValidationType, WidthType } from '../../shared/model';
1
+ import { BaseFormProps, CloseEventState, CustomFormProps, DocumentScrollState, FormMessageProps, FormState, FromValidState, GlobalProps, GlobalState, IconProps, PlacementVerticalType, RequiredProps, ShowIconProps, ShowLabelProps, ValidationType, WidthType } from '../../shared/model';
2
2
  import { DBCustomSelectFormFieldDefaultProps } from '../custom-select-form-field/model';
3
3
  import { CustomSelectDropdownWidthType } from '../custom-select-dropdown/model';
4
4
  import { DBCustomSelectListItemExtraProps } from '../custom-select-list-item/model';
@@ -193,7 +193,6 @@ export type DBCustomSelectDefaultState = {
193
193
  getNativeSelectValue: () => string;
194
194
  getOptionLabel: (option: CustomSelectOptionType) => string;
195
195
  getOptionChecked: (value?: string) => boolean;
196
- getOptionKey: (option: CustomSelectOptionType) => string;
197
196
  getTagRemoveLabel: (index: number) => string;
198
197
  selectAllEnabled: boolean;
199
198
  searchEnabled: boolean;
@@ -215,5 +214,6 @@ export type DBCustomSelectDefaultState = {
215
214
  getSelectAllLabel: () => string;
216
215
  selectAllChecked: boolean;
217
216
  selectAllIndeterminate: boolean;
217
+ handleAutoPlacement: () => void;
218
218
  };
219
- export type DBCustomSelectState = DBCustomSelectDefaultState & GlobalState & FormState & FromValidState & CloseEventState & PopoverState;
219
+ export type DBCustomSelectState = DBCustomSelectDefaultState & GlobalState & FormState & FromValidState & CloseEventState & DocumentScrollState;
@@ -115,7 +115,7 @@ function DBInputFn(props, component) {
115
115
  }, [props.value]);
116
116
  return (React.createElement("div", Object.assign({}, getRootProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { className: cls("db-input", props.className), "data-variant": props.variant, "data-hide-label": getHideProp(props.showLabel), "data-hide-icon": getHideProp(props.showIcon), "data-icon": props.icon, "data-icon-after": props.iconAfter, "data-hide-icon-after": getHideProp(props.showIcon) }),
117
117
  React.createElement("label", { htmlFor: _id }, (_a = props.label) !== null && _a !== void 0 ? _a : DEFAULT_LABEL),
118
- React.createElement("input", Object.assign({ "aria-invalid": props.validation === "invalid", "data-custom-validity": props.validation, ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { id: _id, name: props.name, type: props.type || "text", placeholder: (_b = props.placeholder) !== null && _b !== void 0 ? _b : DEFAULT_PLACEHOLDER, disabled: getBoolean(props.disabled, "disabled"), required: getBoolean(props.required, "required"), step: getNumber(props.step), value: props.value, maxLength: getNumber(props.maxLength, props.maxlength), minLength: getNumber(props.minLength, props.minlength), max: getInputValue(props.max, props.type), min: getInputValue(props.min, props.type), readOnly: getBoolean(props.readOnly, "readOnly") ||
118
+ React.createElement("input", Object.assign({ "aria-invalid": props.validation === "invalid", "data-custom-validity": props.validation, "data-field-sizing": props.fieldSizing, ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { id: _id, name: props.name, type: props.type || "text", placeholder: (_b = props.placeholder) !== null && _b !== void 0 ? _b : DEFAULT_PLACEHOLDER, disabled: getBoolean(props.disabled, "disabled"), required: getBoolean(props.required, "required"), step: getNumber(props.step), value: props.value, maxLength: getNumber(props.maxLength, props.maxlength), minLength: getNumber(props.minLength, props.minlength), max: getInputValue(props.max, props.type), min: getInputValue(props.min, props.type), readOnly: getBoolean(props.readOnly, "readOnly") ||
119
119
  getBoolean(props.readonly, "readonly"), form: props.form, pattern: props.pattern, size: props.size, autoComplete: props.autocomplete, autoFocus: getBoolean(props.autofocus, "autofocus"), onInput: (event) => handleInput(event), onChange: (event) => handleChange(event), onBlur: (event) => handleBlur(event), onFocus: (event) => handleFocus(event), list: props.dataList && _dataListId, "aria-describedby": (_c = props.ariaDescribedBy) !== null && _c !== void 0 ? _c : _descByIds })),
120
120
  props.dataList ? (React.createElement("datalist", { id: _dataListId }, (_d = getDataList()) === null || _d === void 0 ? void 0 : _d.map((option) => (React.createElement("option", { key: _dataListId + "-option-" + option.value, value: option.value }, option.label))))) : null,
121
121
  props.children,
@@ -13,6 +13,5 @@ export type DBPopoverProps = DBPopoverDefaultProps & GlobalProps & SpacingProps
13
13
  export type DBPopoverDefaultState = {
14
14
  isExpanded?: boolean;
15
15
  getTrigger: () => Element | null;
16
- handleLeave: (event: any) => void;
17
16
  };
18
17
  export type DBPopoverState = DBPopoverDefaultState & GlobalState & PopoverState & InitializedState;
@@ -2,29 +2,66 @@
2
2
  import * as React from "react";
3
3
  import { filterPassingProps, getRootProps } from "../../utils/react";
4
4
  import { useState, useRef, useEffect, forwardRef } from "react";
5
- import { cls, getBooleanAsString, handleDataOutside } from "../../utils";
5
+ import { cls, delay as utilsDelay, getBooleanAsString } from "../../utils";
6
+ import { handleFixedPopover } from "../../utils/floating-components";
7
+ import { DocumentScrollListener } from "../../utils/document-scroll-listener";
6
8
  function DBPopoverFn(props, component) {
7
9
  var _a;
8
10
  const _ref = component || useRef(component);
9
11
  const [initialized, setInitialized] = useState(() => false);
10
12
  const [isExpanded, setIsExpanded] = useState(() => false);
13
+ const [_documentScrollListenerCallbackId, set_documentScrollListenerCallbackId,] = useState(() => undefined);
14
+ const [_observer, set_observer] = useState(() => undefined);
15
+ function handleEscape(event) {
16
+ if (!event || event.key === "Escape") {
17
+ // TODO: Recursive for any child
18
+ for (const child of Array.from(_ref.current.children)) {
19
+ child.blur();
20
+ }
21
+ }
22
+ }
11
23
  function handleAutoPlacement() {
12
- setIsExpanded(true);
13
24
  if (!_ref.current)
14
25
  return;
15
26
  const article = _ref.current.querySelector("article");
16
- if (!article)
17
- return;
18
- handleDataOutside(article);
27
+ if (article) {
28
+ // This is a workaround for angular
29
+ utilsDelay(() => {
30
+ var _a;
31
+ handleFixedPopover(article, _ref.current, (_a = props.placement) !== null && _a !== void 0 ? _a : "bottom");
32
+ }, 1);
33
+ }
34
+ }
35
+ function handleDocumentScroll(event) {
36
+ var _a, _b;
37
+ if (((_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a.contains) && ((_b = event === null || event === void 0 ? void 0 : event.target) === null || _b === void 0 ? void 0 : _b.contains(_ref.current))) {
38
+ handleAutoPlacement();
39
+ }
40
+ }
41
+ function handleEnter() {
42
+ setIsExpanded(true);
43
+ set_documentScrollListenerCallbackId(new DocumentScrollListener().addCallback((event) => handleDocumentScroll(event)));
44
+ handleAutoPlacement();
45
+ const child = getTrigger();
46
+ if (child) {
47
+ _observer === null || _observer === void 0 ? void 0 : _observer.observe(child);
48
+ }
19
49
  }
20
50
  function handleLeave(event) {
21
- const element = event.target;
22
- const parent = element.parentNode;
51
+ const element = event === null || event === void 0 ? void 0 : event.target;
52
+ const parent = element === null || element === void 0 ? void 0 : element.parentNode;
23
53
  if (!parent ||
24
54
  (element.parentNode.querySelector(":focus") !== element &&
25
55
  element.parentNode.querySelector(":focus-within") !== element &&
26
56
  element.parentNode.querySelector(":hover") !== element)) {
27
57
  setIsExpanded(false);
58
+ if (_documentScrollListenerCallbackId) {
59
+ new DocumentScrollListener().removeCallback(_documentScrollListenerCallbackId);
60
+ }
61
+ const child = getTrigger();
62
+ if (child) {
63
+ _observer === null || _observer === void 0 ? void 0 : _observer.unobserve(child);
64
+ }
28
65
  }
29
66
  }
30
67
  function getTrigger() {
@@ -51,11 +88,25 @@ function DBPopoverFn(props, component) {
51
88
  }, []);
52
89
  useEffect(() => {
53
90
  if (_ref.current && initialized) {
91
+ setInitialized(false);
54
92
  const child = getTrigger();
55
93
  if (child) {
56
94
  child.ariaHasPopup = "true";
57
95
  }
58
- setInitialized(false);
96
+ handleAutoPlacement();
97
+ _ref.current.addEventListener("keydown", (event) => handleEscape(event));
98
+ ["mouseenter", "focusin"].forEach((event) => {
99
+ _ref.current.addEventListener(event, () => handleEnter());
100
+ });
101
+ ["mouseleave", "focusout"].forEach((event) => {
102
+ _ref.current.addEventListener(event, () => handleLeave());
103
+ });
104
+ set_observer(new IntersectionObserver((payload) => {
105
+ const entry = payload.find(({ target }) => target === getTrigger());
106
+ if (entry && !entry.isIntersecting) {
107
+ handleEscape(false);
108
+ }
109
+ }));
59
110
  }
60
111
  }, [_ref.current, initialized]);
61
112
  useEffect(() => {
@@ -66,7 +117,7 @@ function DBPopoverFn(props, component) {
66
117
  }
67
118
  }
68
119
  }, [_ref.current, isExpanded]);
69
- return (React.createElement("div", Object.assign({ ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { id: props.id }, getRootProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { className: cls("db-popover", props.className), onFocus: (event) => handleAutoPlacement(), onBlur: (event) => handleLeave(event), onMouseEnter: (event) => handleAutoPlacement(), onMouseLeave: (event) => handleLeave(event) }),
120
+ return (React.createElement("div", Object.assign({ ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { id: props.id }, getRootProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { className: cls("db-popover", props.className) }),
70
121
  React.createElement(React.Fragment, null, props.trigger),
71
122
  React.createElement("article", { className: "db-popover-content", "data-spacing": props.spacing, "data-gap": getBooleanAsString(props.gap), "data-animation": getBooleanAsString((_a = props.animation) !== null && _a !== void 0 ? _a : true), "data-open": getBooleanAsString(props.open), "data-delay": props.delay, "data-width": props.width, "data-placement": props.placement }, props.children)));
72
123
  }
@@ -2,11 +2,11 @@
2
2
  import * as React from "react";
3
3
  import { filterPassingProps, getRootProps } from "../../utils/react";
4
4
  import { useState, useRef, useEffect, forwardRef } from "react";
5
- import { cls, delay, getBoolean, getHideProp, hasVoiceOver, stringPropVisible, uuid, } from "../../utils";
5
+ import { cls, delay, getBoolean, getHideProp, getOptionKey, hasVoiceOver, stringPropVisible, uuid, } from "../../utils";
6
6
  import { DEFAULT_INVALID_MESSAGE, DEFAULT_INVALID_MESSAGE_ID_SUFFIX, DEFAULT_LABEL, DEFAULT_MESSAGE_ID_SUFFIX, DEFAULT_PLACEHOLDER_ID_SUFFIX, DEFAULT_VALID_MESSAGE, DEFAULT_VALID_MESSAGE_ID_SUFFIX, } from "../../shared/constants";
7
7
  import DBInfotext from "../infotext/infotext";
8
8
  function DBSelectFn(props, component) {
9
- var _a, _b, _c, _d;
9
+ var _a, _b, _c, _d, _e;
10
10
  const _ref = component || useRef(component);
11
11
  const [_id, set_id] = useState(() => undefined);
12
12
  const [_messageId, set_messageId] = useState(() => undefined);
@@ -123,11 +123,11 @@ function DBSelectFn(props, component) {
123
123
  React.createElement("label", { htmlFor: _id }, (_a = props.label) !== null && _a !== void 0 ? _a : DEFAULT_LABEL),
124
124
  React.createElement("select", Object.assign({ "aria-invalid": props.validation === "invalid", "data-custom-validity": props.validation, ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { required: getBoolean(props.required, "required"), disabled: getBoolean(props.disabled, "disabled"), id: _id, name: props.name, size: props.size, value: props.value, autoComplete: props.autocomplete, multiple: props.multiple, onInput: (event) => handleInput(event), onClick: (event) => handleClick(event), onChange: (event) => handleChange(event), onBlur: (event) => handleBlur(event), onFocus: (event) => handleFocus(event), "aria-describedby": (_b = props.ariaDescribedBy) !== null && _b !== void 0 ? _b : _descByIds }),
125
125
  React.createElement("option", { hidden: true }),
126
- props.options ? (React.createElement(React.Fragment, null, (_c = props.options) === null || _c === void 0 ? void 0 : _c.map((option) => {
126
+ ((_c = props.options) === null || _c === void 0 ? void 0 : _c.length) ? (React.createElement(React.Fragment, null, (_d = props.options) === null || _d === void 0 ? void 0 : _d.map((option) => {
127
127
  var _a;
128
- return option.options ? (React.createElement("optgroup", { label: getOptionLabel(option) }, (_a = option.options) === null || _a === void 0 ? void 0 : _a.map((optgroupOption) => (React.createElement("option", { key: optgroupOption.value.toString(), value: optgroupOption.value, disabled: optgroupOption.disabled }, getOptionLabel(optgroupOption)))))) : (React.createElement("option", { value: option.value, disabled: option.disabled }, getOptionLabel(option)));
128
+ return option.options ? (React.createElement("optgroup", { label: getOptionLabel(option), key: getOptionKey(option, "select-optgroup-") }, (_a = option.options) === null || _a === void 0 ? void 0 : _a.map((optgroupOption) => (React.createElement("option", { value: optgroupOption.value, disabled: optgroupOption.disabled, key: getOptionKey(optgroupOption, "select-optgroup-option-") }, getOptionLabel(optgroupOption)))))) : (React.createElement("option", { value: option.value, disabled: option.disabled, key: getOptionKey(option, "select-option-") }, getOptionLabel(option)));
129
129
  }))) : (React.createElement(React.Fragment, null, props.children))),
130
- React.createElement("span", { id: _placeholderId }, (_d = props.placeholder) !== null && _d !== void 0 ? _d : props.label),
130
+ React.createElement("span", { id: _placeholderId }, (_e = props.placeholder) !== null && _e !== void 0 ? _e : props.label),
131
131
  stringPropVisible(props.message, props.showMessage) ? (React.createElement(DBInfotext, { size: "small", icon: props.messageIcon, id: _messageId }, props.message)) : null,
132
132
  hasValidState() ? (React.createElement(DBInfotext, { size: "small", semantic: "successful", id: _validMessageId }, props.validMessage || DEFAULT_VALID_MESSAGE)) : null,
133
133
  React.createElement(DBInfotext, { size: "small", semantic: "critical", id: _invalidMessageId }, _invalidMessage),
@@ -12,6 +12,10 @@ export type DBTextareaDefaultProps = {
12
12
  * In most browsers, textareas are resizable — you'll notice the drag handle in the right-hand corner, you can control it with this
13
13
  */
14
14
  resize?: TextareaResizeType;
15
+ /**
16
+ * Show/Hides drag handle in the right-hand corner - default: true
17
+ */
18
+ showResizer?: boolean | string;
15
19
  /**
16
20
  * The number of visible text lines for the control. If it is specified, it must be a positive integer
17
21
  */
@@ -6,7 +6,7 @@ import DBInfotext from "../infotext/infotext";
6
6
  import { cls, delay, getBoolean, getHideProp, getNumber, hasVoiceOver, stringPropVisible, uuid, } from "../../utils";
7
7
  import { DEFAULT_INVALID_MESSAGE, DEFAULT_INVALID_MESSAGE_ID_SUFFIX, DEFAULT_LABEL, DEFAULT_MESSAGE_ID_SUFFIX, DEFAULT_PLACEHOLDER, DEFAULT_ROWS, DEFAULT_VALID_MESSAGE, DEFAULT_VALID_MESSAGE_ID_SUFFIX, } from "../../shared/constants";
8
8
  function DBTextareaFn(props, component) {
9
- var _a, _b, _c;
9
+ var _a, _b, _c, _d;
10
10
  const _ref = component || useRef(component);
11
11
  const [_id, set_id] = useState(() => undefined);
12
12
  const [_messageId, set_messageId] = useState(() => undefined);
@@ -102,8 +102,8 @@ function DBTextareaFn(props, component) {
102
102
  }, [props.value]);
103
103
  return (React.createElement("div", Object.assign({}, getRootProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { className: cls("db-textarea", props.className), "data-variant": props.variant, "data-hide-label": getHideProp(props.showLabel) }),
104
104
  React.createElement("label", { htmlFor: _id }, (_a = props.label) !== null && _a !== void 0 ? _a : DEFAULT_LABEL),
105
- React.createElement("textarea", Object.assign({ "aria-invalid": props.validation === "invalid", "data-custom-validity": props.validation, ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { id: _id, "data-resize": props.resize, disabled: getBoolean(props.disabled, "disabled"), required: getBoolean(props.required, "required"), readOnly: getBoolean(props.readOnly, "readOnly") ||
106
- getBoolean(props.readonly, "readonly"), form: props.form, maxLength: getNumber(props.maxLength, props.maxlength), minLength: getNumber(props.minLength, props.minlength), name: props.name, wrap: props.wrap, spellCheck: props.spellCheck, autoComplete: props.autocomplete, onInput: (event) => handleInput(event), onChange: (event) => handleChange(event), onBlur: (event) => handleBlur(event), onFocus: (event) => handleFocus(event), value: props.value, "aria-describedby": (_b = props.ariaDescribedBy) !== null && _b !== void 0 ? _b : _descByIds, placeholder: (_c = props.placeholder) !== null && _c !== void 0 ? _c : DEFAULT_PLACEHOLDER, rows: getNumber(props.rows, DEFAULT_ROWS), cols: getNumber(props.cols) })),
105
+ React.createElement("textarea", Object.assign({ "aria-invalid": props.validation === "invalid", "data-custom-validity": props.validation, "data-field-sizing": props.fieldSizing, ref: _ref }, filterPassingProps(props, ["data-icon-variant", "data-icon-variant-before", "data-icon-variant-after", "data-icon-weight", "data-icon-weight-before", "data-icon-weight-after", "data-interactive", "data-force-mobile", "data-color", "data-container-color", "data-bg-color", "data-on-bg-color", "data-color-scheme", "data-font-size", "data-headline-size", "data-divider", "data-focus", "data-font"]), { id: _id, "data-resize": props.resize, "data-hide-resizer": getHideProp((_b = props.showResizer) !== null && _b !== void 0 ? _b : true), disabled: getBoolean(props.disabled, "disabled"), required: getBoolean(props.required, "required"), readOnly: getBoolean(props.readOnly, "readOnly") ||
106
+ getBoolean(props.readonly, "readonly"), form: props.form, maxLength: getNumber(props.maxLength, props.maxlength), minLength: getNumber(props.minLength, props.minlength), name: props.name, wrap: props.wrap, spellCheck: props.spellCheck, autoComplete: props.autocomplete, onInput: (event) => handleInput(event), onChange: (event) => handleChange(event), onBlur: (event) => handleBlur(event), onFocus: (event) => handleFocus(event), value: props.value, "aria-describedby": (_c = props.ariaDescribedBy) !== null && _c !== void 0 ? _c : _descByIds, placeholder: (_d = props.placeholder) !== null && _d !== void 0 ? _d : DEFAULT_PLACEHOLDER, rows: getNumber(props.rows, DEFAULT_ROWS), cols: getNumber(props.cols) })),
107
107
  stringPropVisible(props.message, props.showMessage) ? (React.createElement(DBInfotext, { size: "small", icon: props.messageIcon, id: _messageId }, props.message)) : null,
108
108
  hasValidState() ? (React.createElement(DBInfotext, { size: "small", semantic: "successful", id: _validMessageId }, props.validMessage || DEFAULT_VALID_MESSAGE)) : null,
109
109
  React.createElement(DBInfotext, { size: "small", semantic: "critical", id: _invalidMessageId }, _invalidMessage),
@@ -1,4 +1,4 @@
1
- import { ClickEventState, EmphasisProps, GlobalProps, GlobalState, InitializedState, PlacementProps, PopoverProps, PopoverState } from '../../shared/model';
1
+ import { ClickEventState, DocumentScrollState, EmphasisProps, GlobalProps, GlobalState, InitializedState, PlacementProps, PopoverProps, PopoverState } from '../../shared/model';
2
2
  export declare const TooltipVariantList: readonly ["description", "label"];
3
3
  export type TooltipVariantType = (typeof TooltipVariantList)[number];
4
4
  export type DBTooltipDefaultProps = {
@@ -14,5 +14,7 @@ export type DBTooltipDefaultProps = {
14
14
  variant?: TooltipVariantType;
15
15
  };
16
16
  export type DBTooltipProps = DBTooltipDefaultProps & GlobalProps & EmphasisProps & PlacementProps & PopoverProps;
17
- export type DBTooltipDefaultState = {};
18
- export type DBTooltipState = DBTooltipDefaultState & GlobalState & ClickEventState<HTMLElement> & PopoverState & InitializedState;
17
+ export type DBTooltipDefaultState = {
18
+ getParent: () => HTMLElement;
19
+ };
20
+ export type DBTooltipState = DBTooltipDefaultState & GlobalState & ClickEventState<HTMLElement> & PopoverState & InitializedState & DocumentScrollState;
@@ -2,19 +2,62 @@
2
2
  import * as React from "react";
3
3
  import { filterPassingProps, getRootProps } from "../../utils/react";
4
4
  import { useState, useRef, useEffect, forwardRef } from "react";
5
- import { cls, getBooleanAsString, handleDataOutside, uuid } from "../../utils";
5
+ import { cls, delay as utilsDelay, getBooleanAsString, uuid, } from "../../utils";
6
6
  import { DEFAULT_ID } from "../../shared/constants";
7
+ import { handleFixedPopover } from "../../utils/floating-components";
8
+ import { DocumentScrollListener } from "../../utils/document-scroll-listener";
7
9
  function DBTooltipFn(props, component) {
8
10
  var _a, _b;
9
11
  const _ref = component || useRef(component);
10
12
  const [_id, set_id] = useState(() => DEFAULT_ID);
11
13
  const [initialized, setInitialized] = useState(() => false);
14
+ const [_documentScrollListenerCallbackId, set_documentScrollListenerCallbackId,] = useState(() => undefined);
15
+ const [_observer, set_observer] = useState(() => undefined);
12
16
  function handleClick(event) {
13
17
  event.stopPropagation();
14
18
  }
15
- function handleAutoPlacement() {
16
- if (_ref.current)
17
- handleDataOutside(_ref.current);
19
+ function handleEscape(event) {
20
+ if ((!event || event.key === "Escape") &&
21
+ _ref.current &&
22
+ getComputedStyle(_ref.current).visibility === "visible") {
23
+ getParent().blur();
24
+ }
25
+ }
26
+ function getParent() {
27
+ let parent = _ref.current.parentElement;
28
+ if (parent && parent.localName.includes("tooltip")) {
29
+ // Angular workaround
30
+ parent = parent.parentElement;
31
+ }
32
+ return parent;
33
+ }
34
+ function handleAutoPlacement(parent) {
35
+ if (!parent)
36
+ return;
37
+ if (_ref.current) {
38
+ // This is a workaround for angular
39
+ utilsDelay(() => {
40
+ var _a;
41
+ handleFixedPopover(_ref.current, parent, (_a = props.placement) !== null && _a !== void 0 ? _a : "bottom");
42
+ }, 1);
43
+ }
44
+ }
45
+ function handleDocumentScroll(event, parent) {
46
+ var _a, _b;
47
+ if (((_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a.contains) && ((_b = event === null || event === void 0 ? void 0 : event.target) === null || _b === void 0 ? void 0 : _b.contains(_ref.current))) {
48
+ handleAutoPlacement(parent);
49
+ }
50
+ }
51
+ function handleLeave() {
52
+ if (_documentScrollListenerCallbackId) {
53
+ new DocumentScrollListener().removeCallback(_documentScrollListenerCallbackId);
54
+ }
55
+ _observer === null || _observer === void 0 ? void 0 : _observer.unobserve(getParent());
56
+ }
57
+ function handleEnter(parent) {
58
+ set_documentScrollListenerCallbackId(new DocumentScrollListener().addCallback((event) => handleDocumentScroll(event, parent)));
59
+ handleAutoPlacement(parent);
60
+ _observer === null || _observer === void 0 ? void 0 : _observer.observe(getParent());
18
61
  }
19
62
  useEffect(() => {
20
63
  set_id(props.id || "tooltip-" + uuid());
@@ -22,14 +65,14 @@ function DBTooltipFn(props, component) {
22
65
  }, []);
23
66
  useEffect(() => {
24
67
  if (_ref.current && initialized && _id) {
25
- let parent = _ref.current.parentElement;
26
- if (parent && parent.localName.includes("tooltip")) {
27
- // Angular workaround
28
- parent = parent.parentElement;
29
- }
68
+ const parent = getParent();
30
69
  if (parent) {
31
- ["mouseenter", "focus"].forEach((event) => {
32
- parent.addEventListener(event, () => handleAutoPlacement());
70
+ ["mouseenter", "focusin"].forEach((event) => {
71
+ parent.addEventListener(event, () => handleEnter(parent));
72
+ });
73
+ parent.addEventListener("keydown", (event) => handleEscape(event));
74
+ ["mouseleave", "focusout"].forEach((event) => {
75
+ parent.addEventListener(event, () => handleLeave());
33
76
  });
34
77
  parent.setAttribute("data-has-tooltip", "true");
35
78
  if (props.variant === "label") {
@@ -39,6 +82,12 @@ function DBTooltipFn(props, component) {
39
82
  parent.setAttribute("aria-describedby", _id);
40
83
  }
41
84
  }
85
+ set_observer(new IntersectionObserver((payload) => {
86
+ const entry = payload.find(({ target }) => target === getParent());
87
+ if (entry && !entry.isIntersecting) {
88
+ handleEscape(false);
89
+ }
90
+ }));
42
91
  setInitialized(false);
43
92
  }
44
93
  }, [_ref.current, initialized]);
@@ -150,9 +150,6 @@ export type PopoverProps = {
150
150
  */
151
151
  width?: PopoverWidthType;
152
152
  };
153
- export type PopoverState = {
154
- handleAutoPlacement: () => void;
155
- };
156
153
  export type NameProps = {
157
154
  /**
158
155
  * The name attribute gives the name of the element to group it.
@@ -234,6 +231,8 @@ export type CustomFormProps = {
234
231
  validation?: ValidationType;
235
232
  };
236
233
  export type FormProps = CustomFormProps & BaseFormProps & RequiredProps & ShowLabelProps & ValueProps;
234
+ export declare const FieldSizingList: readonly ["fixed", "content"];
235
+ export type FieldSizingType = (typeof FieldSizingList)[number];
237
236
  export type FormTextProps = {
238
237
  /**
239
238
  * Maximum length (number of characters) of value
@@ -259,6 +258,11 @@ export type FormTextProps = {
259
258
  * The disabled attribute can be set to keep a user from edit on the form element
260
259
  */
261
260
  readonly?: boolean | string;
261
+ /**
262
+ * Adds shrinkwrap for input and textarea: https://developer.mozilla.org/en-US/docs/Web/CSS/field-sizing
263
+ * Note: Only supported in Chromium browsers so far
264
+ */
265
+ fieldSizing?: FieldSizingType;
262
266
  };
263
267
  export type FormSizeProps = {
264
268
  /**
@@ -493,3 +497,14 @@ export type ValueLabelType = {
493
497
  value: string;
494
498
  label?: string;
495
499
  };
500
+ export type DocumentScrollState = {
501
+ _documentScrollListenerCallbackId?: string;
502
+ handleDocumentScroll: (event: any, parent?: HTMLElement) => void;
503
+ _observer?: IntersectionObserver;
504
+ };
505
+ export type PopoverState = {
506
+ handleEscape: (event: any) => void;
507
+ handleAutoPlacement: (parent?: HTMLElement) => void;
508
+ handleEnter: (parent?: HTMLElement) => void;
509
+ handleLeave: (event?: any) => void;
510
+ } & DocumentScrollState;
@@ -13,6 +13,7 @@ export const PopoverWidthList = ['auto', 'fixed'];
13
13
  export const SizeList = ['small', 'medium'];
14
14
  export const EmphasisList = ['weak', 'strong'];
15
15
  export const ValidationList = ['invalid', 'valid', 'no-validation'];
16
+ export const FieldSizingList = ['fixed', 'content'];
16
17
  export const LabelVariantList = ['above', 'floating'];
17
18
  export const AutoCompleteList = ['off', 'on', 'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix', 'nickname', 'email', 'username', 'new-password', 'current-password', 'one-time-code', 'organization-title', 'organization', 'street-address', 'shipping', 'billing', 'address-line1', 'address-line2', 'address-line3', 'address-level4', 'address-level3', 'address-level2', 'address-level1', 'country', 'country-name', 'postal-code', 'cc-name', 'cc-given-name', 'cc-additional-name', 'cc-family-name', 'cc-number', 'cc-exp', 'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type', 'transaction-currency', 'transaction-amount', 'language', 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex', 'tel', 'tel-country-code', 'tel-national', 'tel-area-code', 'tel-local', 'tel-extension', 'impp', 'url', 'photo', 'webauthn'];
18
19
  export const LinkCurrentList = ['time', 'true', 'false', 'date', 'page', 'step', 'location'];
@@ -0,0 +1,9 @@
1
+ export declare class DocumentScrollListener {
2
+ private static callbacks;
3
+ private static _instance;
4
+ private static runCallbacks;
5
+ private ticking;
6
+ constructor();
7
+ addCallback(callback: (event: any) => void): string;
8
+ removeCallback(id: string): void;
9
+ }
@@ -0,0 +1,38 @@
1
+ import { uuid } from './index';
2
+ export class DocumentScrollListener {
3
+ static runCallbacks(event) {
4
+ for (const callback of Object.values(DocumentScrollListener.callbacks)) {
5
+ if (typeof callback === 'function') {
6
+ callback(event);
7
+ }
8
+ }
9
+ }
10
+ constructor() {
11
+ this.ticking = false;
12
+ if (DocumentScrollListener._instance) {
13
+ return DocumentScrollListener._instance;
14
+ }
15
+ DocumentScrollListener._instance = this;
16
+ if (self.document) {
17
+ self.document.addEventListener('scroll', event => {
18
+ if (!this.ticking) {
19
+ window.requestAnimationFrame(() => {
20
+ DocumentScrollListener.runCallbacks(event);
21
+ this.ticking = false;
22
+ });
23
+ this.ticking = true;
24
+ }
25
+ }, true);
26
+ }
27
+ }
28
+ addCallback(callback) {
29
+ const callbackID = uuid();
30
+ DocumentScrollListener.callbacks[callbackID] = callback;
31
+ return callbackID;
32
+ }
33
+ removeCallback(id) {
34
+ delete DocumentScrollListener.callbacks[id];
35
+ }
36
+ }
37
+ DocumentScrollListener.callbacks = {};
38
+ DocumentScrollListener._instance = null;
@@ -0,0 +1,7 @@
1
+ export interface DBDataOutsidePair {
2
+ vx?: 'left' | 'right';
3
+ vy?: 'top' | 'bottom';
4
+ }
5
+ export declare const handleDataOutside: (el: HTMLElement) => DBDataOutsidePair;
6
+ export declare const handleFixedDropdown: (element: HTMLElement, parent: HTMLElement, placement: string) => void;
7
+ export declare const handleFixedPopover: (element: HTMLElement, parent: HTMLElement, placement: string) => void;
@@ -0,0 +1,290 @@
1
+ // TODO: We should reevaluate this as soon as CSS Anchor Positioning is supported in all relevant browsers
2
+ const isInView = (el) => {
3
+ var _a;
4
+ const { top, bottom, left, right } = el.getBoundingClientRect();
5
+ const { innerHeight, innerWidth } = window;
6
+ let outTop = top < 0;
7
+ let outBottom = bottom > innerHeight;
8
+ let outLeft = left < 0;
9
+ let outRight = right > innerWidth;
10
+ // We need to check if it was already outside
11
+ const outsideY = el.dataset['outsideVy'];
12
+ const outsideX = el.dataset['outsideVx'];
13
+ const parentRect = (_a = el === null || el === void 0 ? void 0 : el.parentElement) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
14
+ if (parentRect) {
15
+ if (outsideY) {
16
+ const position = el.dataset['outsideVy'];
17
+ if (position === 'top') {
18
+ outTop = parentRect.top - (bottom - parentRect.bottom) < 0;
19
+ }
20
+ else {
21
+ outBottom = parentRect.bottom + (parentRect.top - top) > innerHeight;
22
+ }
23
+ }
24
+ if (outsideX) {
25
+ const position = el.dataset['outsideVx'];
26
+ if (position === 'left') {
27
+ outLeft = parentRect.left - (right - parentRect.right) < 0;
28
+ }
29
+ else {
30
+ outRight = parentRect.right + (parentRect.left - left) > innerWidth;
31
+ }
32
+ }
33
+ }
34
+ return {
35
+ outTop,
36
+ outBottom,
37
+ outLeft,
38
+ outRight
39
+ };
40
+ };
41
+ export const handleDataOutside = (el) => {
42
+ const { outTop, outBottom, outLeft, outRight } = isInView(el);
43
+ let dataOutsidePair = {};
44
+ if (outTop || outBottom) {
45
+ dataOutsidePair = {
46
+ vy: outTop ? 'top' : 'bottom'
47
+ };
48
+ el.dataset['outsideVy'] = dataOutsidePair.vy;
49
+ }
50
+ else {
51
+ delete el.dataset['outsideVy'];
52
+ }
53
+ if (outLeft || outRight) {
54
+ dataOutsidePair = Object.assign(Object.assign({}, dataOutsidePair), { vx: outRight ? 'right' : 'left' });
55
+ el.dataset['outsideVx'] = dataOutsidePair.vx;
56
+ }
57
+ else {
58
+ delete el.dataset['outsideVx'];
59
+ }
60
+ return dataOutsidePair;
61
+ };
62
+ export const handleFixedDropdown = (element, parent, placement) => {
63
+ // We skip this if we are in mobile it's already fixed
64
+ if (getComputedStyle(element).zIndex === '9999')
65
+ return;
66
+ const { top, bottom, childHeight, childWidth, width, right, left, correctedPlacement } = getFloatingProps(element, parent, placement);
67
+ const fullWidth = element.dataset['width'] === 'full';
68
+ if (fullWidth) {
69
+ element.style.inlineSize = `${width}px`;
70
+ }
71
+ if (correctedPlacement === 'top' || correctedPlacement === 'bottom' || correctedPlacement === 'top-start' || correctedPlacement === 'bottom-start') {
72
+ element.style.insetInlineStart = `${left}px`;
73
+ }
74
+ else if (correctedPlacement === 'top-end' || correctedPlacement === 'bottom-end') {
75
+ element.style.insetInlineStart = `${right - childWidth}px`;
76
+ }
77
+ if (correctedPlacement === null || correctedPlacement === void 0 ? void 0 : correctedPlacement.startsWith('top')) {
78
+ element.style.insetBlockStart = `${top - childHeight}px`;
79
+ }
80
+ else if (correctedPlacement === null || correctedPlacement === void 0 ? void 0 : correctedPlacement.startsWith('bottom')) {
81
+ element.style.insetBlockStart = `${bottom}px`;
82
+ }
83
+ element.style.position = 'fixed';
84
+ };
85
+ const getFloatingProps = (element, parent, placement) => {
86
+ const childRect = element.getBoundingClientRect();
87
+ const { top, height, bottom, right, left, width } = parent.getBoundingClientRect();
88
+ const { innerHeight, innerWidth } = window;
89
+ let childHeight = childRect.height;
90
+ let childWidth = childRect.width;
91
+ if (placement === 'bottom' || placement === 'top') {
92
+ childWidth = childWidth / 2;
93
+ }
94
+ if (placement === 'left' || placement === 'right') {
95
+ childHeight = childHeight / 2;
96
+ }
97
+ const outsideBottom = bottom + childHeight > innerHeight;
98
+ const outsideTop = top - childHeight < 0;
99
+ const outsideLeft = left - childWidth < 0;
100
+ const outsideRight = right + childWidth > innerWidth;
101
+ let correctedPlacement = placement;
102
+ if (placement.startsWith('bottom')) {
103
+ if (outsideBottom) {
104
+ correctedPlacement = placement === null || placement === void 0 ? void 0 : placement.replace('bottom', 'top');
105
+ if (outsideLeft && outsideRight) {
106
+ correctedPlacement = 'top';
107
+ }
108
+ else if (outsideLeft) {
109
+ correctedPlacement = 'top-start';
110
+ }
111
+ else if (outsideRight) {
112
+ correctedPlacement = 'top-end';
113
+ }
114
+ }
115
+ else {
116
+ if (outsideLeft && outsideRight) {
117
+ correctedPlacement = 'bottom';
118
+ }
119
+ else if (outsideLeft) {
120
+ correctedPlacement = 'bottom-start';
121
+ }
122
+ else if (outsideRight) {
123
+ correctedPlacement = 'bottom-end';
124
+ }
125
+ }
126
+ }
127
+ else if (placement.startsWith('top')) {
128
+ if (outsideTop) {
129
+ correctedPlacement = placement === null || placement === void 0 ? void 0 : placement.replace('top', 'bottom');
130
+ if (outsideLeft && outsideRight) {
131
+ correctedPlacement = 'bottom';
132
+ }
133
+ else if (outsideLeft) {
134
+ correctedPlacement = 'bottom-start';
135
+ }
136
+ else if (outsideRight) {
137
+ correctedPlacement = 'bottom-end';
138
+ }
139
+ }
140
+ else {
141
+ if (outsideLeft && outsideRight) {
142
+ correctedPlacement = 'top';
143
+ }
144
+ else if (outsideLeft) {
145
+ correctedPlacement = 'top-start';
146
+ }
147
+ else if (outsideRight) {
148
+ correctedPlacement = 'top-end';
149
+ }
150
+ }
151
+ }
152
+ else if (placement.startsWith('left')) {
153
+ if (outsideLeft) {
154
+ correctedPlacement = placement === null || placement === void 0 ? void 0 : placement.replace('left', 'right');
155
+ if (outsideBottom && outsideTop) {
156
+ correctedPlacement = 'right';
157
+ }
158
+ else if (outsideBottom) {
159
+ correctedPlacement = 'right-end';
160
+ }
161
+ else if (outsideTop) {
162
+ correctedPlacement = 'right-start';
163
+ }
164
+ }
165
+ else {
166
+ if (outsideBottom && outsideTop) {
167
+ correctedPlacement = 'left';
168
+ }
169
+ else if (outsideBottom) {
170
+ correctedPlacement = 'left-end';
171
+ }
172
+ else if (outsideTop) {
173
+ correctedPlacement = 'left-start';
174
+ }
175
+ }
176
+ }
177
+ else if (correctedPlacement.startsWith('right')) {
178
+ if (outsideRight) {
179
+ correctedPlacement = placement === null || placement === void 0 ? void 0 : placement.replace('right', 'left');
180
+ if (outsideBottom && outsideTop) {
181
+ correctedPlacement = 'left';
182
+ }
183
+ else if (outsideBottom) {
184
+ correctedPlacement = 'left-end';
185
+ }
186
+ else if (outsideTop) {
187
+ correctedPlacement = 'left-start';
188
+ }
189
+ }
190
+ else {
191
+ if (outsideBottom && outsideTop) {
192
+ correctedPlacement = 'right';
193
+ }
194
+ else if (outsideBottom) {
195
+ correctedPlacement = 'right-end';
196
+ }
197
+ else if (outsideTop) {
198
+ correctedPlacement = 'right-start';
199
+ }
200
+ }
201
+ }
202
+ return {
203
+ top,
204
+ bottom,
205
+ right,
206
+ height,
207
+ width,
208
+ left,
209
+ childHeight: childRect.height,
210
+ childWidth: childRect.width,
211
+ correctedPlacement,
212
+ innerWidth,
213
+ innerHeight
214
+ };
215
+ };
216
+ export const handleFixedPopover = (element, parent, placement) => {
217
+ var _a;
218
+ const distance = (_a = getComputedStyle(element).getPropertyValue('--db-popover-distance')) !== null && _a !== void 0 ? _a : '0px';
219
+ const { top, height, width, childHeight, childWidth, right, left, bottom, correctedPlacement, innerWidth, innerHeight } = getFloatingProps(element, parent, placement);
220
+ // Tooltip arrow position
221
+ if (childWidth > width && (correctedPlacement.startsWith('bottom') || correctedPlacement.startsWith('top'))) {
222
+ const diff = width / 2 / childWidth * 100;
223
+ if (correctedPlacement.endsWith('start')) {
224
+ element.style.setProperty('--db-tooltip-arrow-inline-start', `${diff}%`);
225
+ }
226
+ else if (correctedPlacement.endsWith('end')) {
227
+ element.style.setProperty('--db-tooltip-arrow-inline-start', `${100 - diff}%`);
228
+ }
229
+ }
230
+ if (childHeight > height && (correctedPlacement.startsWith('left') || correctedPlacement.startsWith('bottom'))) {
231
+ const diff = height / 2 / childHeight * 100;
232
+ if (correctedPlacement.endsWith('start')) {
233
+ element.style.setProperty('--db-tooltip-arrow-block-start', `${diff}%`);
234
+ }
235
+ else if (correctedPlacement.endsWith('end')) {
236
+ element.style.setProperty('--db-tooltip-arrow-block-start', `${100 - diff}%`);
237
+ }
238
+ }
239
+ // Popover position
240
+ if (correctedPlacement === 'right' || correctedPlacement === 'left') {
241
+ // center horizontally
242
+ element.style.insetBlockStart = `${top + height / 2}px`;
243
+ }
244
+ else if (correctedPlacement === 'right-start' || correctedPlacement === 'left-start') {
245
+ const end = top + childHeight;
246
+ element.style.insetBlockStart = `${top}px`;
247
+ element.style.insetBlockEnd = `${end > innerHeight ? innerHeight : end}px`;
248
+ }
249
+ else if (correctedPlacement === 'right-end' || correctedPlacement === 'left-end') {
250
+ const start = bottom - childHeight;
251
+ element.style.insetBlockStart = `${start < 0 ? 0 : start}px`;
252
+ element.style.insetBlockEnd = `${bottom}px`;
253
+ }
254
+ else if (correctedPlacement === 'top' || correctedPlacement === 'bottom') {
255
+ // center vertically
256
+ element.style.insetInlineStart = `${left + width / 2}px`;
257
+ }
258
+ else if (correctedPlacement === 'top-start' || correctedPlacement === 'bottom-start') {
259
+ const end = left + childWidth;
260
+ element.style.insetInlineStart = `${left}px`;
261
+ element.style.insetInlineEnd = `${end > innerWidth ? innerWidth : end}px`;
262
+ }
263
+ else if (correctedPlacement === 'top-end' || correctedPlacement === 'bottom-end') {
264
+ const start = left - childWidth;
265
+ element.style.insetInlineStart = `${right - childWidth}px`;
266
+ element.style.insetInlineEnd = `${start < 0 ? 0 : start}px`;
267
+ }
268
+ if (correctedPlacement === null || correctedPlacement === void 0 ? void 0 : correctedPlacement.startsWith('right')) {
269
+ const end = right + childWidth;
270
+ element.style.insetInlineStart = `calc(${right}px + ${distance})`;
271
+ element.style.insetInlineEnd = `calc(${end > innerWidth ? innerWidth : end}px + ${distance})`;
272
+ }
273
+ else if (correctedPlacement === null || correctedPlacement === void 0 ? void 0 : correctedPlacement.startsWith('left')) {
274
+ const start = left - childWidth;
275
+ element.style.insetInlineStart = `calc(${start < 0 ? 0 : start}px - ${distance})`;
276
+ element.style.insetInlineEnd = `calc(${right}px - ${distance})`;
277
+ }
278
+ else if (correctedPlacement === null || correctedPlacement === void 0 ? void 0 : correctedPlacement.startsWith('top')) {
279
+ const start = top - childHeight;
280
+ element.style.insetBlockStart = `calc(${start < 0 ? 0 : start}px - ${distance})`;
281
+ element.style.insetBlockEnd = `calc(${bottom}px - ${distance})`;
282
+ }
283
+ else if (correctedPlacement === null || correctedPlacement === void 0 ? void 0 : correctedPlacement.startsWith('bottom')) {
284
+ const end = bottom + childHeight;
285
+ element.style.insetBlockStart = `calc(${bottom}px + ${distance})`;
286
+ element.style.insetBlockEnd = `calc(${end > innerHeight ? innerHeight : end}px + ${distance})`;
287
+ }
288
+ element.style.position = 'fixed';
289
+ element.setAttribute('data-corrected-placement', correctedPlacement);
290
+ };
@@ -7,19 +7,6 @@ export type ClassNameArg = string | {
7
7
  [key: string]: boolean | undefined;
8
8
  } | undefined;
9
9
  export declare const cls: (...args: ClassNameArg[]) => string;
10
- export declare const visibleInVX: (el: Element) => boolean;
11
- export declare const visibleInVY: (el: Element) => boolean;
12
- export declare const isInView: (el: Element) => {
13
- outTop: boolean;
14
- outBottom: boolean;
15
- outLeft: boolean;
16
- outRight: boolean;
17
- };
18
- export interface DBDataOutsidePair {
19
- vx?: 'left' | 'right';
20
- vy?: 'top' | 'bottom';
21
- }
22
- export declare const handleDataOutside: (el: Element) => DBDataOutsidePair;
23
10
  export declare const isArrayOfStrings: (value: unknown) => value is string[];
24
11
  export declare const hasVoiceOver: () => boolean;
25
12
  export declare const delay: (fn: () => void, ms: number) => Promise<unknown>;
@@ -45,3 +32,7 @@ export declare const getInputValue: (value?: number | string, inputType?: string
45
32
  export declare const getHideProp: (show?: boolean | string) => any;
46
33
  export declare const stringPropVisible: (givenString?: string, showString?: boolean | string) => boolean;
47
34
  export declare const getSearchInput: (element: HTMLElement) => HTMLInputElement | null;
35
+ export declare const getOptionKey: (option: {
36
+ id?: string;
37
+ value?: string | number | string[] | undefined;
38
+ }, prefix: string) => string;
@@ -37,76 +37,6 @@ export const cls = (...args) => {
37
37
  }
38
38
  return result.trim();
39
39
  };
40
- export const visibleInVX = (el) => {
41
- const { left, right } = el.getBoundingClientRect();
42
- const { innerWidth } = window;
43
- return left >= 0 && right <= innerWidth;
44
- };
45
- export const visibleInVY = (el) => {
46
- const { top, bottom } = el.getBoundingClientRect();
47
- const { innerHeight } = window;
48
- return top >= 0 && bottom <= innerHeight;
49
- };
50
- export const isInView = (el) => {
51
- var _a;
52
- const { top, bottom, left, right } = el.getBoundingClientRect();
53
- const { innerHeight, innerWidth } = window;
54
- let outTop = top < 0;
55
- let outBottom = bottom > innerHeight;
56
- let outLeft = left < 0;
57
- let outRight = right > innerWidth;
58
- // We need to check if it was already outside
59
- const outsideY = el.hasAttribute('data-outside-vy');
60
- const outsideX = el.hasAttribute('data-outside-vx');
61
- const parentRect = (_a = el === null || el === void 0 ? void 0 : el.parentElement) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
62
- if (parentRect) {
63
- if (outsideY) {
64
- const position = el.getAttribute('data-outside-vy');
65
- if (position === 'top') {
66
- outTop = parentRect.top - (bottom - parentRect.bottom) < 0;
67
- }
68
- else {
69
- outBottom = parentRect.bottom + (parentRect.top - top) > innerHeight;
70
- }
71
- }
72
- if (outsideX) {
73
- const position = el.getAttribute('data-outside-vx');
74
- if (position === 'left') {
75
- outLeft = parentRect.left - (right - parentRect.right) < 0;
76
- }
77
- else {
78
- outRight = parentRect.right + (parentRect.left - left) > innerWidth;
79
- }
80
- }
81
- }
82
- return {
83
- outTop,
84
- outBottom,
85
- outLeft,
86
- outRight
87
- };
88
- };
89
- export const handleDataOutside = (el) => {
90
- const { outTop, outBottom, outLeft, outRight } = isInView(el);
91
- let dataOutsidePair = {};
92
- if (outTop || outBottom) {
93
- dataOutsidePair = {
94
- vy: outTop ? 'top' : 'bottom'
95
- };
96
- el.setAttribute('data-outside-vy', dataOutsidePair.vy);
97
- }
98
- else {
99
- el.removeAttribute('data-outside-vy');
100
- }
101
- if (outLeft || outRight) {
102
- dataOutsidePair = Object.assign(Object.assign({}, dataOutsidePair), { vx: outRight ? 'right' : 'left' });
103
- el.setAttribute('data-outside-vx', dataOutsidePair.vx);
104
- }
105
- else {
106
- el.removeAttribute('data-outside-vx');
107
- }
108
- return dataOutsidePair;
109
- };
110
40
  export const isArrayOfStrings = (value) => Array.isArray(value) && value.every(item => typeof item === 'string');
111
41
  const appleOs = ['Mac', 'iPhone', 'iPad', 'iPod'];
112
42
  export const hasVoiceOver = () => typeof window !== 'undefined' && appleOs.some(os => window.navigator.userAgent.includes(os));
@@ -168,3 +98,8 @@ export const stringPropVisible = (givenString, showString) => {
168
98
  }
169
99
  };
170
100
  export const getSearchInput = (element) => element.querySelector(`input[type="search"]`);
101
+ export const getOptionKey = (option, prefix) => {
102
+ var _a, _b;
103
+ const key = (_b = (_a = option.id) !== null && _a !== void 0 ? _a : option.value) !== null && _b !== void 0 ? _b : uuid();
104
+ return `${prefix}${key}`;
105
+ };
@@ -15,7 +15,7 @@ export declare class NavigationItemSafeTriangle {
15
15
  private initialized;
16
16
  private mouseX;
17
17
  private mouseY;
18
- constructor(element: HTMLElement | null, subNavigation: Element | null);
18
+ constructor(element: HTMLElement | null, subNavigation: HTMLElement | null);
19
19
  private init;
20
20
  enableFollow(): void;
21
21
  disableFollow(): void;
@@ -1,4 +1,4 @@
1
- import { handleDataOutside } from './index';
1
+ import { handleDataOutside } from './floating-components';
2
2
  export const isEventTargetNavigationItem = (event) => {
3
3
  var _a, _b;
4
4
  const { target } = event;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@db-ux/react-core-components",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "React components for @db-ux/core-components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "sideEffects": false,
40
40
  "dependencies": {
41
- "@db-ux/core-components": "2.1.2",
42
- "@db-ux/core-foundations": "2.1.2"
41
+ "@db-ux/core-components": "2.2.1",
42
+ "@db-ux/core-foundations": "2.2.1"
43
43
  }
44
44
  }