@db-ux/react-core-components 4.6.1 → 4.7.0-tabs-34782eb

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.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - DBSelect: correctly hiding empty `option` element for _placeholder_ or _floating label_ components with property `showEmptyOption=false` - [see commit ec01b5c](https://github.com/db-ux-design-system/core-web/commit/ec01b5cb56e1fc05911d33cbff0fc8f385644628)
8
+
9
+ - refactor(notification): not enforcing a paragraph for it's contents anymore. You could set any block level elements now as children. - [see commit 3170b3a](https://github.com/db-ux-design-system/core-web/commit/3170b3a6ef57bb73fa32b3176b7b8cf651a38641) and [commit 2ebe315](https://github.com/db-ux-design-system/core-web/commit/2ebe3156cd45e3702cf4acdc4224cd34da31d907)
10
+
3
11
  ## 4.6.1
4
12
 
5
13
  ### Patch Changes
package/agent/Tabs.md CHANGED
@@ -16,56 +16,56 @@ function Tabs(props: any) {
16
16
  <h2>1. Default Tabs</h2>
17
17
  <DBTabs>
18
18
  <DBTabList>
19
- <DBTabItem>Tab 1</DBTabItem>
20
- <DBTabItem>Tab 2</DBTabItem>
21
- <DBTabItem>Tab 3</DBTabItem>
19
+ <DBTabItem>Def Tab 1</DBTabItem>
20
+ <DBTabItem>Def Tab 2</DBTabItem>
21
+ <DBTabItem>Def Tab 3</DBTabItem>
22
22
  </DBTabList>
23
- <DBTabPanel>Tab Panel 1</DBTabPanel>
24
- <DBTabPanel>Tab Panel 2</DBTabPanel>
25
- <DBTabPanel>Tab Panel 3</DBTabPanel>
23
+ <DBTabPanel>Def Panel 1</DBTabPanel>
24
+ <DBTabPanel>Def Panel 2</DBTabPanel>
25
+ <DBTabPanel>Def Panel 3</DBTabPanel>
26
26
  </DBTabs>
27
27
  <h2>2. Behavior Variants</h2>
28
28
  <DBTabs behavior="scrollbar">
29
29
  <DBTabList>
30
- <DBTabItem>Tab 1</DBTabItem>
31
- <DBTabItem>Tab 2</DBTabItem>
32
- <DBTabItem>Tab 3</DBTabItem>
30
+ <DBTabItem>Scroll Tab 1</DBTabItem>
31
+ <DBTabItem>Scroll Tab 2</DBTabItem>
32
+ <DBTabItem>Scroll Tab 3</DBTabItem>
33
33
  </DBTabList>
34
- <DBTabPanel>Tab Panel 1</DBTabPanel>
35
- <DBTabPanel>Tab Panel 2</DBTabPanel>
36
- <DBTabPanel>Tab Panel 3</DBTabPanel>
34
+ <DBTabPanel>Scroll Panel 1</DBTabPanel>
35
+ <DBTabPanel>Scroll Panel 2</DBTabPanel>
36
+ <DBTabPanel>Scroll Panel 3</DBTabPanel>
37
37
  </DBTabs>
38
38
  <DBTabs behavior="arrows">
39
39
  <DBTabList>
40
- <DBTabItem>Tab 1</DBTabItem>
41
- <DBTabItem>Tab 2</DBTabItem>
42
- <DBTabItem>Tab 3</DBTabItem>
40
+ <DBTabItem>Arrow Tab 1</DBTabItem>
41
+ <DBTabItem>Arrow Tab 2</DBTabItem>
42
+ <DBTabItem>Arrow Tab 3</DBTabItem>
43
43
  </DBTabList>
44
- <DBTabPanel>Tab Panel 1</DBTabPanel>
45
- <DBTabPanel>Tab Panel 2</DBTabPanel>
46
- <DBTabPanel>Tab Panel 3</DBTabPanel>
44
+ <DBTabPanel>Arrow Panel 1</DBTabPanel>
45
+ <DBTabPanel>Arrow Panel 2</DBTabPanel>
46
+ <DBTabPanel>Arrow Panel 3</DBTabPanel>
47
47
  </DBTabs>
48
48
  <h2>3. Initial Selected Index</h2>
49
49
  <DBTabs initialSelectedIndex={1}>
50
50
  <DBTabList>
51
- <DBTabItem>Tab 1</DBTabItem>
52
- <DBTabItem>Tab 2</DBTabItem>
53
- <DBTabItem>Tab 3</DBTabItem>
51
+ <DBTabItem>InitIdx Tab 1</DBTabItem>
52
+ <DBTabItem>InitIdx Tab 2</DBTabItem>
53
+ <DBTabItem>InitIdx Tab 3</DBTabItem>
54
54
  </DBTabList>
55
- <DBTabPanel>Tab Panel 1</DBTabPanel>
56
- <DBTabPanel>Tab Panel 2</DBTabPanel>
57
- <DBTabPanel>Tab Panel 3</DBTabPanel>
55
+ <DBTabPanel>InitIdx Panel 1</DBTabPanel>
56
+ <DBTabPanel>InitIdx Panel 2</DBTabPanel>
57
+ <DBTabPanel>InitIdx Panel 3</DBTabPanel>
58
58
  </DBTabs>
59
59
  <h2>4. Initial Selected Mode</h2>
60
60
  <DBTabs initialSelectedMode="manually">
61
61
  <DBTabList>
62
- <DBTabItem>Tab 1</DBTabItem>
63
- <DBTabItem>Tab 2</DBTabItem>
64
- <DBTabItem>Tab 3</DBTabItem>
62
+ <DBTabItem>Manually Tab 1</DBTabItem>
63
+ <DBTabItem>Manually Tab 2</DBTabItem>
64
+ <DBTabItem>Manually Tab 3</DBTabItem>
65
65
  </DBTabList>
66
- <DBTabPanel>Tab Panel 1</DBTabPanel>
67
- <DBTabPanel>Tab Panel 2</DBTabPanel>
68
- <DBTabPanel>Tab Panel 3</DBTabPanel>
66
+ <DBTabPanel>Manually Panel 1</DBTabPanel>
67
+ <DBTabPanel>Manually Panel 2</DBTabPanel>
68
+ <DBTabPanel>Manually Panel 3</DBTabPanel>
69
69
  </DBTabs>
70
70
  </>
71
71
  );
@@ -23,7 +23,7 @@ function DBNotificationFn(props, component) {
23
23
  }), "aria-live": props.ariaLive, "data-semantic": props.semantic, "data-variant": props.variant, "data-icon": getBoolean(props.showIcon) !== false ? props.icon : undefined, "data-show-icon": getBooleanAsString(props.showIcon), "data-link-variant": props.linkVariant }),
24
24
  React.createElement(React.Fragment, null, props.image),
25
25
  stringPropVisible(props.headline, props.showHeadline) ? (React.createElement("header", null, props.headline)) : null,
26
- React.createElement("p", null, props.text ? React.createElement(React.Fragment, null, props.text) : React.createElement(React.Fragment, null, props.children)),
26
+ React.createElement("div", { "data-area": "content" }, props.text ? React.createElement(React.Fragment, null, props.text) : React.createElement(React.Fragment, null, props.children)),
27
27
  stringPropVisible(props.timestamp, props.showTimestamp) ? (React.createElement("span", null, props.timestamp)) : null,
28
28
  React.createElement(React.Fragment, null, props.link),
29
29
  getBoolean(props.closeable, "closeable") ? (React.createElement(DBButton, { icon: "cross", variant: "ghost", size: "small", id: props.closeButtonId, noText: true, onClick: (event) => handleClose(event) }, (_c = props.closeButtonText) !== null && _c !== void 0 ? _c : DEFAULT_CLOSE_BUTTON)) : null));
