@jetbrains/ring-ui 7.0.59 → 7.0.60-beta.0

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![official JetBrains project](https://jb.gg/badges/official-flat-square.svg)](https://github.com/JetBrains#jetbrains-on-github)
5
5
 
6
6
  - [Design guildelines](https://www.jetbrains.com/help/ring-ui/welcome.html)
7
- - [Figma library[(https://www.figma.com/design/j7UivSrGze5xCDKrqzR7Fa/RingUI--Community)
7
+ - [Figma library](https://www.figma.com/design/j7UivSrGze5xCDKrqzR7Fa/RingUI--Community)
8
8
  - [Usage examples in Storybook][docsite]
9
9
  - [GitHub repository](https://github.com/JetBrains/ring-ui)
10
10
  - [Issues in YouTrack](https://youtrack.jetbrains.com/issues/RG)
@@ -5,10 +5,11 @@ export interface AlertItem extends Partial<Omit<AlertProps, 'children'>> {
5
5
  message: ReactNode;
6
6
  ref?: Ref<Alert>;
7
7
  }
8
+ export declare const DEFAULT_ALERT_TIMEOUT = 10000;
8
9
  /**
9
10
  * @name Alert Service
10
11
  */
11
- declare class AlertService {
12
+ export declare class AlertService {
12
13
  defaultTimeout: number;
13
14
  showingAlerts: AlertItem[];
14
15
  containerElement: HTMLDivElement;
@@ -2,11 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createRoot } from 'react-dom/client';
3
3
  import getUID from '../global/get-uid';
4
4
  import Alert, { ANIMATION_TIME, Container as AlertContainer } from '../alert/alert';
5
+ export const DEFAULT_ALERT_TIMEOUT = 10000; // 10 seconds
5
6
  /**
6
7
  * @name Alert Service
7
8
  */
8
- class AlertService {
9
- defaultTimeout = 0;
9
+ export class AlertService {
10
+ defaultTimeout = DEFAULT_ALERT_TIMEOUT;
10
11
  // This alerts are stored in inverse order (last shown is first in array)
11
12
  showingAlerts = [];
12
13
  containerElement = document.createElement('div');
@@ -43,7 +43,7 @@
43
43
  --ring-button-background-color: var(--ring-button-default-background-color);
44
44
  --ring-button-hover-background-color: var(--ring-content-background-color);
45
45
  --ring-button-pressed-background-color: var(--ring-selected-background-color);
46
- --ring-button-active-background-color: var(--ring-hover-background-color);
46
+ --ring-button-active-background-color: var(--ring-main-container-light-color);
47
47
  --ring-button-disabled-background-color: var(--ring-button-default-background-color);
48
48
 
49
49
  box-sizing: border-box;
@@ -50,10 +50,10 @@ export default class Item extends PureComponent {
50
50
  const { title, items, showMoreLessButton, level, parentShift, showFocus, selectable, selected, partialSelected, collapsible, collapsed, onCollapse, onExpand, } = this.props;
51
51
  let moreLessButton;
52
52
  if (showMoreLessButton === moreLessButtonStates.MORE || showMoreLessButton === moreLessButtonStates.MORE_LOADING) {
53
- moreLessButton = (_jsxs(Text, { info: true, children: [_jsx(Link, { inherit: true, pseudo: true, onClick: this.onShowMore, children: 'Show more' }), showMoreLessButton === moreLessButtonStates.MORE_LOADING && (_jsx(LoaderInline, { className: styles.showMoreLoader }))] }));
53
+ moreLessButton = (_jsxs(Text, { info: true, size: Text.Size.S, children: [_jsx(Link, { inherit: true, pseudo: true, onClick: this.onShowMore, children: 'Show more' }), showMoreLessButton === moreLessButtonStates.MORE_LOADING && (_jsx(LoaderInline, { className: styles.showMoreLoader }))] }));
54
54
  }
55
55
  else if (showMoreLessButton === moreLessButtonStates.LESS) {
56
- moreLessButton = (_jsx(Text, { info: true, children: _jsx(Link, { inherit: true, pseudo: true, onClick: this.onShowLess, children: 'Show less' }) }));
56
+ moreLessButton = (_jsx(Text, { info: true, size: Text.Size.S, children: _jsx(Link, { inherit: true, pseudo: true, onClick: this.onShowLess, children: 'Show less' }) }));
57
57
  }
58
58
  let collapserExpander = null;
59
59
  if (collapsible) {
@@ -21,7 +21,7 @@ import { I18nContext } from '../i18n/i18n-context';
21
21
  import DatePopup from './date-popup';
22
22
  import styles from './date-picker.css';
23
23
  import formats from './formats';
24
- const PopupComponent = ({ hidden = false, className, popupRef, onClear, datePopupProps, onComplete, ...restProps }) => (_jsx(Popup, { hidden: hidden, className: className, ref: popupRef, directions: [
24
+ const PopupComponent = ({ hidden = false, className, popupRef, onClear, datePopupProps, onComplete, ...restProps }) => (_jsx(Popup, { cssPositioning: true, hidden: hidden, className: className, ref: popupRef, directions: [
25
25
  Popup.PopupProps.Directions.BOTTOM_RIGHT,
26
26
  Popup.PopupProps.Directions.BOTTOM_LEFT,
27
27
  Popup.PopupProps.Directions.TOP_LEFT,
@@ -152,7 +152,7 @@ export default class DatePicker extends PureComponent {
152
152
  const classes = classNames(styles.datePicker, className, styles[`size${this.props.size}`], {
153
153
  [styles.inline]: inline,
154
154
  });
155
- return (_jsx(Dropdown, { className: classes, disabled: this.props.disabled, "data-test": "ring-date-picker", anchor: inline ? (_jsx(Link, { "data-test-ring-dropdown-anchor": true, className: styles.anchor, disabled: this.props.disabled ?? false, pseudo: true, children: this.getAnchorText() })) : (_jsx(Button, { "data-test-ring-dropdown-anchor": true, className: styles.anchor, inline: false, disabled: this.props.disabled ?? false, ...this.props.buttonAttributes, children: anchorContent })), ...dropdownProps, children: _jsx(PopupComponent, { className: popupClassName, popupRef: this.popupRef, onClear: clear ? this.clear : null, datePopupProps: {
155
+ return (_jsx(Dropdown, { className: classes, disabled: this.props.disabled, "data-test": "ring-date-picker", anchor: inline ? (_jsx(Link, { "data-test-ring-dropdown-anchor": true, className: styles.anchor, disabled: this.props.disabled ?? false, pseudo: true, children: this.getAnchorText() })) : (_jsx(Button, { "data-test-ring-dropdown-anchor": true, className: styles.anchor, inline: false, disabled: this.props.disabled ?? false, ...this.props.buttonAttributes, children: anchorContent })), ...dropdownProps, children: _jsx(PopupComponent, { className: popupClassName, cssPositioning: true, popupRef: this.popupRef, onClear: clear ? this.clear : null, datePopupProps: {
156
156
  ...datePopupProps,
157
157
  translations,
158
158
  onChange: this.handleChange,
@@ -12,6 +12,6 @@ export default class ErrorBubble extends PureComponent {
12
12
  const { children, className, ...restProps } = this.props;
13
13
  const errorBubbleClasses = classNames(styles.errorBubble, className);
14
14
  return (_jsxs("div", { className: styles.errorBubbleWrapper, "data-test": "ring-error-bubble-wrapper", children: [children &&
15
- Children.map(children, (child) => cloneElement(child, { ...child.props, ...restProps })), restProps.error && (_jsx(Popup, { trapFocus: false, className: styles.errorBubblePopup, hidden: false, attached: false, directions: [Directions.RIGHT_CENTER, Directions.RIGHT_BOTTOM, Directions.RIGHT_TOP], children: _jsx("div", { className: errorBubbleClasses, "data-test": "ring-error-bubble", children: restProps.error }) }))] }));
15
+ Children.map(children, (child) => cloneElement(child, { ...child.props, ...restProps })), restProps.error && (_jsx(Popup, { cssPositioning: true, trapFocus: false, className: styles.errorBubblePopup, hidden: false, attached: false, directions: [Directions.RIGHT_CENTER, Directions.RIGHT_BOTTOM, Directions.RIGHT_TOP], children: _jsx("div", { className: errorBubbleClasses, "data-test": "ring-error-bubble", children: restProps.error }) }))] }));
16
16
  }
17
17
  }
@@ -33,7 +33,7 @@ export default class Services extends PureComponent {
33
33
  const servicesWithIcons = sortedServices.filter(service => service.iconUrl && service.homeUrl);
34
34
  const servicesWithOutIcons = sortedServices.filter(service => !service.iconUrl && service.homeUrl);
35
35
  const separatorIsRequired = servicesWithIcons.length !== 0 && servicesWithOutIcons.length !== 0;
36
- return (_jsx(Dropdown, { ...props, anchor: makeAnchor(loading), initShown: initShown, children: _jsxs(Popup, { className: classNames(styles.services, { [darkStyles.dark]: theme === Theme.DARK }), top: -3, children: [servicesWithIcons.map(service => {
36
+ return (_jsx(Dropdown, { ...props, anchor: makeAnchor(loading), initShown: initShown, children: _jsxs(Popup, { cssPositioning: true, className: classNames(styles.services, { [darkStyles.dark]: theme === Theme.DARK }), top: -3, children: [servicesWithIcons.map(service => {
37
37
  const isActive = this.serviceIsActive(service);
38
38
  return (_jsx(Services.Link, { active: isActive, className: isActive ? styles.activeItem : styles.item, service: service }, service.id));
39
39
  }), separatorIsRequired && _jsx("div", { className: styles.line }, "separator"), servicesWithOutIcons.map(service => {
@@ -17,8 +17,8 @@ export interface InputTranslations {
17
17
  clear: string;
18
18
  }
19
19
  export interface InputBaseProps {
20
- size: Size;
21
- enableShortcuts: boolean | string[];
20
+ size?: Size;
21
+ enableShortcuts?: boolean | string[];
22
22
  children?: string | undefined;
23
23
  inputClassName?: string | null | undefined;
24
24
  label?: ReactNode;
@@ -37,11 +37,11 @@ export interface InputBaseProps {
37
37
  type Override<D, S> = Omit<D, keyof S> & S;
38
38
  export type InputSpecificProps = Override<InputHTMLAttributes<HTMLInputElement>, InputBaseProps> & {
39
39
  multiline?: false | undefined;
40
- inputRef: Ref<HTMLInputElement>;
40
+ inputRef?: Ref<HTMLInputElement>;
41
41
  };
42
42
  type TextAreaSpecificProps = Override<TextareaHTMLAttributes<HTMLTextAreaElement>, InputBaseProps> & {
43
43
  multiline: true;
44
- inputRef: Ref<HTMLTextAreaElement>;
44
+ inputRef?: Ref<HTMLTextAreaElement>;
45
45
  };
46
46
  export type InputProps = InputSpecificProps | TextAreaSpecificProps;
47
47
  export declare class Input extends PureComponent<InputProps> {
@@ -98,6 +98,6 @@ export default class Message extends Component {
98
98
  const tailClasses = classNames(styles.tail, tailClassName);
99
99
  const popupDirections = this.props.direction ? [this.props.direction] : this.props.directions;
100
100
  const { direction } = this.state;
101
- return (_jsx(I18nContext.Consumer, { children: ({ translate }) => (_jsx(WithThemeClasses, { theme: theme, children: themeClasses => (_jsx(Popup, { ref: this.popupRef, hidden: false, directions: popupDirections, className: classNames(classes, themeClasses), offset: UNIT * 2, onDirectionChange: this._onDirectionChange, ...popupProps, children: _jsxs(ThemeProvider, { theme: theme, passToPopups: true, children: [direction && _jsx("div", { className: tailClasses, style: getTailOffsets(this.getTailOffset())[direction] }), icon && _jsx(Icon, { className: styles.icon, glyph: icon }), title && (_jsx("h1", { "data-test": "rgMessageTitle", className: styles.title, children: title })), children && _jsx("div", { className: styles.description, children: children }), (onClose || buttonProps) && (_jsx(Button, { className: styles.gotIt, onClick: onClose, primary: true, ...buttonProps, children: translations?.gotIt ?? translate('gotIt') })), onDismiss && (_jsx(Button, { onClick: onDismiss, inline: true, children: translations?.dismiss ?? translate('dismiss') }))] }) })) })) }));
101
+ return (_jsx(I18nContext.Consumer, { children: ({ translate }) => (_jsx(WithThemeClasses, { theme: theme, children: themeClasses => (_jsx(Popup, { ref: this.popupRef, cssPositioning: true, hidden: false, directions: popupDirections, className: classNames(classes, themeClasses), offset: UNIT * 2, onDirectionChange: this._onDirectionChange, ...popupProps, children: _jsxs(ThemeProvider, { theme: theme, passToPopups: true, children: [direction && _jsx("div", { className: tailClasses, style: getTailOffsets(this.getTailOffset())[direction] }), icon && _jsx(Icon, { className: styles.icon, glyph: icon }), title && (_jsx("h1", { "data-test": "rgMessageTitle", className: styles.title, children: title })), children && _jsx("div", { className: styles.description, children: children }), (onClose || buttonProps) && (_jsx(Button, { className: styles.gotIt, onClick: onClose, primary: true, ...buttonProps, children: translations?.gotIt ?? translate('gotIt') })), onDismiss && (_jsx(Button, { onClick: onDismiss, inline: true, children: translations?.dismiss ?? translate('dismiss') }))] }) })) })) }));
102
102
  }
103
103
  }
@@ -3,10 +3,7 @@
3
3
  .popup {
4
4
  composes: font from "../global/global.css";
5
5
 
6
- position: fixed;
7
6
  z-index: var(--ring-overlay-z-index);
8
- top: -100vh;
9
- left: -100vw;
10
7
 
11
8
  overflow-y: auto;
12
9
 
@@ -19,6 +16,86 @@
19
16
  box-shadow: var(--ring-popup-shadow);
20
17
  }
21
18
 
19
+ .jsAnchoredPopup {
20
+ position: fixed;
21
+
22
+ top: -100vh;
23
+ left: -100vw;
24
+ }
25
+
26
+ :root {
27
+ --ring-popup-offset: 8px;
28
+ }
29
+
30
+ .cssAnchoredPopup {
31
+ --ring-popup-offset: 0;
32
+
33
+ position: absolute;
34
+
35
+ margin: 0
36
+ }
37
+
38
+ @position-try --bottom-right {
39
+ position-area: block-end span-inline-end;
40
+ margin-block-start: var(--ring-popup-offset);
41
+ }
42
+
43
+ @position-try --bottom-left {
44
+ position-area: block-end span-inline-start;
45
+ margin-block-start: var(--ring-popup-offset);
46
+ }
47
+
48
+ @position-try --bottom-center {
49
+ position-area: block-end center;
50
+ margin-block-start: var(--ring-popup-offset);
51
+ }
52
+
53
+ @position-try --top-center {
54
+ position-area: block-start center;
55
+ margin-block-end: var(--ring-popup-offset);
56
+ }
57
+
58
+ @position-try --top-right {
59
+ position-area: block-start span-inline-end;
60
+ margin-block-end: var(--ring-popup-offset);
61
+ }
62
+
63
+ @position-try --top-left {
64
+ position-area: block-start span-inline-start;
65
+ margin-block-end: var(--ring-popup-offset);
66
+ }
67
+
68
+ @position-try --right-center {
69
+ position-area: center inline-end;
70
+ margin-inline-start: var(--ring-popup-offset);
71
+ }
72
+
73
+ @position-try --right-top {
74
+ position-area: span-block-start inline-end;
75
+ margin-inline-start: var(--ring-popup-offset);
76
+ }
77
+
78
+ @position-try --right-bottom {
79
+ position-area: span-block-end inline-end;
80
+ margin-inline-start: var(--ring-popup-offset);
81
+ }
82
+
83
+ @position-try --left-center {
84
+ position-area: center inline-start;
85
+ margin-inline-end: var(--ring-popup-offset);
86
+ }
87
+
88
+ @position-try --left-top {
89
+ position-area: span-block-start inline-start;
90
+ margin-inline-end: var(--ring-popup-offset);
91
+ }
92
+
93
+ @position-try --left-bottom {
94
+ position-area: span-block-end inline-start;
95
+ margin-inline-end: var(--ring-popup-offset);
96
+ }
97
+
98
+
22
99
  .largeBorderRadius {
23
100
  border-radius: var(--ring-border-radius-large);
24
101
  }
@@ -32,6 +32,7 @@ export interface BasePopupProps {
32
32
  withTail?: boolean;
33
33
  tailOffset?: number;
34
34
  largeBorderRadius?: boolean;
35
+ cssPositioning?: boolean;
35
36
  anchorElement?: HTMLElement | null | undefined;
36
37
  target?: string | Element | null | undefined;
37
38
  className?: string | null | undefined;
@@ -83,6 +84,7 @@ export default class Popup<P extends BasePopupProps = PopupProps> extends PureCo
83
84
  trapFocus: boolean;
84
85
  autoFocusFirst: boolean;
85
86
  legacy: boolean;
87
+ cssPositioning: boolean;
86
88
  };
87
89
  state: PopupState;
88
90
  componentDidMount(): void;
@@ -15,6 +15,7 @@ import position from './position';
15
15
  import styles from './popup.css';
16
16
  import { DEFAULT_DIRECTIONS, Dimension, Directions, Display, MaxHeight, MinWidth } from './popup.consts';
17
17
  import { PopupTargetContext, PopupTarget } from './popup.target';
18
+ import { setCSSAnchorPositioning, supportsCSSAnchorPositioning } from './position-css';
18
19
  export { PopupTargetContext, PopupTarget };
19
20
  const isPossibleClientSideNavigation = (event) => {
20
21
  const target = event.target;
@@ -65,6 +66,7 @@ export default class Popup extends PureComponent {
65
66
  trapFocus: false,
66
67
  autoFocusFirst: false,
67
68
  legacy: false,
69
+ cssPositioning: false,
68
70
  };
69
71
  state = {
70
72
  display: Display.SHOWING,
@@ -160,21 +162,37 @@ export default class Popup extends PureComponent {
160
162
  };
161
163
  _updatePosition = () => {
162
164
  const popup = this.popup;
165
+ const anchor = this._getAnchor();
163
166
  if (popup) {
164
- popup.style.position = 'absolute';
165
- if (this.isVisible()) {
166
- const { styles: style, direction } = this.position();
167
- Object.entries(style).forEach(([key, value]) => {
168
- const propKey = key;
169
- if (typeof value === 'number') {
170
- popup.style[propKey] = `${value}px`;
171
- }
172
- else {
173
- popup.style[propKey] = value.toString();
174
- }
167
+ if (this.props.cssPositioning && supportsCSSAnchorPositioning() && anchor) {
168
+ // Use CSS Anchor positioning
169
+ setCSSAnchorPositioning({
170
+ popup,
171
+ anchor,
172
+ uid: this.uid,
173
+ minWidth: this.props.minWidth,
174
+ top: this.props.top,
175
+ left: this.props.left,
176
+ directions: this.props.directions,
177
+ offset: this.props.offset,
175
178
  });
176
- if (direction != null) {
177
- this._updateDirection(direction);
179
+ }
180
+ else {
181
+ popup.style.position = 'absolute';
182
+ if (this.isVisible()) {
183
+ const { styles: style, direction } = this.position();
184
+ Object.entries(style).forEach(([key, value]) => {
185
+ const propKey = key;
186
+ if (typeof value === 'number') {
187
+ popup.style[propKey] = `${value}px`;
188
+ }
189
+ else {
190
+ popup.style[propKey] = value.toString();
191
+ }
192
+ });
193
+ if (direction != null) {
194
+ this._updateDirection(direction);
195
+ }
178
196
  }
179
197
  }
180
198
  this.setState(this.calculateDisplay);
@@ -199,16 +217,20 @@ export default class Popup extends PureComponent {
199
217
  clearTimeout(this._prevTimeout);
200
218
  this._prevTimeout = window.setTimeout(() => {
201
219
  this._listenersEnabled = true;
202
- this.listeners.add(window, 'resize', this._redraw);
203
- if (this.props.autoPositioningOnScroll) {
204
- this.listeners.add(window, 'scroll', this._redraw);
220
+ // CSS positioning doesn't need resize/scroll listeners as it's handled by CSS
221
+ // But we need them if CSS positioning isn't supported
222
+ if (!this.props.cssPositioning || !supportsCSSAnchorPositioning()) {
223
+ this.listeners.add(window, 'resize', this._redraw);
224
+ if (this.props.autoPositioningOnScroll) {
225
+ this.listeners.add(window, 'scroll', this._redraw);
226
+ }
227
+ let el = this._getAnchor();
228
+ while (el) {
229
+ this.listeners.add(el, 'scroll', this._redraw);
230
+ el = el.parentElement;
231
+ }
205
232
  }
206
233
  this.listeners.add(document, 'pointerdown', this._onDocumentClick, true);
207
- let el = this._getAnchor();
208
- while (el) {
209
- this.listeners.add(el, 'scroll', this._redraw);
210
- el = el.parentElement;
211
- }
212
234
  }, 0);
213
235
  return;
214
236
  }
@@ -257,8 +279,11 @@ export default class Popup extends PureComponent {
257
279
  };
258
280
  render() {
259
281
  const { className, style, hidden, attached, keepMounted, client, onMouseDown, onMouseUp, onMouseOver, onMouseOut, onContextMenu, 'data-test': dataTest, largeBorderRadius, } = this.props;
282
+ const useCssPositioning = this.props.cssPositioning && supportsCSSAnchorPositioning();
260
283
  const showing = this.state.display === Display.SHOWING;
261
284
  const classes = classNames(className, styles.popup, {
285
+ [styles.jsAnchoredPopup]: !useCssPositioning,
286
+ [styles.cssAnchoredPopup]: useCssPositioning,
262
287
  [styles.attached]: attached,
263
288
  [styles.hidden]: hidden,
264
289
  [styles.showing]: showing,
@@ -0,0 +1,14 @@
1
+ import { Directions } from './popup.consts';
2
+ export declare const supportsCSSAnchorPositioning: () => boolean;
3
+ interface SetCSSAnchorPositioningParams {
4
+ popup: HTMLElement;
5
+ anchor: HTMLElement;
6
+ uid: string;
7
+ minWidth?: number | 'target' | null;
8
+ top?: number;
9
+ left?: number;
10
+ directions: readonly Directions[];
11
+ offset?: number;
12
+ }
13
+ export declare const setCSSAnchorPositioning: ({ popup, anchor, uid, minWidth, top, left, directions, offset, }: SetCSSAnchorPositioningParams) => void;
14
+ export {};
@@ -0,0 +1,77 @@
1
+ import { getRect } from '../global/dom';
2
+ import { calculateMinWidth } from './position';
3
+ import { Directions } from './popup.consts';
4
+ export const supportsCSSAnchorPositioning = () => {
5
+ return CSS?.supports?.('anchor-name', 'none');
6
+ };
7
+ const getPositionArea = (direction) => {
8
+ switch (direction) {
9
+ case Directions.BOTTOM_RIGHT:
10
+ return ['block-end span-inline-end', '--bottom-right'];
11
+ case Directions.BOTTOM_LEFT:
12
+ return ['block-end span-inline-start', '--bottom-left'];
13
+ case Directions.BOTTOM_CENTER:
14
+ return ['block-end center', '--bottom-center'];
15
+ case Directions.TOP_CENTER:
16
+ return ['block-start center', '--top-center'];
17
+ case Directions.TOP_RIGHT:
18
+ return ['block-start span-inline-end', '--top-right'];
19
+ case Directions.TOP_LEFT:
20
+ return ['block-start span-inline-start', '--top-left'];
21
+ case Directions.RIGHT_CENTER:
22
+ return ['center inline-end', '--right-center'];
23
+ case Directions.RIGHT_TOP:
24
+ return ['span-block-start inline-end', '--right-top'];
25
+ case Directions.RIGHT_BOTTOM:
26
+ return ['span-block-end inline-end', '--right-bottom'];
27
+ case Directions.LEFT_CENTER:
28
+ return ['center inline-start', '--left-center'];
29
+ case Directions.LEFT_TOP:
30
+ return ['span-block-start inline-start', '--left-top'];
31
+ case Directions.LEFT_BOTTOM:
32
+ return ['span-block-end inline-start', '--left-bottom'];
33
+ default:
34
+ return ['block-end span-inline-end', '--bottom-right'];
35
+ }
36
+ };
37
+ const getPositionFallbacks = (directions) => {
38
+ return directions
39
+ .slice(1)
40
+ .map(direction => getPositionArea(direction)[1])
41
+ .join(', ');
42
+ };
43
+ export const setCSSAnchorPositioning = ({ popup, anchor, uid, minWidth, top, left, directions, offset, }) => {
44
+ const anchorName = anchor.style.getPropertyValue('anchor-name') || `--anchor-${uid}`;
45
+ if (!anchor.style.getPropertyValue('anchor-name')) {
46
+ anchor.style.setProperty('anchor-name', anchorName);
47
+ }
48
+ // Set positioning properties on the popup using setProperty
49
+ popup.style.position = 'absolute';
50
+ popup.style.setProperty('position-anchor', anchorName);
51
+ const calculatedMinWidth = calculateMinWidth(getRect(anchor).width, minWidth);
52
+ if (calculatedMinWidth !== null) {
53
+ popup.style.minWidth = `${calculatedMinWidth}px`;
54
+ }
55
+ if (top) {
56
+ popup.style.top = `${top}px`;
57
+ }
58
+ if (left) {
59
+ popup.style.left = `${left}px`;
60
+ }
61
+ const [initialPositionStyle, initialPositionName] = getPositionArea(directions[0]);
62
+ popup.style.setProperty('position-area', initialPositionStyle);
63
+ if (offset) {
64
+ popup.style.setProperty('--ring-popup-offset', `${offset}px`);
65
+ if (initialPositionName.startsWith('--bottom') || initialPositionName.startsWith('--top')) {
66
+ popup.style.margin = `${offset}px 0`;
67
+ }
68
+ else {
69
+ popup.style.margin = `0 ${offset}px`;
70
+ }
71
+ }
72
+ // Add fallbacks for better positioning if there are multiple directions
73
+ const fallbacks = getPositionFallbacks(directions);
74
+ if (fallbacks) {
75
+ popup.style.setProperty('position-try-fallbacks', fallbacks);
76
+ }
77
+ };
@@ -24,6 +24,7 @@ export interface PositionAttrs {
24
24
  }
25
25
  export declare const positionPropKeys: readonly ["directions", "autoPositioning", "autoCorrectTopOverflow", "sidePadding", "top", "left", "offset", "maxHeight", "minWidth"];
26
26
  export declare function maxHeightForDirection(direction: Directions, anchorNode: Element, containerNode?: Element | null): number | null;
27
+ export declare function calculateMinWidth(anchorWidth: number, minWidth: PositionAttrs['minWidth']): number | null;
27
28
  export default function position(attrs: PositionAttrs): {
28
29
  styles: PositionStyles;
29
30
  direction: Directions | null;
@@ -126,6 +126,15 @@ export function maxHeightForDirection(direction, anchorNode, containerNode) {
126
126
  return null;
127
127
  }
128
128
  }
129
+ export function calculateMinWidth(anchorWidth, minWidth) {
130
+ if (minWidth === MinWidth.TARGET || minWidth === 'target') {
131
+ return anchorWidth;
132
+ }
133
+ else if (minWidth) {
134
+ return anchorWidth < minWidth ? minWidth : anchorWidth;
135
+ }
136
+ return null;
137
+ }
129
138
  export default function position(attrs) {
130
139
  const { popup, anchor, container, directions, autoPositioning, sidePadding, top, left, offset, maxHeight, minWidth, autoCorrectTopOverflow = true, } = attrs;
131
140
  let styles = {
@@ -185,11 +194,9 @@ export default function position(attrs) {
185
194
  scroll,
186
195
  });
187
196
  }
188
- if (minWidth === MinWidth.TARGET || minWidth === 'target') {
189
- styles.minWidth = anchorRect.width;
190
- }
191
- else if (minWidth) {
192
- styles.minWidth = anchorRect.width < minWidth ? minWidth : anchorRect.width;
197
+ const newMinWidth = calculateMinWidth(anchorRect.width, minWidth);
198
+ if (newMinWidth !== null) {
199
+ styles.minWidth = newMinWidth;
193
200
  }
194
201
  return { styles, direction: chosenDirection };
195
202
  }
@@ -47,6 +47,7 @@ export default class PopupMenu<T = unknown> extends Popup<PopupMenuProps<T>> {
47
47
  trapFocus: boolean;
48
48
  autoFocusFirst: boolean;
49
49
  legacy: boolean;
50
+ cssPositioning: boolean;
50
51
  data: never[];
51
52
  restoreActiveIndex: boolean;
52
53
  activateSingleItem: boolean;
@@ -704,7 +704,7 @@ export default class QueryAssist extends Component {
704
704
  return (_jsx(ControlsHeightContext.Provider, { value: ControlsHeight.M, children: _jsx(I18nContext.Consumer, { children: ({ translate }) => (_jsxs("div", { "data-test": dataTests('ring-query-assist', dataTest), className: containerClasses, role: "presentation", ref: this.nodeRef, children: [this.state.shortcuts && _jsx(Shortcuts, { map: this.shortcutsMap, scope: this.shortcutsScope }), renderGlass && !huge && (_jsx(Icon, { glyph: searchIcon, className: styles.icon, title: translations?.searchTitle ?? translate('searchTitle'), ref: this.glassRef, "data-test": "query-assist-search-icon" })), renderLoader && (_jsx("div", { className: classNames(styles.icon, styles.loader, {
705
705
  [styles.loaderOnTheRight]: !glass && !huge,
706
706
  [styles.loaderActive]: renderLoader,
707
- }), ref: this.loaderRef, children: _jsx(LoaderInline, {}) })), _jsx(ContentEditable, { "aria-label": translations?.searchTitle ?? translate('searchTitle'), className: inputClasses, "data-test": "ring-query-assist-input", inputRef: this.inputRef, disabled: this.props.disabled, onComponentUpdate: () => this.setCaretPosition({ fromContentEditable: true }), onBlur: this.handleFocusChange, onClick: this.handleCaretMove, onCompositionStart: this.trackCompositionState, onCompositionEnd: this.trackCompositionState, onFocus: this.handleFocusChange, onInput: this.handleInput, onKeyUp: this.handleInput, onKeyDown: this.handleEnter, onPaste: this.handlePaste, spellCheck: "false", children: this.state.query && _jsx("span", { children: this.renderQuery() }) }), renderPlaceholder && (_jsx("button", { type: "button", className: placeholderStyles, ref: this.placeholderRef, onClick: this.handleCaretMove, "data-test": "query-assist-placeholder", disabled: this.props.disabled, tabIndex: -1, children: this.props.placeholder })), actions.length ? (_jsx("div", { "data-test": "ring-query-assist-actions", className: styles.actions, children: actions })) : null, _jsx(PopupMenu, { hidden: !this.state.showPopup, onCloseAttempt: this.closePopup, ref: this.popupRef, anchorElement: this.node, keepMounted: true, attached: true, className: this.props.popupClassName, directions: [PopupMenu.PopupProps.Directions.BOTTOM_RIGHT], data: useCustomItemRender ? this.state.suggestions : this.renderSuggestions(), "data-test": "ring-query-assist-popup", hint: this.props.hint, shortcutsMap: this.listShortcutsMap, hintOnSelection: this.props.hintOnSelection, left: this.getPopupOffset(this.state.suggestions), maxHeight: PopupMenu.PopupProps.MaxHeight.SCREEN, onMouseDown: this.trackPopupMouseState, onMouseUp: this.trackPopupMouseState, onSelect: item => this.handleComplete(item) }), glass && huge && (_jsx("div", { className: styles.rightSearchButton, "data-test": "query-assist-search-button", children: _jsx(Icon, { glyph: searchIcon, className: styles.rightSearchIcon, title: translations?.searchTitle ?? translate('searchTitle'), onClick: this.handleApply, ref: this.glassRef, "data-test": "query-assist-search-icon" }) }))] })) }) }));
707
+ }), ref: this.loaderRef, children: _jsx(LoaderInline, {}) })), _jsx(ContentEditable, { "aria-label": translations?.searchTitle ?? translate('searchTitle'), className: inputClasses, "data-test": "ring-query-assist-input", inputRef: this.inputRef, disabled: this.props.disabled, onComponentUpdate: () => this.setCaretPosition({ fromContentEditable: true }), onBlur: this.handleFocusChange, onClick: this.handleCaretMove, onCompositionStart: this.trackCompositionState, onCompositionEnd: this.trackCompositionState, onFocus: this.handleFocusChange, onInput: this.handleInput, onKeyUp: this.handleInput, onKeyDown: this.handleEnter, onPaste: this.handlePaste, spellCheck: "false", children: this.state.query && _jsx("span", { children: this.renderQuery() }) }), renderPlaceholder && (_jsx("button", { type: "button", className: placeholderStyles, ref: this.placeholderRef, onClick: this.handleCaretMove, "data-test": "query-assist-placeholder", disabled: this.props.disabled, tabIndex: -1, children: this.props.placeholder })), actions.length ? (_jsx("div", { "data-test": "ring-query-assist-actions", className: styles.actions, children: actions })) : null, _jsx(PopupMenu, { cssPositioning: true, hidden: !this.state.showPopup, onCloseAttempt: this.closePopup, ref: this.popupRef, anchorElement: this.node, keepMounted: true, attached: true, className: this.props.popupClassName, directions: [PopupMenu.PopupProps.Directions.BOTTOM_RIGHT], data: useCustomItemRender ? this.state.suggestions : this.renderSuggestions(), "data-test": "ring-query-assist-popup", hint: this.props.hint, shortcutsMap: this.listShortcutsMap, hintOnSelection: this.props.hintOnSelection, left: this.getPopupOffset(this.state.suggestions), maxHeight: PopupMenu.PopupProps.MaxHeight.SCREEN, onMouseDown: this.trackPopupMouseState, onMouseUp: this.trackPopupMouseState, onSelect: item => this.handleComplete(item) }), glass && huge && (_jsx("div", { className: styles.rightSearchButton, "data-test": "query-assist-search-button", children: _jsx(Icon, { glyph: searchIcon, className: styles.rightSearchIcon, title: translations?.searchTitle ?? translate('searchTitle'), onClick: this.handleApply, ref: this.glassRef, "data-test": "query-assist-search-icon" }) }))] })) }) }));
708
708
  }
709
709
  }
710
710
  export const RerenderableQueryAssist = rerenderHOC(QueryAssist);
@@ -310,7 +310,7 @@ export default class SelectPopup extends PureComponent {
310
310
  const list = this.getList(this.props.ringPopupTarget || ringPopupTarget);
311
311
  const bottomLine = this.getBottomLine();
312
312
  const hasContent = filterWithTags || selectAll || list || bottomLine || toolbar || topbar;
313
- return (_jsx(Popup, { trapFocus: false, ref: this.popupRef, hidden: hidden || !hasContent, attached: isInputMode, className: classes, dontCloseOnAnchorClick: true, anchorElement: anchorElement, minWidth: minWidth, onCloseAttempt: onCloseAttempt, onOutsideClick: onOutsideClick, directions: directions, top: top, left: left, offset: offset, onMouseDown: this.mouseDownHandler, target: this.props.ringPopupTarget, autoCorrectTopOverflow: false, style: style, largeBorderRadius: true, children: _jsxs("div", { dir: dir, children: [!hidden && filter && _jsx(Shortcuts, { map: this.shortcutsMap, scope: this.shortcutsScope }), topbar, hidden ? _jsx("div", {}) : filterWithTags, selectAll, list, bottomLine, toolbar] }) }));
313
+ return (_jsx(Popup, { trapFocus: false, cssPositioning: true, ref: this.popupRef, hidden: hidden || !hasContent, attached: isInputMode, className: classes, dontCloseOnAnchorClick: true, anchorElement: anchorElement, minWidth: minWidth, onCloseAttempt: onCloseAttempt, onOutsideClick: onOutsideClick, directions: directions, top: top, left: left, offset: offset, onMouseDown: this.mouseDownHandler, target: this.props.ringPopupTarget, autoCorrectTopOverflow: false, style: style, largeBorderRadius: true, children: _jsxs("div", { dir: dir, children: [!hidden && filter && _jsx(Shortcuts, { map: this.shortcutsMap, scope: this.shortcutsScope }), topbar, hidden ? _jsx("div", {}) : filterWithTags, selectAll, list, bottomLine, toolbar] }) }));
314
314
  } }));
315
315
  }
316
316
  }
@@ -52,7 +52,7 @@ export const MoreButton = memo(({ items, selected, onSelect, moreClassName, more
52
52
  return popupItems;
53
53
  }, [items, morePopupBeforeEnd, morePopupItemClassName, selected]);
54
54
  const popupAnchor = useMemo(() => (_jsx(AnchorLink, { moreClassName: moreClassName, moreActiveClassName: moreActiveClassName, hasActiveChildren: hasActiveChild })), [hasActiveChild, moreActiveClassName, moreClassName]);
55
- const popup = useMemo(() => (_jsx(PopupMenu, { directions: morePopupDirections, className: morePopupClassName, onSelect: onSelectHandler, data: data })), [data, morePopupClassName, onSelectHandler]);
55
+ const popup = useMemo(() => (_jsx(PopupMenu, { cssPositioning: true, directions: morePopupDirections, className: morePopupClassName, onSelect: onSelectHandler, data: data })), [data, morePopupClassName, onSelectHandler]);
56
56
  if (items.length === 0) {
57
57
  return null;
58
58
  }
@@ -1,17 +1,28 @@
1
1
  import { Component, HTMLAttributes } from 'react';
2
- type TextSize = 's' | 'm' | 'l';
2
+ declare const TextSizes: {
3
+ readonly S: "s";
4
+ readonly M: "m";
5
+ readonly L: "l";
6
+ };
7
+ type TextSize = (typeof TextSizes)[keyof typeof TextSizes];
3
8
  export interface TextProps extends HTMLAttributes<HTMLElement> {
4
9
  info?: boolean | null | undefined;
5
10
  size?: TextSize;
6
11
  'data-test'?: string | null | undefined;
7
12
  bold?: boolean | null | undefined;
8
13
  }
9
- declare const TextSize: Record<string, TextSize>;
10
14
  /**
11
15
  * @name Text
12
16
  */
13
17
  export default class Text extends Component<TextProps> {
14
- static Size: Record<string, TextSize>;
18
+ static defaultProps: {
19
+ size: "m";
20
+ };
21
+ static Size: {
22
+ readonly S: "s";
23
+ readonly M: "m";
24
+ readonly L: "l";
25
+ };
15
26
  render(): import("react/jsx-runtime").JSX.Element;
16
27
  }
17
28
  export {};
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Component } from 'react';
3
3
  import classNames from 'classnames';
4
4
  import styles from './text.css';
5
- const TextSize = {
5
+ const TextSizes = {
6
6
  S: 's',
7
7
  M: 'm',
8
8
  L: 'l',
@@ -11,7 +11,10 @@ const TextSize = {
11
11
  * @name Text
12
12
  */
13
13
  export default class Text extends Component {
14
- static Size = TextSize;
14
+ static defaultProps = {
15
+ size: TextSizes.M,
16
+ };
17
+ static Size = TextSizes;
15
18
  render() {
16
19
  const { children, className, info, size, bold, ...restProps } = this.props;
17
20
  const classes = classNames(styles.text, className, {
@@ -20,7 +20,7 @@ export default class UserCardTooltip extends Component {
20
20
  };
21
21
  render() {
22
22
  const { children, renderUserCard, renderNoUser, dropdownProps, user, ...restProps } = this.props;
23
- return (_jsx(Dropdown, { anchor: children, hoverMode: true, clickMode: false, ...dropdownProps, children: _jsx(Popup, { attached: false, children: user
23
+ return (_jsx(Dropdown, { anchor: children, hoverMode: true, clickMode: false, ...dropdownProps, children: _jsx(Popup, { attached: false, cssPositioning: true, children: user
24
24
  ? renderUserCard({
25
25
  ...restProps,
26
26
  user,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetbrains/ring-ui",
3
- "version": "7.0.59",
3
+ "version": "7.0.60-beta.0",
4
4
  "description": "JetBrains UI library",
5
5
  "author": {
6
6
  "name": "JetBrains"
@@ -102,9 +102,9 @@
102
102
  "@babel/eslint-parser": "^7.28.0",
103
103
  "@csstools/css-parser-algorithms": "^3.0.4",
104
104
  "@csstools/stylelint-no-at-nest-rule": "^4.0.0",
105
- "@eslint/compat": "^1.3.1",
105
+ "@eslint/compat": "^1.3.2",
106
106
  "@eslint/eslintrc": "^3.2.0",
107
- "@eslint/js": "^9.32.0",
107
+ "@eslint/js": "^9.33.0",
108
108
  "@figma/code-connect": "^1.3.4",
109
109
  "@jetbrains/eslint-config": "^6.0.5",
110
110
  "@jetbrains/logos": "3.0.0-canary.734b213.0",
@@ -115,11 +115,11 @@
115
115
  "@rollup/plugin-json": "^6.1.0",
116
116
  "@rollup/plugin-node-resolve": "^16.0.1",
117
117
  "@rollup/plugin-replace": "^6.0.2",
118
- "@storybook/addon-a11y": "9.0.18",
119
- "@storybook/addon-docs": "^9.0.18",
120
- "@storybook/addon-themes": "^9.0.18",
118
+ "@storybook/addon-a11y": "9.1.2",
119
+ "@storybook/addon-docs": "^9.1.2",
120
+ "@storybook/addon-themes": "^9.1.2",
121
121
  "@storybook/csf": "^0.1.13",
122
- "@storybook/react-webpack5": "9.0.18",
122
+ "@storybook/react-webpack5": "9.1.2",
123
123
  "@storybook/test-runner": "^0.23.0",
124
124
  "@testing-library/dom": "^10.4.1",
125
125
  "@testing-library/react": "^16.3.0",
@@ -129,33 +129,33 @@
129
129
  "@types/chai-dom": "1.11.3",
130
130
  "@types/eslint__js": "^9.14.0",
131
131
  "@types/markdown-it": "^14.1.2",
132
- "@types/react": "^19.1.8",
133
- "@types/react-dom": "^19.1.6",
132
+ "@types/react": "^19.1.10",
133
+ "@types/react-dom": "^19.1.7",
134
134
  "@types/webpack-env": "^1.18.8",
135
- "@vitejs/plugin-react": "^4.7.0",
135
+ "@vitejs/plugin-react": "^5.0.0",
136
136
  "@vitest/eslint-plugin": "^1.3.4",
137
137
  "acorn": "^8.15.0",
138
138
  "babel-plugin-require-context-hook": "^1.0.0",
139
- "caniuse-lite": "^1.0.30001726",
139
+ "caniuse-lite": "^1.0.30001731",
140
140
  "chai": "^5.2.1",
141
141
  "chai-as-promised": "^8.0.1",
142
142
  "chai-dom": "^1.12.1",
143
143
  "cheerio": "^1.1.2",
144
- "chromatic": "^13.1.2",
145
- "core-js": "^3.44.0",
146
- "cpy-cli": "^5.0.0",
144
+ "chromatic": "^13.1.3",
145
+ "core-js": "^3.45.0",
146
+ "cpy-cli": "^6.0.0",
147
147
  "dotenv-cli": "^10.0.0",
148
- "eslint": "^9.32.0",
148
+ "eslint": "^9.33.0",
149
149
  "eslint-config-prettier": "^10.1.8",
150
150
  "eslint-formatter-jslint-xml": "^8.40.0",
151
151
  "eslint-import-resolver-exports": "^1.0.0-beta.5",
152
152
  "eslint-import-resolver-webpack": "^0.13.10",
153
153
  "eslint-plugin-import": "^2.32.0",
154
154
  "eslint-plugin-jsx-a11y": "^6.10.2",
155
- "eslint-plugin-prettier": "^5.5.3",
155
+ "eslint-plugin-prettier": "^5.5.4",
156
156
  "eslint-plugin-react": "^7.37.5",
157
157
  "eslint-plugin-react-hooks": "^5.2.0",
158
- "eslint-plugin-storybook": "^9.0.18",
158
+ "eslint-plugin-storybook": "^9.1.2",
159
159
  "events": "^3.3.0",
160
160
  "glob": "^11.0.3",
161
161
  "globals": "^16.3.0",
@@ -166,30 +166,30 @@
166
166
  "jest": "~30.0.5",
167
167
  "jest-environment-jsdom": "^30.0.5",
168
168
  "jest-teamcity": "^1.12.0",
169
- "lint-staged": "^16.1.2",
169
+ "lint-staged": "^16.1.5",
170
170
  "markdown-it": "^14.1.0",
171
171
  "merge-options": "^3.0.4",
172
172
  "pinst": "^3.0.0",
173
173
  "prettier": "^3.6.2",
174
174
  "raw-loader": "^4.0.2",
175
- "react": "^19.1.0",
176
- "react-dom": "^19.1.0",
175
+ "react": "^19.1.1",
176
+ "react-dom": "^19.1.1",
177
177
  "regenerator-runtime": "^0.14.1",
178
178
  "rimraf": "^6.0.1",
179
179
  "rollup": "^4.46.2",
180
180
  "rollup-plugin-clear": "^2.0.7",
181
181
  "storage-mock": "^2.1.0",
182
- "storybook": "9.0.18",
183
- "stylelint": "^16.23.0",
182
+ "storybook": "9.1.2",
183
+ "stylelint": "^16.23.1",
184
184
  "svg-inline-loader": "^0.8.2",
185
185
  "teamcity-service-messages": "^0.1.14",
186
186
  "terser-webpack-plugin": "^5.3.14",
187
- "typescript": "~5.8.3",
188
- "typescript-eslint": "^8.38.0",
187
+ "typescript": "~5.9.2",
188
+ "typescript-eslint": "^8.39.1",
189
189
  "vitest": "^3.2.4",
190
190
  "vitest-teamcity-reporter": "^0.3.1",
191
191
  "wallaby-webpack": "^3.9.16",
192
- "webpack": "^5.101.0",
192
+ "webpack": "^5.101.1",
193
193
  "webpack-cli": "^6.0.1",
194
194
  "xmlappend": "^1.0.4"
195
195
  },
@@ -216,7 +216,7 @@
216
216
  "@babel/core": "^7.28.0",
217
217
  "@babel/preset-typescript": "^7.27.1",
218
218
  "@jetbrains/babel-preset-jetbrains": "^2.4.0",
219
- "@jetbrains/icons": "^5.10.0",
219
+ "@jetbrains/icons": "^5.12.0",
220
220
  "@jetbrains/postcss-require-hover": "^0.2.0",
221
221
  "@types/combokeys": "^2.4.9",
222
222
  "@types/element-resize-detector": "^1.1.6",