@db-ux/react-core-components 4.12.0 → 4.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @db-ux/react-core-components
2
2
 
3
+ ## 4.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fix(custom-select): dropdown with `dropdownWidth="auto"` now correctly sizes to content width and respects the trigger minimum width. Long option labels no longer get truncated: `auto` keeps them on a single line (dropdown grows to the longest option), while `fixed` and `full` wrap long labels onto new lines. - [see commit 68dedc3](https://github.com/db-ux-design-system/core-web/commit/68dedc33c324b48339d5bb73a85fdff3045ed059)
8
+
9
+ - fix(drawer): prevent backdrop drag-close when selection starts inside content - [see commit b53ff8a](https://github.com/db-ux-design-system/core-web/commit/b53ff8a4f0a5350c5be41fad072e14797676bba7)
10
+
3
11
  ## 4.12.0
4
12
 
5
13
  ### Minor Changes
@@ -449,6 +449,9 @@ function DBCustomSelectFn(props, component) {
449
449
  .toLowerCase()
450
450
  .includes(filterText.toLowerCase())));
451
451
  }
452
+ if (detailsRef.current?.open) {
453
+ handleAutoPlacement();
454
+ }
452
455
  }
453
456
  function handleClearAll(event) {
454
457
  event.stopPropagation();
@@ -597,6 +600,10 @@ function DBCustomSelectFn(props, component) {
597
600
  useEffect(() => {
598
601
  set_options(props.options);
599
602
  setAmountOptions(props.options?.filter((option) => !option.isGroupTitle).length ?? 0);
603
+ // Reposition open auto-width dropdowns: replacing options can change their content width.
604
+ if (detailsRef.current?.open) {
605
+ handleAutoPlacement();
606
+ }
600
607
  }, [props.options]);
601
608
  useEffect(() => {
602
609
  set_searchValue(props.searchValue);
@@ -24,7 +24,7 @@ function DBCustomSelectListItemFn(props, component) {
24
24
  return (_jsx("li", { 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", "data-density"]), id: props.id ?? props.propOverrides?.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", "data-density"]), className: cls("db-custom-select-list-item", props.className, {
25
25
  "db-checkbox": props.type === "checkbox" && !props.isGroupTitle,
26
26
  "db-radio": props.type !== "checkbox" && !props.isGroupTitle,
27
- }), "data-divider": getBooleanAsString(hasDivider, "hasDivider"), children: !props.isGroupTitle ? (_jsxs("label", { "data-icon": props.type !== "checkbox" && props.icon ? props.icon : undefined, "data-show-icon": getBooleanAsString(props.showIcon, "showIcon"), "data-icon-trailing": getIconTrailing(), children: [_jsx("input", { className: "db-custom-select-list-item-checkbox", "data-disable-focus": "true", type: props.type, name: props.name, form: props.name, checked: getBoolean(props.checked, "checked"), disabled: getBoolean(props.disabled, "disabled"), value: props.value, onChange: (event) => handleChange(event) }), props.label ? _jsx(_Fragment, { children: props.label }) : null, props.children] })) : (_jsx("span", { children: props.groupTitle })) }));
27
+ }), "data-divider": getBooleanAsString(hasDivider, "hasDivider"), children: !props.isGroupTitle ? (_jsxs("label", { "data-icon": props.type !== "checkbox" && props.icon ? props.icon : undefined, "data-show-icon": getBooleanAsString(props.showIcon, "showIcon"), "data-icon-trailing": getIconTrailing(), children: [_jsx("input", { className: "db-custom-select-list-item-checkbox", "data-disable-focus": "true", type: props.type, name: props.name, form: props.name, checked: getBoolean(props.checked, "checked"), disabled: getBoolean(props.disabled, "disabled"), value: props.value, onChange: (event) => handleChange(event) }), _jsxs("span", { className: "db-custom-select-list-item-label", children: [props.label ? _jsx(_Fragment, { children: props.label }) : null, props.children] })] })) : (_jsx("span", { children: props.groupTitle })) }));
28
28
  }