@@ -1,9 +1,5 @@
1
- import { ActiveProps, ChangeEventProps, ChangeEventState, GlobalProps, GlobalState, IconLeadingProps, IconProps, IconTrailingProps, InitializedState, NameProps, NameState, ShowIconLeadingProps, ShowIconProps, ShowIconTrailingProps } from '../../shared/model';
1
+ import { ActiveProps, ClickEventProps, GlobalProps, GlobalState, IconLeadingProps, IconProps, IconTrailingProps, InitializedState, ShowIconLeadingProps, ShowIconProps, ShowIconTrailingProps, WidthProps } from '../../shared/model';
2
2
  export type DBTabItemDefaultProps = {
3
- /**
4
- * To control the component
5
- */
6
- checked?: boolean | string;
7
3
  /**
8
4
  * The disabled attribute can be set to keep a user from clicking on the tab-item.
9
5
  */
@@ -16,12 +12,33 @@ export type DBTabItemDefaultProps = {
16
12
  * Define the text next to the icon specified via the icon Property to get hidden.
17
13
  */
18
14
  noText?: boolean | string;
15
+ /**
16
+ * Set the tabIndex manually (internal use for roving tabindex).
17
+ */
18
+ tabIndex?: number | string;
19
+ /**
20
+ * The id of the panel this tab controls (WAI-ARIA).
21
+ */
22
+ ariaControls?: string;
23
+ /**
24
+ * Semantic value of this tab item. When set, onIndexChange will emit this value
25
+ * (via the onValueChange event) instead of only the numeric index.
26
+ * Useful for form binding (e.g. Angular FormControl, React useState).
27
+ */
28
+ value?: string;
19
29
  };
20
- export type DBTabItemProps = GlobalProps & DBTabItemDefaultProps & IconProps & IconTrailingProps & IconLeadingProps & ShowIconLeadingProps & ShowIconTrailingProps & ActiveProps & ChangeEventProps<HTMLInputElement> & ShowIconProps & NameProps;
30
+ export type DBTabItemProps = DBTabItemDefaultProps & GlobalProps & ClickEventProps<HTMLButtonElement> & IconProps & ShowIconProps & IconTrailingProps & IconLeadingProps & ShowIconTrailingProps & ShowIconLeadingProps & ActiveProps & WidthProps;
21
31
  export type DBTabItemDefaultState = {
22
- _selected: boolean;
23
- _listenerAdded: boolean;
24
- boundSetSelectedOnChange?: (event: any) => void;
25
- setSelectedOnChange: (event: any) => void;
32
+ internalActive: boolean | undefined;
33
+ internalTabIndex: number;
34
+ getCurrentTabIndex: () => number;
35
+ _resizeObserver: ResizeObserver | null | undefined;
36
+ _ariaSelectedListener: {
37
+ fn: (event: any) => void;
38
+ } | null;
39
+ handleClick: (event: any) => void;
40
+ isTruncated: boolean;
41
+ checkTruncation: () => void;
42
+ tooltipText: string;
26
43
  };
27
- export type DBTabItemState = DBTabItemDefaultState & GlobalState & ChangeEventState<HTMLInputElement> & InitializedState & NameState;
44
+ export type DBTabItemState = DBTabItemDefaultState & GlobalState & InitializedState;
@@ -1,3 +1,3 @@
1
1
  import * as React from "react";
2
- declare const DBTabItem: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<any>, keyof import("../..").GlobalProps | "name" | "icon" | "showIcon" | "showIconLeading" | "showIconTrailing" | "iconLeading" | "iconTrailing" | keyof import("../..").ChangeEventProps<HTMLInputElement> | "active" | keyof import("./model").DBTabItemDefaultProps> & import("../..").GlobalProps & import("./model").DBTabItemDefaultProps & import("../..").IconProps & import("../..").IconTrailingProps & import("../..").IconLeadingProps & import("../..").ShowIconLeadingProps & import("../..").ShowIconTrailingProps & import("../..").ActiveProps & import("../..").ChangeEventProps<HTMLInputElement> & import("../..").ShowIconProps & import("../..").NameProps & React.RefAttributes<any>>;
2
+ declare const DBTabItem: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLButtonElement>, keyof import("../..").GlobalProps | "icon" | "showIcon" | keyof import("../..").ClickEventProps<HTMLButtonElement> | "width" | "showIconLeading" | "showIconTrailing" | "iconLeading" | "iconTrailing" | "active" | keyof import("./model").DBTabItemDefaultProps> & import("./model").DBTabItemDefaultProps & import("../..").GlobalProps & import("../..").ClickEventProps<HTMLButtonElement> & import("../..").IconProps & import("../..").ShowIconProps & import("../..").IconTrailingProps & import("../..").IconLeadingProps & import("../..").ShowIconTrailingProps & import("../..").ShowIconLeadingProps & import("../..").ActiveProps & import("../..").WidthProps & React.RefAttributes<HTMLButtonElement>>;
3
3
  export default DBTabItem;
@@ -2,77 +2,150 @@
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, getBoolean, getBooleanAsString } from "../../utils";
5
+ import { cls, getBoolean } from "../../utils";
6
+ import DBTooltip from "../tooltip/tooltip";
6
7
  function DBTabItemFn(props, component) {
7
- var _a, _b, _c, _d, _e, _f;
8
+ var _a, _b, _c, _d;
8
9
  const _ref = component || useRef(component);
9
- const [_selected, set_selected] = useState(() => false);
10
- const [_name, set_name] = useState(() => undefined);
10
+ const _labelRef = useRef(null);
11
11
  const [initialized, setInitialized] = useState(() => false);
12
- const [_listenerAdded, set_listenerAdded] = useState(() => false);
13
- const [boundSetSelectedOnChange, setBoundSetSelectedOnChange] = useState(() => undefined);
14
- function setSelectedOnChange(event) {
15
- event.stopPropagation();
16
- set_selected(event.target === _ref.current);
12
+ const [internalActive, setInternalActive] = useState(() => false);
13
+ const [internalTabIndex, setInternalTabIndex] = useState(() => -1);
14
+ function getCurrentTabIndex() {
15
+ return props.tabIndex !== undefined
16
+ ? Number(props.tabIndex)
17
+ : internalTabIndex;
17
18
  }
18
- function handleNameAttribute() {
19
- if (_ref.current) {
20
- const setAttribute = _ref.current.setAttribute;
21
- _ref.current.setAttribute = (attribute, value) => {
22
- setAttribute.call(_ref.current, attribute, value);
23
- if (attribute === "name") {
24
- set_name(value);
25
- }
26
- };
19
+ const [isTruncated, setIsTruncated] = useState(() => false);
20
+ const [tooltipText, setTooltipText] = useState(() => "");
21
+ const [_resizeObserver, set_resizeObserver] = useState(() => null);
22
+ const [_ariaSelectedListener, set_ariaSelectedListener] = useState(() => null);
23
+ function handleClick(event) {
24
+ if (event && event.preventDefault) {
25
+ event.preventDefault();
26
+ }
27
+ if (!getBoolean(props.disabled) && props.onClick) {
28
+ props.onClick(event);
27
29
  }
28
30
  }
29
- function handleChange(event) {
30
- if (props.onChange) {
31
- props.onChange(event);
31
+ function checkTruncation() {
32
+ if (_labelRef.current) {
33
+ const scrollWidth = Math.ceil(_labelRef.current.scrollWidth);
34
+ const clientWidth = Math.ceil(_labelRef.current.clientWidth);
35
+ const truncated = scrollWidth > clientWidth + 1;
36
+ if (isTruncated !== truncated) {
37
+ setIsTruncated(truncated);
38
+ if (!truncated && _ref.current) {
39
+ if (_ref.current.hasAttribute("data-has-tooltip")) {
40
+ _ref.current.removeAttribute("data-has-tooltip");
41
+ }
42
+ if (_ref.current.hasAttribute("aria-describedby")) {
43
+ _ref.current.removeAttribute("aria-describedby");
44
+ }
45
+ }
46
+ }
47
+ setTooltipText(truncated
48
+ ? props.label ||
49
+ _labelRef.current.innerText ||
50
+ _labelRef.current.textContent ||
51
+ ""
52
+ : "");
32
53
  }
33
54
  }
34
- useEffect(() => {
35
- setBoundSetSelectedOnChange(() => setSelectedOnChange);
36
- setInitialized(true);
37
- }, []);
38
55
  useEffect(() => {
39
56
  var _a;
40
- if (_ref.current && initialized && boundSetSelectedOnChange) {
41
- handleNameAttribute();
42
- setInitialized(false);
43
- // deselect this tab when another tab in tablist is selected
44
- if (!_listenerAdded) {
45
- (_a = _ref.current
46
- .closest("[role=tablist]")) === null || _a === void 0 ? void 0 : _a.addEventListener("change", boundSetSelectedOnChange);
47
- set_listenerAdded(true);
57
+ setInternalActive(getBoolean(props.active) || false);
58
+ setInternalTabIndex(getBoolean(props.active) ? 0 : -1);
59
+ if (typeof window !== "undefined") {
60
+ const setupObserverAndCheck = () => {
61
+ requestAnimationFrame(() => {
62
+ checkTruncation();
63
+ const labelEl = _labelRef.current;
64
+ if (labelEl && !labelEl.dataset.label) {
65
+ labelEl.dataset.label =
66
+ props.label || labelEl.innerText || labelEl.textContent || "";
67
+ }
68
+ if (_labelRef.current) {
69
+ const resizeObserver = new ResizeObserver(() => {
70
+ requestAnimationFrame(() => {
71
+ checkTruncation();
72
+ });
73
+ });
74
+ resizeObserver.observe(_labelRef.current);
75
+ set_resizeObserver(resizeObserver);
76
+ }
77
+ });
78
+ };
79
+ const hasIcon = props.showIcon && props.icon;
80
+ if (hasIcon && ((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)) {
81
+ document.fonts.ready.then(setupObserverAndCheck);
48
82
  }
49
- // Initialize selected state from either active prop (set by parent) or checked attribute
50
- if (props.active || _ref.current.checked) {
51
- set_selected(true);
52
- _ref.current.click();
83
+ else {
84
+ setupObserverAndCheck();
53
85
  }
54
86
  }
55
- }, [_ref.current, initialized, boundSetSelectedOnChange]);
87
+ if (_ref.current) {
88
+ const listener = (event) => {
89
+ setInternalActive(event.detail.selected);
90
+ if (props.tabIndex === undefined) {
91
+ if (event.detail.tabIndex !== undefined) {
92
+ setInternalTabIndex(event.detail.tabIndex);
93
+ }
94
+ else {
95
+ setInternalTabIndex(event.detail.selected ? 0 : -1);
96
+ }
97
+ }
98
+ };
99
+ set_ariaSelectedListener({
100
+ fn: listener,
101
+ });
102
+ _ref.current.addEventListener("aria-selected-changed", listener);
103
+ }
104
+ }, []);
105
+ useEffect(() => {
106
+ if (props.active !== undefined) {
107
+ setInternalActive(getBoolean(props.active) || false);
108
+ }
109
+ }, [props.active]);
56
110
  useEffect(() => {
57
- if (props.name) {
58
- set_name(props.name);
111
+ var _a, _b;
112
+ if (_ref.current) {
113
+ const isDisabled = getBoolean(props.disabled);
114
+ const disabledStr = isDisabled ? "true" : "false";
115
+ if (((_a = _ref.current) === null || _a === void 0 ? void 0 : _a.getAttribute("aria-disabled")) !== disabledStr) {
116
+ (_b = _ref.current) === null || _b === void 0 ? void 0 : _b.setAttribute("aria-disabled", disabledStr);
117
+ }
118
+ if (!isTruncated) {
119
+ if (_ref.current.hasAttribute("data-has-tooltip")) {
120
+ _ref.current.removeAttribute("data-has-tooltip");
121
+ }
122
+ if (_ref.current.hasAttribute("aria-describedby")) {
123
+ _ref.current.removeAttribute("aria-describedby");
124
+ }
125
+ }
59
126
  }
60
- }, [props.name]);
127
+ });
61
128
  useEffect(() => {
62
129
  return () => {
63
- var _a;
64
- if (_listenerAdded && _ref.current && boundSetSelectedOnChange) {
65
- (_a = _ref.current
66
- .closest("[role=tablist]")) === null || _a === void 0 ? void 0 : _a.removeEventListener("change", boundSetSelectedOnChange);
67
- set_listenerAdded(false);
130
+ _resizeObserver === null || _resizeObserver === void 0 ? void 0 : _resizeObserver.disconnect();
131
+ const _listener = _ariaSelectedListener;
132
+ if (_ref.current && _listener) {
133
+ _ref.current.removeEventListener("aria-selected-changed", _listener.fn);
68
134
  }
69
135
  };
70
136
  }, []);
71
- return (React.createElement("li", Object.assign({ role: "none" }, 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-tab-item", props.className) }),
72
- React.createElement("label", { htmlFor: (_a = props.id) !== null && _a !== void 0 ? _a : (_b = props.propOverrides) === null || _b === void 0 ? void 0 : _b.id, "data-icon": (_c = props.iconLeading) !== null && _c !== void 0 ? _c : props.icon, "data-icon-trailing": props.iconTrailing, "data-show-icon": getBooleanAsString((_d = props.showIconLeading) !== null && _d !== void 0 ? _d : props.showIcon), "data-show-icon-trailing": getBooleanAsString(props.showIconTrailing), "data-no-text": getBooleanAsString(props.noText) },
73
- React.createElement("input", Object.assign({ type: "radio", role: "tab", disabled: getBoolean(props.disabled, "disabled"), "aria-selected": _selected, checked: getBoolean(props.checked, "checked"), 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"]), { name: _name, id: (_e = props.id) !== null && _e !== void 0 ? _e : (_f = props.propOverrides) === null || _f === void 0 ? void 0 : _f.id, onInput: (event) => handleChange(event) })),
137
+ return (React.createElement("button", Object.assign({ type: "button", role: "tab", title: "", 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"]), 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-tab-item", props.className), "aria-label": getBoolean(props.noText) ? props.label : undefined, "aria-selected": (props.active !== undefined ? getBoolean(props.active) : internalActive)
138
+ ? "true"
139
+ : "false", "aria-controls": props.ariaControls, disabled: getBoolean(props.disabled) ? true : undefined, tabIndex: getCurrentTabIndex(), id: props.id, "data-active": props.active !== undefined ? getBoolean(props.active) : internalActive, "data-no-text": getBoolean(props.noText) ? "true" : undefined, "data-value": props.value, onClick: (event) => handleClick(event) }),
140
+ !props.noText ? (React.createElement("span", { className: "db-tab-label", title: "", ref: _labelRef, "data-icon": getBoolean((_a = props.showIconLeading) !== null && _a !== void 0 ? _a : props.showIcon)
141
+ ? (_b = props.iconLeading) !== null && _b !== void 0 ? _b : props.icon
142
+ : undefined, "data-icon-trailing": getBoolean(props.showIconTrailing) ? props.iconTrailing : undefined },
74
143
  props.label ? React.createElement(React.Fragment, null, props.label) : null,
75
- props.children)));
144
+ !props.label ? React.createElement(React.Fragment, null, props.children) : null)) : null,
145
+ getBoolean(props.noText) ? (React.createElement("span", { className: "db-tab-label", "aria-hidden": "true", "data-icon": getBoolean((_c = props.showIconLeading) !== null && _c !== void 0 ? _c : props.showIcon)
146
+ ? (_d = props.iconLeading) !== null && _d !== void 0 ? _d : props.icon
147
+ : undefined })) : null,
148
+ isTruncated && tooltipText ? (React.createElement(DBTooltip, { placement: "right" }, tooltipText)) : null));
76
149
  }
77
150
  const DBTabItem = forwardRef(DBTabItemFn);
78
151
  export default DBTabItem;
@@ -1,5 +1,15 @@
1
- import { GlobalProps } from '../../shared/model';
2
- export type DBTabListDefaultProps = {};
3
- export type DBTabListProps = DBTabListDefaultProps & GlobalProps;
4
- export type DBTabListDefaultState = {};
5
- export type DBTabListState = DBTabListDefaultState;
1
+ import { GlobalProps, GlobalState, OrientationProps } from '../../shared/model';
2
+ export type DBTabListDefaultProps = {
3
+ /**
4
+ * Defines a string value that labels the current element (WAI-ARIA).
5
+ */
6
+ ariaLabel?: string;
7
+ /**
8
+ * Identifies the element (or elements) that labels the current element (WAI-ARIA).
9
+ */
10
+ ariaLabelledby?: string;
11
+ };
12
+ export type DBTabListProps = DBTabListDefaultProps & GlobalProps & OrientationProps;
13
+ export interface DBTabListState extends GlobalState {
14
+ _id?: string;
15
+ }
@@ -1,3 +1,3 @@
1
1
  import * as React from "react";
2
- declare const DBTabList: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<any>, keyof import("../..").GlobalProps> & import("../..").GlobalProps & React.RefAttributes<any>>;
2
+ declare const DBTabList: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, keyof import("../..").GlobalProps | keyof import("./model").DBTabListDefaultProps | "orientation"> & import("./model").DBTabListDefaultProps & import("../..").GlobalProps & import("../..").OrientationProps & React.RefAttributes<HTMLDivElement>>;
3
3
  export default DBTabList;
@@ -1,13 +1,17 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { filterPassingProps, getRootProps } from "../../utils/react";
4
- import { useRef, forwardRef } from "react";
4
+ import { useState, useRef, useEffect, forwardRef } from "react";
5
5
  import { cls } from "../../utils";
6
+ import { useId } from "react";
6
7
  function DBTabListFn(props, component) {
7
- var _a, _b;
8
+ const uuid = useId();
8
9
  const _ref = component || useRef(component);
9
- 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", "data-density"]), { id: (_a = props.id) !== null && _a !== void 0 ? _a : (_b = props.propOverrides) === null || _b === void 0 ? void 0 : _b.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-tab-list", props.className) }),
10
- React.createElement("ul", { role: "tablist" }, props.children)));
10
+ const [_id, set_id] = useState(() => "tab-list-base-id");
11
+ useEffect(() => {
12
+ set_id(props.id || "tab-list-" + uuid);
13
+ }, []);
14
+ return (React.createElement("div", Object.assign({ role: "tablist", 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: _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-tab-list", props.className), "aria-orientation": props.orientation || "horizontal", "aria-label": props.ariaLabelledby ? undefined : props.ariaLabel, "aria-labelledby": props.ariaLabelledby }), props.children));
11
15
  }
12
16
  const DBTabList = forwardRef(DBTabListFn);
13
17
  export default DBTabList;
@@ -1,10 +1,20 @@
1
- import { GlobalProps, GlobalState } from '../../shared/model';
1
+ import { GlobalProps } from '../../shared/model';
2
2
  export type DBTabPanelDefaultProps = {
3
3
  /**
4
4
  * The content if you don't want to use children.
5
5
  */
6
6
  content?: string;
7
+ /**
8
+ * If the panel is hidden.
9
+ */
10
+ hidden?: boolean;
11
+ /**
12
+ * The id of the tab that labels this panel (WAI-ARIA).
13
+ */
14
+ ariaLabelledby?: string;
15
+ /**
16
+ * Accessible label for the panel, overrides ariaLabelledby for the accessible name.
17
+ */
18
+ ariaLabel?: string;
7
19
  };
8
20
  export type DBTabPanelProps = DBTabPanelDefaultProps & GlobalProps;
9
- export type DBTabPanelDefaultState = {};
10
- export type DBTabPanelState = DBTabPanelDefaultState & GlobalState;
@@ -1,3 +1,3 @@
1
1
  import * as React from "react";
2
- declare const DBTabPanel: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<any>, "content" | keyof import("../..").GlobalProps> & import("./model").DBTabPanelDefaultProps & import("../..").GlobalProps & React.RefAttributes<any>>;
2
+ declare const DBTabPanel: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, keyof import("../..").GlobalProps | keyof import("./model").DBTabPanelDefaultProps> & import("./model").DBTabPanelDefaultProps & import("../..").GlobalProps & React.RefAttributes<HTMLDivElement>>;
3
3
  export default DBTabPanel;
@@ -1,13 +1,12 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { filterPassingProps, getRootProps } from "../../utils/react";
4
- import { useRef, useEffect, forwardRef } from "react";
4
+ import { useRef, forwardRef } from "react";
5
5
  import { cls } from "../../utils";
6
6
  function DBTabPanelFn(props, component) {
7
7
  var _a, _b;
8
8
  const _ref = component || useRef(component);
9
- useEffect(() => { }, []);
10
- return (React.createElement("section", Object.assign({ role: "tabpanel", 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"]), 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-tab-panel", props.className), id: (_a = props.id) !== null && _a !== void 0 ? _a : (_b = props.propOverrides) === null || _b === void 0 ? void 0 : _b.id }),
9
+ return (React.createElement("div", Object.assign({ role: "tabpanel", 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"]), 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-tab-panel", props.className), id: (_a = props.id) !== null && _a !== void 0 ? _a : (_b = props.propOverrides) === null || _b === void 0 ? void 0 : _b.id, tabIndex: props.hidden ? -1 : 0, hidden: props.hidden, "aria-label": props.ariaLabel, "aria-labelledby": props.ariaLabel ? undefined : props.ariaLabelledby }),
11
10
  props.content ? React.createElement(React.Fragment, null, props.content) : null,
12
11
  props.children));
13
12
  }
@@ -1,4 +1,4 @@
1
- import { AlignmentProps, GlobalProps, InitializedState, InputEvent, OrientationProps, WidthProps } from '../../shared/model';
1
+ import { GlobalProps, InitializedState, OrientationProps, TabItemAlignmentProps, WidthType } from '../../shared/model';
2
2
  import { DBTabItemProps } from '../tab-item/model';
3
3
  import { DBTabPanelProps } from '../tab-panel/model';
4
4
  export declare const TabsBehaviorList: readonly ["scrollbar", "arrows"];
@@ -19,6 +19,12 @@ export type DBTabsDefaultProps = {
19
19
  * Default behavior is auto selecting the first tab, change selected tab by index
20
20
  */
21
21
  initialSelectedIndex?: number | string;
22
+ /**
23
+ * Controlled active tab index. When set, the component becomes controlled:
24
+ * the consumer is responsible for updating this value in the onIndexChange handler.
25
+ * Takes precedence over initialSelectedIndex after mount.
26
+ */
27
+ activeIndex?: number | string;
22
28
  /**
23
29
  * Default behavior is auto selecting the first tab, disable it with 'manually'
24
30
  */
@@ -31,6 +37,18 @@ export type DBTabsDefaultProps = {
31
37
  * Provide simple tabs with label + text as content
32
38
  */
33
39
  tabs?: DBSimpleTabProps[] | string;
40
+ /**
41
+ * Width of the tab-items. Auto width based on tab-item size, full width based on parent elements width.
42
+ */
43
+ tabItemWidth?: WidthType | string;
44
+ /**
45
+ * Accessible label for the "scroll towards start" button (i18n). Only used with behavior="arrows".
46
+ */
47
+ scrollStartLabel?: string;
48
+ /**
49
+ * Accessible label for the "scroll towards end" button (i18n). Only used with behavior="arrows".
50
+ */
51
+ scrollEndLabel?: string;
34
52
  };
35
53
  export type DBTabsEventProps = {
36
54
  /**
@@ -42,26 +60,42 @@ export type DBTabsEventProps = {
42
60
  */
43
61
  onIndexChange?: (index?: number) => void;
44
62
  /**
45
- * Informs the user if another tab has been selected.
46
- */
47
- onTabSelect?: (event?: InputEvent<HTMLElement>) => void;
48
- /**
49
- * Informs the user if another tab has been selected.
63
+ * Fires when the active tab changes and a `value` prop is set on the tab items.
64
+ * Payload is the `value` string of the newly active tab item, or undefined
65
+ * if the tab item has no `value` prop set.
66
+ * Use this for form binding (e.g. Angular FormControl, React controlled state).
50
67
  */
51
- tabSelect?: (event?: InputEvent<HTMLElement>) => void;
68
+ onValueChange?: (value?: string) => void;
52
69
  };
53
- export type DBTabsProps = DBTabsDefaultProps & GlobalProps & OrientationProps & WidthProps & AlignmentProps & DBTabsEventProps;
70
+ export type DBTabsProps = DBTabsDefaultProps & GlobalProps & OrientationProps & TabItemAlignmentProps & DBTabsEventProps;
54
71
  export type DBTabsDefaultState = {
55
- _name: string;
56
- scrollContainer?: Element | null;
57
- scroll: (left?: boolean) => void;
58
- showScrollLeft?: boolean;
59
- showScrollRight?: boolean;
72
+ _generatedId: string;
73
+ _generatedName: string;
74
+ _id: () => string;
75
+ _name: () => string;
76
+ _getScrollContainer: () => Element | null;
77
+ scroll: (toStart?: boolean) => void;
78
+ showScrollStart?: boolean;
79
+ showScrollEnd?: boolean;
80
+ _isRtl: () => boolean;
60
81
  evaluateScrollButtons: (tabList: Element) => void;
61
- convertTabs: () => DBSimpleTabProps[];
82
+ _cachedTabs: DBSimpleTabProps[];
83
+ _updateCachedTabs: () => void;
62
84
  initTabList: () => void;
63
- initTabs: (init?: boolean) => void;
64
- handleChange: (event: InputEvent<HTMLElement>) => void;
65
- _resizeObserver?: ResizeObserver;
85
+ initTabs: (activeIndex?: number) => void;
86
+ _resizeObserver?: ResizeObserver | null;
87
+ _observer?: MutationObserver | null;
88
+ _pendingRafId: number | null;
89
+ _scrollListener: {
90
+ fn: () => void;
91
+ } | null;
92
+ activeTabIndex: number;
93
+ activateTab: (index: number) => void;
94
+ getTabId: (index: number | string) => string;
95
+ getPanelId: (index: number | string) => string;
96
+ handleClick: (event: any) => void;
97
+ handleKeyDown: (event: any) => void;
98
+ isIndexActive: (index: number | string) => boolean;
99
+ getTabItemTabIndex: (index: number | string) => 0 | -1;
66
100
  };
67
101
  export type DBTabsState = DBTabsDefaultState & InitializedState;
@@ -1,3 +1,3 @@
1
1
  import * as React from "react";
2
- declare const DBTabs: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<any>, keyof import("../../shared/model").GlobalProps | "width" | "alignment" | keyof import("./model").DBTabsDefaultProps | "orientation" | keyof import("./model").DBTabsEventProps> & import("./model").DBTabsDefaultProps & import("../../shared/model").GlobalProps & import("../../shared/model").OrientationProps & import("../../shared/model").WidthProps & import("../../shared/model").AlignmentProps & import("./model").DBTabsEventProps & React.RefAttributes<any>>;
2
+ declare const DBTabs: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, keyof import("../..").GlobalProps | "orientation" | keyof import("./model").DBTabsDefaultProps | "tabItemAlignment" | keyof import("./model").DBTabsEventProps> & import("./model").DBTabsDefaultProps & import("../..").GlobalProps & import("../..").OrientationProps & import("../..").TabItemAlignmentProps & import("./model").DBTabsEventProps & React.RefAttributes<HTMLDivElement>>;
3
3
  export default DBTabs;
@@ -9,172 +9,414 @@ import DBTabList from "../tab-list/tab-list";
9
9
  import DBTabPanel from "../tab-panel/tab-panel";
10
10
  import { useId } from "react";
11
11
  function DBTabsFn(props, component) {
12
- var _a, _b, _c, _d, _e, _f;
12
+ var _a, _b, _c;
13
+ props = Object.assign({ tabItemWidth: "auto", tabItemAlignment: "start", scrollStartLabel: "Scroll start", scrollEndLabel: "Scroll end" }, props);
13
14
  const uuid = useId();
14
15
  const _ref = component || useRef(component);
15
- const [_name, set_name] = useState(() => "");
16
+ const [_generatedId, set_generatedId] = useState(() => "tabs-" + uuid);
17
+ const [_generatedName, set_generatedName] = useState(() => uuid);
18
+ const [activeTabIndex, setActiveTabIndex] = useState(() => 0);
16
19
  const [initialized, setInitialized] = useState(() => false);
17
- const [showScrollLeft, setShowScrollLeft] = useState(() => false);
18
- const [showScrollRight, setShowScrollRight] = useState(() => false);
19
- const [scrollContainer, setScrollContainer] = useState(() => null);
20
- const [_resizeObserver, set_resizeObserver] = useState(() => undefined);
21
- function convertTabs() {
20
+ const [showScrollStart, setShowScrollStart] = useState(() => false);
21
+ const [showScrollEnd, setShowScrollEnd] = useState(() => false);
22
+ const [_resizeObserver, set_resizeObserver] = useState(() => null);
23
+ const [_observer, set_observer] = useState(() => null);
24
+ const [_pendingRafId, set_pendingRafId] = useState(() => null);
25
+ const [_scrollListener, set_scrollListener] = useState(() => null);
26
+ function _id() {
27
+ return props.id || _generatedId;
28
+ }
29
+ function _name() {
30
+ return "tabs-" + (props.name || _generatedName);
31
+ }
32
+ function getTabId(index) {
33
+ return `${_name()}-tab-${index}`;
34
+ }
35
+ function getPanelId(index) {
36
+ return `${_name()}-tab-panel-${index}`;
37
+ }
38
+ function activateTab(index) {
39
+ var _a, _b, _c;
40
+ // Prevent activating a disabled tab
41
+ if (_ref.current) {
42
+ const tabList = _ref.current.querySelector('[role="tablist"]');
43
+ if (tabList) {
44
+ const tabs = Array.from(tabList.querySelectorAll('[role="tab"]'));
45
+ const tab = tabs[index];
46
+ if ((tab === null || tab === void 0 ? void 0 : tab.disabled) || (tab === null || tab === void 0 ? void 0 : tab.getAttribute("aria-disabled")) === "true") {
47
+ return;
48
+ }
49
+ }
50
+ }
51
+ if (activeTabIndex !== index) {
52
+ setActiveTabIndex(index);
53
+ if (props.onIndexChange) {
54
+ props.onIndexChange(index);
55
+ }
56
+ // Emit value of the newly active tab item if value props are set
57
+ if (props.onValueChange) {
58
+ const tabList = (_a = _ref.current) === null || _a === void 0 ? void 0 : _a.querySelector('[role="tablist"]');
59
+ const tabs = tabList
60
+ ? Array.from(tabList.querySelectorAll('[role="tab"]'))
61
+ : [];
62
+ const value = (_c = (_b = tabs[index]) === null || _b === void 0 ? void 0 : _b.dataset) === null || _c === void 0 ? void 0 : _c["value"];
63
+ props.onValueChange(value);
64
+ }
65
+ initTabs(index);
66
+ }
67
+ }
68
+ function handleClick(event) {
69
+ var _a;
70
+ // In props-mode (props.tabs), tab activation is handled via onClick on each DBTabItem directly.
71
+ // In slot-mode (!props.tabs), clicks bubble up and are handled here via DOM traversal.
72
+ if (props.tabs) {
73
+ return;
74
+ }
75
+ const target = event.target;
76
+ const button = target.closest('[role="tab"]');
77
+ if (!button || !_ref.current)
78
+ return;
79
+ const tabList = (_a = _ref.current) === null || _a === void 0 ? void 0 : _a.querySelector('[role="tablist"]');
80
+ if (!tabList)
81
+ return;
82
+ const buttons = Array.from(tabList.querySelectorAll('[role="tab"]'));
83
+ const index = buttons.indexOf(button);
84
+ if (index !== -1) {
85
+ event.preventDefault();
86
+ activateTab(index);
87
+ }
88
+ }
89
+ function handleKeyDown(event) {
90
+ var _a;
91
+ if (!_ref.current)
92
+ return;
93
+ const key = event.key;
94
+ const navigationKeys = [
95
+ "ArrowRight",
96
+ "ArrowDown",
97
+ "ArrowLeft",
98
+ "ArrowUp",
99
+ "Home",
100
+ "End",
101
+ "Enter",
102
+ " ",
103
+ ];
104
+ if (!navigationKeys.includes(key)) {
105
+ return;
106
+ }
107
+ const tabList = _ref.current.querySelector('[role="tablist"]');
108
+ if (!tabList)
109
+ return;
110
+ const buttons = Array.from(tabList.querySelectorAll('[role="tab"]'));
111
+ // find currently focused element within the buttons list
112
+ let currentIndex = -1;
113
+ if (typeof document !== "undefined") {
114
+ // Traverse Shadow DOM boundaries to find the truly focused element.
115
+ // document.activeElement only returns the shadow host when focus is inside a Shadow DOM,
116
+ // so we must walk through each shadowRoot to reach the actual focused element.
117
+ let activeEl = document.activeElement;
118
+ while ((_a = activeEl === null || activeEl === void 0 ? void 0 : activeEl.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) {
119
+ activeEl = activeEl.shadowRoot.activeElement;
120
+ }
121
+ if (activeEl) {
122
+ const focusedButton = activeEl.closest('[role="tab"]');
123
+ if (focusedButton) {
124
+ currentIndex = buttons.indexOf(focusedButton);
125
+ }
126
+ }
127
+ }
128
+ if (currentIndex === -1) {
129
+ currentIndex = activeTabIndex;
130
+ }
131
+ if (buttons.length > 0) {
132
+ // handle activation (enter / space) -> change panel
133
+ if (key === "Enter" || key === " ") {
134
+ event.preventDefault();
135
+ activateTab(currentIndex);
136
+ return;
137
+ }
138
+ // handle navigation (arrows) -> moves focus
139
+ let nextIndex;
140
+ const length = buttons.length;
141
+ if (key === "ArrowRight" || key === "ArrowDown") {
142
+ nextIndex = (currentIndex + 1) % length;
143
+ }
144
+ else if (key === "ArrowLeft" || key === "ArrowUp") {
145
+ nextIndex = (currentIndex - 1 + length) % length;
146
+ }
147
+ else if (key === "Home") {
148
+ nextIndex = 0;
149
+ }
150
+ else if (key === "End") {
151
+ nextIndex = length - 1;
152
+ }
153
+ if (nextIndex !== undefined) {
154
+ event.preventDefault();
155
+ // Skip disabled tabs when navigating with arrow keys
156
+ const isForward = key === "ArrowRight" || key === "ArrowDown";
157
+ const maxAttempts = length;
158
+ for (let i = 0; i < maxAttempts; i++) {
159
+ const candidate = buttons[nextIndex];
160
+ if (!(candidate === null || candidate === void 0 ? void 0 : candidate.disabled) &&
161
+ (candidate === null || candidate === void 0 ? void 0 : candidate.getAttribute("aria-disabled")) !== "true") {
162
+ break;
163
+ }
164
+ if (isForward) {
165
+ nextIndex = (nextIndex + 1) % length;
166
+ }
167
+ else {
168
+ nextIndex = (nextIndex - 1 + length) % length;
169
+ }
170
+ }
171
+ // do not activateTab here for manual activation, just move the focus
172
+ const nextButton = buttons[nextIndex];
173
+ if (nextButton &&
174
+ !nextButton.disabled &&
175
+ nextButton.getAttribute("aria-disabled") !== "true") {
176
+ nextButton.focus();
177
+ }
178
+ }
179
+ }
180
+ }
181
+ function isIndexActive(index) {
182
+ return activeTabIndex === Number(index);
183
+ }
184
+ function getTabItemTabIndex(index) {
185
+ const i = Number(index);
186
+ // only the active tab should be reachable via Tab key
187
+ return activeTabIndex === i || (activeTabIndex === -1 && i === 0) ? 0 : -1;
188
+ }
189
+ const [_cachedTabs, set_cachedTabs] = useState(() => []);
190
+ function _updateCachedTabs() {
22
191
  try {
23
192
  if (typeof props.tabs === "string") {
24
- return JSON.parse(props.tabs);
193
+ set_cachedTabs(JSON.parse(props.tabs));
194
+ }
195
+ else if (props.tabs) {
196
+ set_cachedTabs(props.tabs);
197
+ }
198
+ else {
199
+ set_cachedTabs([]);
25
200
  }
26
- return props.tabs;
27
201
  }
28
202
  catch (error) {
29
203
  console.error(error);
204
+ set_cachedTabs([]);
30
205
  }
31
- return [];
206
+ }
207
+ function _getScrollContainer() {
208
+ var _a, _b;
209
+ return (_b = (_a = _ref.current) === null || _a === void 0 ? void 0 : _a.querySelector('[role="tablist"]')) !== null && _b !== void 0 ? _b : null;
210
+ }
211
+ function _isRtl() {
212
+ const container = _getScrollContainer();
213
+ return (!!container &&
214
+ typeof getComputedStyle !== "undefined" &&
215
+ getComputedStyle(container).direction === "rtl");
32
216
  }
33
217
  function evaluateScrollButtons(tList) {
34
218
  const needsScroll = tList.scrollWidth > tList.clientWidth;
35
- setShowScrollLeft(needsScroll && tList.scrollLeft > 1);
36
- setShowScrollRight(needsScroll && tList.scrollLeft < tList.scrollWidth - tList.clientWidth);
219
+ if (!needsScroll) {
220
+ setShowScrollStart(false);
221
+ setShowScrollEnd(false);
222
+ return;
223
+ }
224
+ const scrollPos = Math.abs(tList.scrollLeft);
225
+ const maxScroll = tList.scrollWidth - tList.clientWidth;
226
+ const tolerance = 2;
227
+ // scrollPos=0 means "at inline-start" in both LTR and RTL
228
+ setShowScrollStart(scrollPos > tolerance);
229
+ setShowScrollEnd(scrollPos < maxScroll - tolerance);
37
230
  }
38
- function scroll(left) {
39
- let step = Number(props.arrowScrollDistance) || 100;
40
- if (left) {
231
+ function scroll(toStart) {
232
+ const container = _getScrollContainer();
233
+ if (!container) {
234
+ return;
235
+ }
236
+ let step = Number(props.arrowScrollDistance) || 120;
237
+ const isLeft = !!toStart;
238
+ const isRtl = _isRtl();
239
+ // Map logical direction (start/end) to physical direction.
240
+ // In LTR: toStart=true → scroll left (negative), toEnd → scroll right (positive).
241
+ // In RTL: directions are inverted physically.
242
+ if (isLeft !== isRtl) {
41
243
  step *= -1;
42
244
  }
43
- scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.scrollBy({
44
- top: 0,
245
+ container.scrollBy({
45
246
  left: step,
46
247
  behavior: "smooth",
47
248
  });
48
249
  }
49
250
  function initTabList() {
251
+ var _a, _b;
50
252
  if (_ref.current) {
51
- const tabList = _ref.current.querySelector(".db-tab-list");
52
- if (tabList) {
53
- const container = tabList.querySelector('[role="tablist"]');
54
- if (container) {
55
- container.setAttribute("aria-orientation", props.orientation || "horizontal");
56
- if (props.behavior === "arrows") {
57
- setScrollContainer(container);
58
- evaluateScrollButtons(container);
59
- container.addEventListener("scroll", () => {
253
+ const container = _ref.current.querySelector('[role="tablist"]');
254
+ if (container) {
255
+ if (!container.getAttribute("aria-orientation")) {
256
+ container.setAttribute("aria-orientation", (_a = props.orientation) !== null && _a !== void 0 ? _a : "horizontal");
257
+ }
258
+ if (props.behavior === "arrows") {
259
+ evaluateScrollButtons(container);
260
+ const _listener = _scrollListener;
261
+ if (_listener && container) {
262
+ container.removeEventListener("scroll", _listener.fn);
263
+ set_scrollListener(null);
264
+ }
265
+ const onScroll = () => evaluateScrollButtons(container);
266
+ set_scrollListener({
267
+ fn: onScroll,
268
+ });
269
+ container.addEventListener("scroll", onScroll);
270
+ if (!_resizeObserver) {
271
+ const observer = new ResizeObserver(() => {
60
272
  evaluateScrollButtons(container);
61
273
  });
62
- // Use ResizeObserver to re-evaluate scroll buttons because it provides more accurate, container-specific resize detection than global window resize events.
63
- if (!_resizeObserver) {
64
- const observer = new ResizeObserver(() => {
65
- evaluateScrollButtons(container);
66
- });
67
- observer.observe(container);
68
- set_resizeObserver(observer);
69
- }
274
+ observer.observe(container);
275
+ set_resizeObserver(observer);
70
276
  }
71
277
  }
278
+ if (props.name && !container.getAttribute("aria-label")) {
279
+ container.setAttribute("aria-label", (_b = props.name) !== null && _b !== void 0 ? _b : "");
280
+ }
72
281
  }
73
282
  }
74
283
  }
75
- function initTabs(init) {
284
+ function initTabs(activeIndex) {
285
+ var _a, _b;
286
+ const currentIndex = activeIndex !== undefined ? activeIndex : activeTabIndex;
76
287
  if (_ref.current) {
77
- const tabItems = Array.from(_ref.current.getElementsByClassName("db-tab-item"));
78
- const tabPanels = Array.from(_ref.current.querySelectorAll(":is(:scope > .db-tab-panel, :scope > db-tab-panel > .db-tab-panel)"));
79
- for (const tabItem of tabItems) {
80
- const index = tabItems.indexOf(tabItem);
81
- const label = tabItem.querySelector("label");
82
- const input = tabItem.querySelector("input");
83
- if (input && label) {
84
- if (!input.id) {
85
- const tabId = `${_name}-tab-${index}`;
86
- label.setAttribute("for", tabId);
87
- input.id = tabId;
88
- input.setAttribute("name", _name);
89
- if (tabPanels.length > index) {
90
- input.setAttribute("aria-controls", `${_name}-tab-panel-${index}`);
91
- }
288
+ const tabListEl = _ref.current.querySelector('[role="tablist"]');
289
+ const panels = Array.from((_b = (_a = _ref.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('[role="tabpanel"]')) !== null && _b !== void 0 ? _b : []).filter((panel) => panel.closest(".db-tabs") === _ref.current);
290
+ if (!tabListEl)
291
+ return;
292
+ const buttons = Array.from(tabListEl.querySelectorAll('[role="tab"]'));
293
+ buttons.forEach((button, index) => {
294
+ const isSelected = currentIndex === index;
295
+ const panel = panels[index];
296
+ const tabId = button.id || getTabId(index);
297
+ const panelId = (panel === null || panel === void 0 ? void 0 : panel.id) || getPanelId(index);
298
+ if (!button.id) {
299
+ button.id = tabId;
300
+ }
301
+ if (!button.getAttribute("aria-controls")) {
302
+ button.setAttribute("aria-controls", panelId);
303
+ }
304
+ button.dispatchEvent(new CustomEvent("aria-selected-changed", {
305
+ detail: {
306
+ selected: isSelected,
307
+ tabIndex: currentIndex === index || (currentIndex === -1 && index === 0)
308
+ ? 0
309
+ : -1,
310
+ },
311
+ }));
312
+ if (panel) {
313
+ if (!panel.id) {
314
+ panel.id = panelId;
92
315
  }
93
- if (init) {
94
- // Auto select
95
- const autoSelect = !props.initialSelectedMode ||
96
- props.initialSelectedMode === "auto";
97
- const shouldAutoSelect = (props.initialSelectedIndex == null && index === 0) ||
98
- Number(props.initialSelectedIndex) === index;
99
- if (autoSelect && shouldAutoSelect) {
100
- input.click();
101
- }
316
+ if (!panel.getAttribute("aria-label") &&
317
+ !panel.getAttribute("aria-labelledby")) {
318
+ panel.setAttribute("aria-labelledby", tabId);
102
319
  }
320
+ // toggle visibility
321
+ panel.hidden = !isSelected;
103
322
  }
104
- }
105
- for (const panel of tabPanels) {
106
- if (panel.id)
107
- continue;
108
- const index = tabPanels.indexOf(panel);
109
- panel.id = `${_name}-tab-panel-${index}`;
110
- panel.setAttribute("aria-labelledby", `${_name}-tab-${index}`);
111
- }
323
+ });
112
324
  }
113
325
  }
114
- function handleChange(event) {
115
- var _a;
116
- event.stopPropagation();
117
- if (event.target) {
118
- const target = event.target;
119
- const parent = target.parentElement;
120
- if (parent &&
121
- parent.parentElement &&
122
- ((_a = parent.parentElement) === null || _a === void 0 ? void 0 : _a.nodeName) === "LI") {
123
- const tabItem = parent.parentElement;
124
- if (tabItem) {
125
- const list = tabItem.parentElement;
126
- if (list) {
127
- const tabIndex = Array.from(list.children).indexOf(tabItem);
128
- if (props.onIndexChange) {
129
- props.onIndexChange(tabIndex);
130
- }
131
- if (props.onTabSelect) {
132
- props.onTabSelect(event);
133
- }
134
- }
326
+ useEffect(() => {
327
+ // 1. Calculate final start index synchronously to avoid race conditions
328
+ let startIndex = 0;
329
+ if (props.initialSelectedIndex !== undefined) {
330
+ const parsedIndex = Number(props.initialSelectedIndex);
331
+ startIndex = isNaN(parsedIndex) ? 0 : parsedIndex;
332
+ }
333
+ else if (props.initialSelectedMode === "manually") {
334
+ startIndex = -1;
335
+ }
336
+ // 2. Support deep linking: URL hash takes precedence over initial index
337
+ if (typeof window !== "undefined" && window.location.hash) {
338
+ const hashId = window.location.hash.substring(1);
339
+ const name = props.name ? "tabs-" + props.name : _name();
340
+ const prefix = `${name}-tab-`;
341
+ if (hashId.startsWith(prefix)) {
342
+ const indexStr = hashId.replace(prefix, "");
343
+ const index = parseInt(indexStr, 10);
344
+ if (!isNaN(index)) {
345
+ startIndex = index;
135
346
  }
136
347
  }
137
348
  }
138
- }
139
- useEffect(() => {
140
- set_name(`tabs-${props.name || uuid}`);
349
+ // 3. Set initial state synchronously
350
+ setActiveTabIndex(startIndex);
141
351
  setInitialized(true);
142
- }, []);
143
- useEffect(() => {
144
- if (_ref.current && initialized) {
145
- initTabList();
146
- initTabs(true);
147
- const tabList = _ref.current.querySelector(".db-tab-list");
148
- if (tabList) {
149
- const observer = new MutationObserver((mutations) => {
150
- mutations.forEach((mutation) => {
151
- if (mutation.removedNodes.length || mutation.addedNodes.length) {
152
- initTabList();
153
- initTabs();
154
- }
155
- });
352
+ _updateCachedTabs();
353
+ // 4. Trigger single initial DOM update after paint
354
+ if (typeof window !== "undefined") {
355
+ requestAnimationFrame(() => {
356
+ initTabList();
357
+ initTabs(startIndex);
358
+ });
359
+ }
360
+ if (_ref.current) {
361
+ const tabListEl = _ref.current.querySelector('[role="tablist"]');
362
+ if (tabListEl) {
363
+ const observer = new MutationObserver(() => {
364
+ const rafId = _pendingRafId;
365
+ if (rafId !== null)
366
+ cancelAnimationFrame(rafId);
367
+ set_pendingRafId(requestAnimationFrame(() => {
368
+ set_pendingRafId(null);
369
+ initTabList();
370
+ initTabs(activeTabIndex);
371
+ }));
156
372
  });
157
- observer.observe(tabList, {
373
+ // Observe only the tablist (not panel content) to avoid unnecessary
374
+ // re-evaluations when user content inside panels changes.
375
+ // childList only – attribute changes (set by initTabs) are not observed, preventing infinite loops.
376
+ observer.observe(tabListEl, {
158
377
  childList: true,
159
378
  subtree: true,
160
379
  });
380
+ set_observer(observer);
161
381
  }
162
- setInitialized(false);
163
382
  }
164
- }, [_ref.current, initialized]);
383
+ }, []);
384
+ useEffect(() => {
385
+ _updateCachedTabs();
386
+ }, [props.tabs]);
387
+ useEffect(() => {
388
+ if (props.activeIndex !== undefined) {
389
+ const newIndex = Number(props.activeIndex);
390
+ if (!isNaN(newIndex) && newIndex !== activeTabIndex) {
391
+ activateTab(newIndex);
392
+ }
393
+ }
394
+ }, [props.activeIndex]);
165
395
  useEffect(() => {
166
396
  return () => {
397
+ const rafId = _pendingRafId;
398
+ if (rafId !== null) {
399
+ cancelAnimationFrame(rafId);
400
+ set_pendingRafId(null);
401
+ }
402
+ const _listener = _scrollListener;
403
+ const _container = _getScrollContainer();
404
+ if (_listener && _container) {
405
+ _container.removeEventListener("scroll", _listener.fn);
406
+ }
167
407
  _resizeObserver === null || _resizeObserver === void 0 ? void 0 : _resizeObserver.disconnect();
168
- set_resizeObserver(undefined);
408
+ set_resizeObserver(null);
409
+ _observer === null || _observer === void 0 ? void 0 : _observer.disconnect();
410
+ set_observer(null);
169
411
  };
170
412
  }, []);
171
- 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", "data-density", "onTabSelect", "onIndexChange"]), { id: (_a = props.id) !== null && _a !== void 0 ? _a : (_b = props.propOverrides) === null || _b === void 0 ? void 0 : _b.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-tabs", props.className), "data-orientation": props.orientation, "data-scroll-behavior": props.behavior, "data-alignment": (_c = props.alignment) !== null && _c !== void 0 ? _c : "start", "data-width": (_d = props.width) !== null && _d !== void 0 ? _d : "auto", onInput: (event) => handleChange(event), onChange: (event) => handleChange(event) }),
172
- showScrollLeft ? (React.createElement(DBButton, { className: "tabs-scroll-left", variant: "ghost", icon: "chevron_left", type: "button", noText: true, onClick: (event) => scroll(true) }, "Scroll left")) : null,
413
+ 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", "data-density", "onTabSelect", "onIndexChange"]), { id: (_c = (_a = props.id) !== null && _a !== void 0 ? _a : (_b = props.propOverrides) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : _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-tabs", props.className), "data-orientation": props.orientation, "data-scroll-behavior": props.behavior, "data-tab-item-alignment": props.tabItemAlignment, "data-tab-item-width": props.tabItemWidth, onClick: (event) => handleClick(event), onKeyDown: (event) => handleKeyDown(event) }),
414
+ showScrollStart ? (React.createElement(DBButton, { className: "tabs-scroll-start", variant: "ghost", icon: "chevron_left", type: "button", noText: true, onClick: (event) => scroll(true) }, props.scrollStartLabel)) : null,
173
415
  props.tabs ? (React.createElement(React.Fragment, null,
174
- React.createElement(DBTabList, null, (_e = convertTabs()) === null || _e === void 0 ? void 0 : _e.map((tab, index) => (React.createElement(DBTabItem, { key: props.name + "tab-item" + index, active: tab.active, label: tab.label, iconTrailing: tab.iconTrailing, icon: tab.icon, noText: tab.noText })))), (_f = convertTabs()) === null || _f === void 0 ? void 0 :
175
- _f.map((tab, index) => (React.createElement(DBTabPanel, { key: props.name + "tab-panel" + index, content: tab.content }, tab.children))))) : null,
176
- showScrollRight ? (React.createElement(DBButton, { className: "tabs-scroll-right", variant: "ghost", icon: "chevron_right", type: "button", noText: true, onClick: (event) => scroll() }, "Scroll right")) : null,
177
- props.children));
416
+ React.createElement(DBTabList, { orientation: props.orientation, ariaLabel: props.name }, _cachedTabs === null || _cachedTabs === void 0 ? void 0 : _cachedTabs.map((tab, index) => (React.createElement(DBTabItem, { key: props.name + "tab-item" + index, id: getTabId(index), ariaControls: getPanelId(index), active: isIndexActive(index), tabIndex: getTabItemTabIndex(index), label: tab.label, iconTrailing: tab.iconTrailing, icon: tab.icon, noText: tab.noText, onClick: (event) => activateTab(index) })))), _cachedTabs === null || _cachedTabs === void 0 ? void 0 :
417
+ _cachedTabs.map((tab, index) => (React.createElement(DBTabPanel, { key: props.name + "tab-panel" + index, id: getPanelId(index), ariaLabelledby: getTabId(index), content: tab.content, hidden: !isIndexActive(index) }, tab.children))))) : null,
418
+ !props.tabs ? React.createElement(React.Fragment, null, props.children) : null,
419
+ showScrollEnd ? (React.createElement(DBButton, { className: "tabs-scroll-end", variant: "ghost", icon: "chevron_right", type: "button", noText: true, onClick: (event) => scroll(false) }, props.scrollEndLabel)) : null));
178
420
  }
179
421
  const DBTabs = forwardRef(DBTabsFn);
180
422
  export default DBTabs;
@@ -2,7 +2,6 @@
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 { DEFAULT_ID } from "../../shared/constants";
6
5
  import { cls, getBooleanAsString, delay as utilsDelay } from "../../utils";
7
6
  import { DocumentScrollListener } from "../../utils/document-scroll-listener";
8
7
  import { handleFixedPopover } from "../../utils/floating-components";
@@ -11,7 +10,7 @@ function DBTooltipFn(props, component) {
11
10
  var _a, _b, _c;
12
11
  const uuid = useId();
13
12
  const _ref = component || useRef(component);
14
- const [_id, set_id] = useState(() => DEFAULT_ID);
13
+ const [_id, set_id] = useState(() => "tooltip-" + uuid);
15
14
  const [initialized, setInitialized] = useState(() => false);
16
15
  const [_documentScrollListenerCallbackId, set_documentScrollListenerCallbackId,] = useState(() => undefined);
17
16
  const [_observer, set_observer] = useState(() => undefined);
@@ -461,13 +461,13 @@ export type CloseEventProps<T> = {
461
461
  export type CloseEventState<T> = {
462
462
  handleClose: (event?: T | void, forceClose?: boolean) => void;
463
463
  };
464
- export declare const AlignmentList: readonly ["start", "center"];
465
- export type AlignmentType = (typeof AlignmentList)[number];
466
- export type AlignmentProps = {
464
+ export declare const TabItemAlignmentList: readonly ["start", "center", "end"];
465
+ export type TabItemAlignmentType = (typeof TabItemAlignmentList)[number];
466
+ export type TabItemAlignmentProps = {
467
467
  /**
468
- * Define the content alignment in full width
468
+ * Define the tab-item alignment in full width
469
469
  */
470
- alignment?: AlignmentType | string;
470
+ tabItemAlignment?: TabItemAlignmentType | string;
471
471
  };
472
472
  export type ActiveProps = {
473
473
  /**
@@ -19,4 +19,4 @@ export const LabelVariantHorizontalList = ['leading', 'trailing'];
19
19
  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'];
20
20
  export const LinkTargetList = ['_self', '_blank', '_parent', '_top'];
21
21
  export const LinkReferrerPolicyList = ['no-referrer', 'no-referrer-when-downgrade', 'origin', 'origin-when-cross-origin', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url'];
22
- export const AlignmentList = ['start', 'center'];
22
+ export const TabItemAlignmentList = ['start', 'center', 'end'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@db-ux/react-core-components",
3
- "version": "4.6.1",
3
+ "version": "4.7.0-tabs-34782eb",
4
4
  "description": "React components for @db-ux/core-components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "sideEffects": false,
43
43
  "dependencies": {
44
- "@db-ux/core-components": "4.6.1",
45
- "@db-ux/core-foundations": "4.6.1"
44
+ "@db-ux/core-components": "4.7.0-tabs-34782eb",
45
+ "@db-ux/core-foundations": "4.7.0-tabs-34782eb"
46
46
  }
47
47
  }