29
29
  const DBCustomSelectListItem = forwardRef(DBCustomSelectListItemFn);
30
30
  export default DBCustomSelectListItem;
@@ -9,6 +9,19 @@ function DBDrawerFn(props, component) {
9
9
  const _ref = component || useRef(component);
10
10
  const dialogContainerRef = useRef(null);
11
11
  const [initialized, setInitialized] = useState(() => false);
12
+ const [backdropPointerDown, setBackdropPointerDown] = useState(() => false);
13
+ function isNotModal() {
14
+ return (props.position === "absolute" ||
15
+ props.backdrop === "none" ||
16
+ props.variant === "inside");
17
+ }
18
+ function handleBackdropPointerDown(event) {
19
+ // Remember whether the pointer interaction started on the backdrop
20
+ // (the DIALOG element itself) so we only close on a real backdrop
21
+ // click and not when a drag started inside the content and ended
22
+ // on the backdrop.
23
+ setBackdropPointerDown(event?.target?.nodeName === "DIALOG");
24
+ }
12
25
  function handleClose(event, forceClose) {
13
26
  if (!event)
14
27
  return;
@@ -29,11 +42,15 @@ function DBDrawerFn(props, component) {
29
42
  }
30
43
  if (event.target?.nodeName === "DIALOG" &&
31
44
  event.type === "click" &&
32
- props.backdrop !== "none") {
45
+ props.backdrop !== "none" &&
46
+ backdropPointerDown) {
33
47
  if (props.onClose) {
34
48
  props.onClose(event);
35
49
  }
36
50
  }
51
+ // Reset after handling the click so the next interaction
52
+ // starts from a clean state.
53
+ setBackdropPointerDown(false);
37
54
  }
38
55
  }
39
56
  function handleDialogOpen() {
@@ -43,12 +60,15 @@ function DBDrawerFn(props, component) {
43
60
  if (dialogContainerRef.current) {
44
61
  dialogContainerRef.current.removeAttribute("data-transition");
45
62
  }
46
- if (props.position === "absolute" ||
47
- props.backdrop === "none" ||
48
- props.variant === "inside") {
63
+ if (isNotModal()) {
49
64
  _ref.current.show();
50
65
  }
51
66
  else {
67
+ // Set the closedby attribute imperatively: the JSX
68
+ // dialog type does not know this attribute yet, and it
69
+ // only applies to modal dialogs. "any" enables native
70
+ // light dismiss (backdrop click / Esc).
71
+ _ref.current.setAttribute("closedby", "any");
52
72
  _ref.current.showModal();
53
73
  }
54
74
  void delay(() => {
@@ -68,6 +88,7 @@ function DBDrawerFn(props, component) {
68
88
  }
69
89
  }
70
90
  }
91
+ const [state, setState] = useState(() => null);
71
92
  useEffect(() => {
72
93
  handleDialogOpen();
73
94
  setInitialized(true);
@@ -84,7 +105,7 @@ function DBDrawerFn(props, component) {
84
105
  }
85
106
  }
86
107
  }, [_ref.current, initialized, props.position]);
87
- return (_jsx("dialog", { className: "db-drawer", id: props.id ?? props.propOverrides?.id, 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", "data-density", "onClose"]), onClick: (event) => handleClose(event), onKeyDown: (event) => handleClose(event), "data-position": props.position, "data-backdrop": props.backdrop, "data-direction": props.direction, "data-variant": props.variant, children: _jsxs("article", { ref: dialogContainerRef, ...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", "data-density"]), className: cls("db-drawer-container", props.className), "data-spacing": props.spacing, "data-width": props.width, "data-direction": props.direction, "data-rounded": getBooleanAsString(props.rounded, "rounded"), children: [_jsxs("header", { className: "db-drawer-header", children: [_jsx("div", { className: "db-drawer-header-text", children: _jsx(_Fragment, { children: props.drawerHeader }) }), _jsx(DBButton, { className: "button-close-drawer", icon: "cross", variant: "ghost", id: props.closeButtonId, noText: true, onClick: (event) => handleClose(event, true), children: props.closeButtonText ?? DEFAULT_CLOSE_BUTTON })] }), _jsx("div", { className: "db-drawer-content", children: props.children })] }) }));
108
+ return (_jsx("dialog", { className: "db-drawer", id: props.id ?? props.propOverrides?.id, 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", "data-density", "onClose"]), onClick: (event) => handleClose(event), onMouseDown: (event) => handleBackdropPointerDown(event), onKeyDown: (event) => handleClose(event), "data-position": props.position, "data-backdrop": props.backdrop, "data-direction": props.direction, "data-variant": props.variant, children: _jsxs("article", { ref: dialogContainerRef, ...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", "data-density"]), className: cls("db-drawer-container", props.className), "data-spacing": props.spacing, "data-width": props.width, "data-direction": props.direction, "data-rounded": getBooleanAsString(props.rounded, "rounded"), children: [_jsxs("header", { className: "db-drawer-header", children: [_jsx("div", { className: "db-drawer-header-text", children: _jsx(_Fragment, { children: props.drawerHeader }) }), _jsx(DBButton, { className: "button-close-drawer", icon: "cross", variant: "ghost", id: props.closeButtonId, noText: true, onClick: (event) => handleClose(event, true), children: props.closeButtonText ?? DEFAULT_CLOSE_BUTTON })] }), _jsx("div", { className: "db-drawer-content", children: props.children })] }) }));
88
109
  }
89
110
  const DBDrawer = forwardRef(DBDrawerFn);
90
111
  export default DBDrawer;
@@ -46,5 +46,8 @@ export type DBDrawerDefaultProps = {
46
46
  export type DBDrawerProps = DBDrawerDefaultProps & GlobalProps & CloseEventProps<ClickEvent<HTMLButtonElement | HTMLDialogElement> | GeneralKeyboardEvent<HTMLDialogElement>> & InnerCloseButtonProps & WidthProps & SpacingProps;
47
47
  export type DBDrawerDefaultState = {
48
48
  handleDialogOpen: () => void;
49
+ isNotModal: () => boolean;
50
+ handleBackdropPointerDown: (event: any) => void;
51
+ backdropPointerDown: boolean;
49
52
  };
50
53
  export type DBDrawerState = DBDrawerDefaultState & GlobalState & CloseEventState<ClickEvent<HTMLButtonElement | HTMLDialogElement> | GeneralKeyboardEvent<HTMLDialogElement>> & InitializedState;
@@ -64,24 +64,69 @@ export const handleDataOutside = (el) => {
64
64
  export const handleFixedDropdown = (element, parent, placement) => {
65
65
  if (!element || !parent)
66
66
  return;
67
- // We skip this if we are in mobile it's already fixed
67
+ const fullWidth = element.dataset['width'] === 'full';
68
+ const autoWidth = element.dataset['width'] === 'auto';
69
+ // Reset width-specific inline styles first so a previous mode (e.g. "auto")
70
+ // doesn't leave a stale minInlineSize/inlineSize behind when the dropdown
71
+ // width changes at runtime. This must happen before getFloatingProps
72
+ // measures the element, otherwise the dropdown would be measured with a
73
+ // width it no longer has and positioned incorrectly. It also has to run
74
+ // before the mobile bailout below: otherwise a desktop minInlineSize would
75
+ // survive into the mobile sheet, where CSS min-inline-size beats the
76
+ // mobile max-inline-size guard and overflows the viewport.
77
+ element.style.inlineSize = '';
78
+ element.style.minInlineSize = '';
79
+ // We skip the rest if we are in mobile, it's already fixed via CSS.
68
80
  if (getComputedStyle(element).zIndex === '9999')
69
81
  return;
70
- const { top, bottom, childHeight, childWidth, width, right, left, correctedPlacement } = getFloatingProps(element, parent, placement);
71
- const fullWidth = element.dataset['width'] === 'full';
82
+ const { top, bottom, childHeight, childWidth, width, right, left, correctedPlacement, innerWidth } = getFloatingProps(element, parent, placement);
83
+ // For auto width the dropdown is forced to be at least as wide as the trigger,
84
+ // but clamped to its own max-inline-size: CSS lets a min-inline-size override
85
+ // the max when the minimum is larger, so a trigger wider than the viewport
86
+ // limit would otherwise drop the side margins or overflow horizontally.
87
+ let autoMinWidth = width;
88
+ if (autoWidth) {
89
+ const maxInlineSize = parseFloat(getComputedStyle(element).maxInlineSize);
90
+ if (!isNaN(maxInlineSize) && maxInlineSize > 0) {
91
+ autoMinWidth = Math.min(width, maxInlineSize);
92
+ }
93
+ }
72
94
  if (fullWidth) {
73
95
  element.style.inlineSize = `${width}px`;
74
96
  }
75
- if (correctedPlacement === 'top' || correctedPlacement === 'bottom' || correctedPlacement === 'top-start' || correctedPlacement === 'bottom-start') {
97
+ else if (autoWidth) {
98
+ element.style.minInlineSize = `${autoMinWidth}px`;
99
+ }
100
+ // getFloatingProps measured childWidth before the inline styles were
101
+ // (re)applied, so use the width the dropdown will actually have:
102
+ // - auto: the clamped minimum, so end-aligned dropdowns don't extend past
103
+ // the trigger's right edge.
104
+ // - full: the trigger width (the reset above drops it to content width).
105
+ let effectiveChildWidth = childWidth;
106
+ if (autoWidth) {
107
+ effectiveChildWidth = Math.max(childWidth, autoMinWidth);
108
+ }
109
+ else if (fullWidth) {
110
+ effectiveChildWidth = width;
111
+ }
112
+ // getFloatingProps detects horizontal overflow assuming a centered element
113
+ // (it halves childWidth). The dropdown is actually start-aligned (inset =
114
+ // left), so for the wider auto dropdown re-check overflow against its full
115
+ // width and flip to end-alignment when it would extend past the viewport.
116
+ let dropdownPlacement = correctedPlacement;
117
+ if (autoWidth && (dropdownPlacement === 'top' || dropdownPlacement === 'bottom' || dropdownPlacement === 'top-start' || dropdownPlacement === 'bottom-start') && left + effectiveChildWidth > innerWidth) {
118
+ dropdownPlacement = dropdownPlacement.startsWith('top') ? 'top-end' : 'bottom-end';
119
+ }
120
+ if (dropdownPlacement === 'top' || dropdownPlacement === 'bottom' || dropdownPlacement === 'top-start' || dropdownPlacement === 'bottom-start') {
76
121
  element.style.insetInlineStart = `${left}px`;
77
122
  }
78
- else if (correctedPlacement === 'top-end' || correctedPlacement === 'bottom-end') {
79
- element.style.insetInlineStart = `${right - childWidth}px`;
123
+ else if (dropdownPlacement === 'top-end' || dropdownPlacement === 'bottom-end') {
124
+ element.style.insetInlineStart = `${Math.max(right - effectiveChildWidth, 0)}px`;
80
125
  }
81
- if (correctedPlacement?.startsWith('top')) {
126
+ if (dropdownPlacement?.startsWith('top')) {
82
127
  element.style.insetBlockStart = `${top - childHeight}px`;
83
128
  }
84
- else if (correctedPlacement?.startsWith('bottom')) {
129
+ else if (dropdownPlacement?.startsWith('bottom')) {
85
130
  element.style.insetBlockStart = `${bottom}px`;
86
131
  }
87
132
  element.style.position = 'fixed';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@db-ux/react-core-components",
3
- "version": "4.12.0",
3
+ "version": "4.12.1",
4
4
  "type": "module",
5
5
  "description": "React components for @db-ux/core-components",
6
6
  "repository": {
@@ -17,8 +17,8 @@
17
17
  "dist/"
18
18
  ],
19
19
  "dependencies": {
20
- "@db-ux/core-components": "4.12.0",
21
- "@db-ux/core-foundations": "4.12.0"
20
+ "@db-ux/core-components": "4.12.1",
21
+ "@db-ux/core-foundations": "4.12.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playwright/experimental-ct-react": "1.60.0",