@jetbrains/ring-ui 8.0.0-beta.10 → 8.0.0-beta.11

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.
@@ -10,6 +10,9 @@ export const reactRoot = createRoot(containerElement);
10
10
  * Renders AuthDialog into virtual node to skip maintaining container
11
11
  */
12
12
  function renderAuthDialog(props) {
13
+ if (!containerElement.isConnected) {
14
+ document.body.appendChild(containerElement);
15
+ }
13
16
  reactRoot.render(<ControlsHeightContext value={getGlobalControlsHeight()}>
14
17
  <AuthDialog {...props}/>
15
18
  </ControlsHeightContext>);
@@ -27,17 +27,21 @@ export default class Confirm extends PureComponent {
27
27
  };
28
28
  render() {
29
29
  const { show, className, inProgress, cancelIsDefault, text, description, confirmLabel, rejectLabel, onConfirm, onReject, native, } = this.props;
30
+ const actions = [
31
+ <Button key='reject' data-test='confirm-reject-button' onClick={onReject} disabled={inProgress} primary={cancelIsDefault}>
32
+ {rejectLabel}
33
+ </Button>,
34
+ <Button key='confirm' data-test='confirm-ok-button' primary={!cancelIsDefault} loader={inProgress} disabled={inProgress} onClick={onConfirm}>
35
+ {confirmLabel}
36
+ </Button>,
37
+ ];
38
+ if (cancelIsDefault) {
39
+ actions.reverse();
40
+ }
30
41
  return (<Dialog label={text || (typeof description === 'string' ? description : undefined)} className={className} onEscPress={this.onEscPress} show={show} trapFocus data-test='ring-confirm' native={native}>
31
42
  {text && <Header>{text}</Header>}
32
43
  {description && <Content>{description}</Content>}
33
- <Panel>
34
- <Button data-test='confirm-ok-button' primary={!cancelIsDefault} loader={inProgress} disabled={inProgress} onClick={onConfirm}>
35
- {confirmLabel}
36
- </Button>
37
- <Button data-test='confirm-reject-button' onClick={onReject} disabled={inProgress} primary={cancelIsDefault}>
38
- {rejectLabel}
39
- </Button>
40
- </Panel>
44
+ <Panel>{actions}</Panel>
41
45
  </Dialog>);
42
46
  }
43
47
  }
@@ -7,6 +7,9 @@ export const reactRoot = createRoot(containerElement);
7
7
  * Renders Confirm into virtual node to skip maintaining container
8
8
  */
9
9
  function renderConfirm(props) {
10
+ if (!containerElement.isConnected) {
11
+ document.body.appendChild(containerElement);
12
+ }
10
13
  const { buttonsHeight = getGlobalControlsHeight(), ...restProps } = props;
11
14
  reactRoot.render(<ControlsHeightContext value={buttonsHeight}>
12
15
  <Confirm {...restProps}/>
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import type { InteractionDirection } from './dialog-geometry';
3
+ interface DialogControlsProps {
4
+ movable: boolean | undefined;
5
+ resizable: boolean | undefined;
6
+ showCloseButton: boolean;
7
+ closeButtonTitle: string | undefined;
8
+ onCloseClick: React.MouseEventHandler<HTMLElement>;
9
+ onStartInteraction: (event: React.PointerEvent<HTMLElement>, direction: InteractionDirection) => void;
10
+ onStopInteraction: React.PointerEventHandler<HTMLElement>;
11
+ }
12
+ export default function DialogControls({ movable, resizable, showCloseButton, closeButtonTitle, onCloseClick, onStartInteraction, onStopInteraction, }: DialogControlsProps): React.JSX.Element;
13
+ export {};
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+ import closeIcon from '@jetbrains/icons/close';
3
+ import resizeIcon from '@jetbrains/icons/resize-corner-12px';
4
+ import Button from '../button/button';
5
+ import Icon from '../icon/icon';
6
+ import styles from './dialog.css';
7
+ const resizeDirections = ['n', 'ne', 'e', 's', 'sw', 'w', 'nw', 'se'];
8
+ export default function DialogControls({ movable, resizable, showCloseButton, closeButtonTitle, onCloseClick, onStartInteraction, onStopInteraction, }) {
9
+ return (<>
10
+ {movable && (<div aria-hidden='true' className={styles.moveHandle} data-ring-dialog-move-handle='' data-test='ring-dialog-move-handle' onLostPointerCapture={onStopInteraction}/>)}
11
+ {showCloseButton && (<Button icon={closeIcon} data-test='ring-dialog-close-button' className={styles.closeButton} iconClassName={styles.closeIcon} onClick={onCloseClick} title={closeButtonTitle} aria-label={closeButtonTitle || 'close dialog'}/>)}
12
+ {resizable && (<>
13
+ {resizeDirections.map(direction => (<div aria-hidden='true' className={styles.resizeHandle} data-resize-direction={direction} data-test={`ring-dialog-resize-handle-${direction}`} key={direction} onPointerDown={event => onStartInteraction(event, direction)} onPointerUp={onStopInteraction} onPointerCancel={onStopInteraction} onLostPointerCapture={onStopInteraction}/>))}
14
+ <Icon aria-hidden='true' className={styles.resizeMarker} data-test='ring-dialog-resize-marker' glyph={resizeIcon}/>
15
+ </>)}
16
+ </>);
17
+ }
@@ -0,0 +1,31 @@
1
+ export type ResizeDirection = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
2
+ export type InteractionDirection = 'move' | ResizeDirection;
3
+ export interface Geometry {
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export interface DialogState {
10
+ shortcutsScope: string;
11
+ geometry: Geometry | null;
12
+ resized: boolean;
13
+ }
14
+ export interface Interaction {
15
+ pointerId: number;
16
+ direction: InteractionDirection;
17
+ startX: number;
18
+ startY: number;
19
+ minWidth: number;
20
+ minHeight: number;
21
+ geometry: Geometry;
22
+ }
23
+ export declare const getNativeTabIndex: (autoFocusFirst: boolean | undefined) => -1 | undefined;
24
+ export declare const getTrapDisabled: (trapFocus: boolean, trapDisabled: boolean | undefined) => boolean;
25
+ export declare const getResizeMinimum: (element: Element) => {
26
+ minWidth: number;
27
+ minHeight: number;
28
+ };
29
+ export declare const moveGeometry: (start: Geometry, dx: number, dy: number, viewportWidth: number, viewportHeight: number) => Geometry;
30
+ export declare const fitGeometry: (geometry: Geometry, viewportWidth: number, viewportHeight: number) => Geometry;
31
+ export declare const resizeGeometry: ({ geometry: start, direction, minWidth, minHeight }: Interaction, dx: number, dy: number, viewportWidth: number, viewportHeight: number) => Geometry;
@@ -0,0 +1,48 @@
1
+ const MIN_WIDTH = 256;
2
+ const MIN_HEIGHT = 160;
3
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
4
+ export const getNativeTabIndex = (autoFocusFirst) => (autoFocusFirst ? undefined : -1);
5
+ export const getTrapDisabled = (trapFocus, trapDisabled) => trapDisabled ?? !trapFocus;
6
+ export const getResizeMinimum = (element) => {
7
+ const { minWidth, minHeight } = getComputedStyle(element);
8
+ const parsedWidth = parseFloat(minWidth);
9
+ const parsedHeight = parseFloat(minHeight);
10
+ return {
11
+ minWidth: Number.isNaN(parsedWidth) ? MIN_WIDTH : parsedWidth,
12
+ minHeight: Number.isNaN(parsedHeight) ? MIN_HEIGHT : parsedHeight,
13
+ };
14
+ };
15
+ export const moveGeometry = (start, dx, dy, viewportWidth, viewportHeight) => ({
16
+ ...start,
17
+ left: clamp(start.left + dx, 0, Math.max(0, viewportWidth - start.width)),
18
+ top: clamp(start.top + dy, 0, Math.max(0, viewportHeight - start.height)),
19
+ });
20
+ export const fitGeometry = (geometry, viewportWidth, viewportHeight) => {
21
+ const width = Math.min(geometry.width, viewportWidth);
22
+ const height = Math.min(geometry.height, viewportHeight);
23
+ return {
24
+ left: clamp(geometry.left, 0, Math.max(0, viewportWidth - width)),
25
+ top: clamp(geometry.top, 0, Math.max(0, viewportHeight - height)),
26
+ width,
27
+ height,
28
+ };
29
+ };
30
+ export const resizeGeometry = ({ geometry: start, direction, minWidth, minHeight }, dx, dy, viewportWidth, viewportHeight) => {
31
+ let left = start.left;
32
+ let right = start.left + start.width;
33
+ let top = start.top;
34
+ let bottom = start.top + start.height;
35
+ if (direction.includes('w')) {
36
+ left = clamp(start.left + dx, 0, right - Math.min(minWidth, right));
37
+ }
38
+ else if (direction.includes('e')) {
39
+ right = clamp(right + dx, left + Math.min(minWidth, viewportWidth - left), viewportWidth);
40
+ }
41
+ if (direction.includes('n')) {
42
+ top = clamp(start.top + dy, 0, bottom - Math.min(minHeight, bottom));
43
+ }
44
+ else if (direction.includes('s')) {
45
+ bottom = clamp(bottom + dy, top + Math.min(minHeight, viewportHeight - top), viewportHeight);
46
+ }
47
+ return { left, top, width: right - left, height: bottom - top };
48
+ };
@@ -1,6 +1,6 @@
1
1
  @import '../global/variables.css';
2
2
 
3
- @value header, scrollableWrapper from '../island/island.css';
3
+ @value content as islandContent, header, title as islandTitle, scrollableWrapper from '../island/island.css';
4
4
  @value panel from '../panel/panel.css';
5
5
 
6
6
  .container {
@@ -41,40 +41,58 @@
41
41
  }
42
42
 
43
43
  .content {
44
+ --dialog-padding: calc(var(--ring-unit) * 2.5);
45
+
44
46
  position: relative;
45
47
 
46
- width: calc(var(--ring-unit) * 58);
47
- min-height: calc(var(--ring-unit) * 15);
48
+ box-sizing: border-box;
49
+ width: calc(var(--ring-unit) * 57.5);
50
+ min-width: var(--ring-dialog-min-width, calc(var(--ring-unit) * 32));
51
+ min-height: var(--ring-dialog-min-height, calc(var(--ring-unit) * 20));
48
52
  margin: auto;
53
+ padding: var(--dialog-padding);
49
54
 
50
55
  cursor: default;
51
56
  overflow-wrap: break-word;
52
57
 
53
58
  & .scrollableWrapper {
54
- padding-bottom: var(--ring-unit);
59
+ padding: var(--ring-unit) var(--dialog-padding) 0;
60
+ }
61
+
62
+ & .islandContent {
63
+ width: calc(100% + var(--dialog-padding) * 2);
64
+ min-height: calc(var(--ring-unit) * 5);
65
+ margin-inline: calc(var(--dialog-padding) * -1);
55
66
  }
56
67
 
57
68
  & .panel {
58
- margin-top: 0;
59
- padding: calc(var(--ring-unit) * 2) calc(var(--ring-unit) * 4) calc(var(--ring-unit) * 4);
69
+ margin-top: auto;
70
+ padding: calc(var(--ring-unit) * 3) 0 0;
60
71
 
61
- border-top: 0;
72
+ text-align: end;
62
73
 
74
+ border-top: 0;
63
75
  background-color: transparent;
64
76
  }
65
77
 
66
78
  /* stylelint-disable-next-line selector-max-specificity */
67
79
  & .panel > button:not(:last-child) {
68
- margin-right: var(--ring-unit);
80
+ margin-inline-end: var(--ring-unit);
69
81
  }
70
82
  }
71
83
 
72
- .contentCloseButtonOutside {
73
- max-width: calc(100dvw - var(--ring-unit) * 8);
84
+ .content {
85
+ max-width: calc(100dvw - var(--ring-unit) * 4);
86
+ max-height: calc(100dvh - var(--ring-unit) * 8);
74
87
  }
75
88
 
76
- .contentCloseButtonInside {
77
- max-width: calc(100dvw - var(--ring-unit) * 4);
89
+ .resized .content.content {
90
+ width: 100%;
91
+ min-width: 0;
92
+ max-width: none;
93
+ height: 100%;
94
+ min-height: 0;
95
+ max-height: none;
78
96
  }
79
97
 
80
98
  .content.content {
@@ -91,10 +109,6 @@
91
109
  cursor: pointer;
92
110
  }
93
111
 
94
- .closeIconOutside.closeIconOutside {
95
- color: var(--ring-icon-hover-color);
96
- }
97
-
98
112
  .clickableOverlay:hover + * .closeIcon {
99
113
  color: var(--ring-link-hover-color);
100
114
  }
@@ -105,18 +119,132 @@
105
119
 
106
120
  .closeButton.closeButton {
107
121
  position: absolute;
122
+ z-index: var(--ring-fixed-z-index);
123
+ inset-block-start: calc(var(--ring-unit) * 2);
124
+ inset-inline-end: calc(var(--ring-unit) * 2);
108
125
 
109
126
  line-height: calc(var(--ring-unit) * 2);
110
127
  }
111
128
 
112
- .closeButtonOutside {
129
+ .moveHandle {
130
+ position: absolute;
131
+ z-index: var(--ring-fixed-z-index);
132
+ top: 0;
133
+ right: 0;
134
+ left: 0;
135
+
136
+ height: var(--dialog-padding);
137
+
138
+ cursor: move;
139
+ touch-action: none;
140
+ }
141
+
142
+ .movable .header {
143
+ cursor: move;
144
+ touch-action: none;
145
+ }
146
+
147
+ .resizeHandle {
148
+ position: absolute;
149
+ z-index: var(--ring-fixed-z-index);
150
+
151
+ touch-action: none;
152
+ }
153
+
154
+ .resizeHandle[data-resize-direction='n'],
155
+ .resizeHandle[data-resize-direction='s'] {
156
+ right: var(--ring-unit);
157
+ left: var(--ring-unit);
158
+
159
+ height: var(--ring-unit);
160
+
161
+ cursor: ns-resize;
162
+ }
163
+
164
+ .resizeHandle[data-resize-direction='n'] {
165
+ top: calc(var(--ring-unit) * -0.5);
166
+ }
167
+
168
+ .resizeHandle[data-resize-direction='s'] {
169
+ bottom: calc(var(--ring-unit) * -0.5);
170
+ }
171
+
172
+ .resizeHandle[data-resize-direction='e'],
173
+ .resizeHandle[data-resize-direction='w'] {
113
174
  top: var(--ring-unit);
114
- right: calc(var(--ring-unit) * -3);
175
+ bottom: var(--ring-unit);
176
+
177
+ width: var(--ring-unit);
178
+
179
+ cursor: ew-resize;
115
180
  }
116
181
 
117
- .closeButtonInside {
118
- top: calc(var(--ring-unit) * 2);
119
- right: calc(var(--ring-unit) * 2);
182
+ .resizeHandle[data-resize-direction='e'] {
183
+ right: calc(var(--ring-unit) * -0.5);
184
+ }
185
+
186
+ .resizeHandle[data-resize-direction='w'] {
187
+ left: calc(var(--ring-unit) * -0.5);
188
+ }
189
+
190
+ .resizeHandle[data-resize-direction='ne'],
191
+ .resizeHandle[data-resize-direction='se'],
192
+ .resizeHandle[data-resize-direction='sw'],
193
+ .resizeHandle[data-resize-direction='nw'] {
194
+ width: calc(var(--ring-unit) * 1.5);
195
+ height: calc(var(--ring-unit) * 1.5);
196
+ }
197
+
198
+ .resizeHandle[data-resize-direction='ne'] {
199
+ top: calc(var(--ring-unit) * -0.5);
200
+ right: calc(var(--ring-unit) * -0.5);
201
+
202
+ cursor: nesw-resize;
203
+ }
204
+
205
+ .resizeHandle[data-resize-direction='se'] {
206
+ right: calc(var(--ring-unit) * -0.5);
207
+ bottom: calc(var(--ring-unit) * -0.5);
208
+
209
+ width: calc(var(--ring-unit) * 2.5);
210
+ height: calc(var(--ring-unit) * 2.5);
211
+
212
+ cursor: nwse-resize;
213
+ }
214
+
215
+ .resizeHandle[data-resize-direction='sw'] {
216
+ bottom: calc(var(--ring-unit) * -0.5);
217
+ left: calc(var(--ring-unit) * -0.5);
218
+
219
+ cursor: nesw-resize;
220
+ }
221
+
222
+ .resizeHandle[data-resize-direction='nw'] {
223
+ top: calc(var(--ring-unit) * -0.5);
224
+ left: calc(var(--ring-unit) * -0.5);
225
+
226
+ cursor: nwse-resize;
227
+ }
228
+
229
+ .resizeMarker {
230
+ position: absolute;
231
+ bottom: calc(var(--ring-unit) * 0.5);
232
+ inset-inline-end: calc(var(--ring-unit) * 0.5);
233
+
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+
238
+ width: calc(var(--ring-unit) * 1.5);
239
+ height: calc(var(--ring-unit) * 1.5);
240
+
241
+ pointer-events: none;
242
+
243
+ color: var(--ring-secondary-color);
244
+
245
+ &:dir(rtl) {
246
+ transform: scaleX(-1);
247
+ }
120
248
  }
121
249
 
122
250
  .documentWithoutScroll {
@@ -141,16 +269,27 @@
141
269
  }
142
270
 
143
271
  .innerContainer .header {
144
- padding-top: calc(var(--ring-unit) * 4);
272
+ width: calc(100% + var(--dialog-padding) * 2);
273
+ margin-inline: calc(var(--dialog-padding) * -1);
145
274
 
146
- font-size: 24px;
147
- line-height: 28px;
275
+ padding-block: 0;
276
+ padding-inline: var(--dialog-padding) calc(var(--dialog-padding) + var(--ring-unit) * 3);
277
+ padding-block-start: 0 !important;
278
+
279
+ font-size: calc(var(--ring-unit) * 2.5);
280
+ line-height: calc(var(--ring-unit) * 3);
148
281
  }
149
282
 
150
- .dense .header {
151
- padding-top: calc(var(--ring-unit) * 2);
283
+ .innerContainer .islandTitle {
284
+ float: none;
285
+
286
+ text-align: start;
287
+ }
288
+
289
+ .dense {
290
+ --dialog-padding: calc(var(--ring-unit) * 2);
152
291
  }
153
292
 
154
293
  .dense .panel {
155
- padding-bottom: calc(var(--ring-unit) * 2);
294
+ padding-top: calc(var(--ring-unit) * 2);
156
295
  }
@@ -1,6 +1,7 @@
1
1
  import { PureComponent } from 'react';
2
2
  import * as React from 'react';
3
3
  import { type TabTrapObject, type TabTrapProps } from '../tab-trap/tab-trap';
4
+ import { type DialogState, type Interaction, type InteractionDirection } from './dialog-geometry';
4
5
  import type { ShortcutsScopeOptions } from '../shortcuts/core';
5
6
  export interface DialogProps extends Partial<TabTrapProps> {
6
7
  show: boolean;
@@ -11,8 +12,9 @@ export interface DialogProps extends Partial<TabTrapProps> {
11
12
  onCloseAttempt: (event: React.MouseEvent<HTMLElement> | KeyboardEvent) => void;
12
13
  showCloseButton: boolean;
13
14
  shortcutOptions: ShortcutsScopeOptions;
14
- closeButtonInside: boolean;
15
15
  closeButtonTitle?: string;
16
+ movable?: boolean;
17
+ resizable?: boolean;
16
18
  trapFocus: boolean;
17
19
  contentClassName?: string | null | undefined;
18
20
  portalTarget?: Element | null | undefined;
@@ -22,11 +24,9 @@ export interface DialogProps extends Partial<TabTrapProps> {
22
24
  modal?: boolean;
23
25
  preventBodyScroll?: boolean;
24
26
  }
25
- export default class Dialog extends PureComponent<DialogProps> {
27
+ export default class Dialog extends PureComponent<DialogProps, DialogState> {
26
28
  static defaultProps: Partial<DialogProps>;
27
- state: {
28
- shortcutsScope: string;
29
- };
29
+ state: DialogState;
30
30
  componentDidMount(): void;
31
31
  componentDidUpdate(prevProps: DialogProps): void;
32
32
  componentWillUnmount(): void;
@@ -45,6 +45,14 @@ export default class Dialog extends PureComponent<DialogProps> {
45
45
  dialog?: HTMLElement | null;
46
46
  dialogRef: (tabTrapObj: TabTrapObject | null) => void;
47
47
  nativeDialog: React.RefObject<HTMLDialogElement | null>;
48
+ innerContainer: React.RefObject<HTMLDivElement | null>;
49
+ interaction: Interaction | null;
50
+ resizeMinimum: Pick<Interaction, 'minWidth' | 'minHeight'> | null;
51
+ fitGeometryToViewport: () => void;
52
+ startInteraction: (event: React.PointerEvent<HTMLElement>, direction: InteractionDirection) => void;
53
+ continueInteraction: (event: React.PointerEvent<HTMLElement>) => void;
54
+ stopInteraction: (event: React.PointerEvent<HTMLElement>) => void;
55
+ startMove: (event: React.PointerEvent<HTMLElement>) => void;
48
56
  render(): false | React.JSX.Element;
49
57
  }
50
58
  export type DialogAttrs = React.JSX.LibraryManagedAttributes<typeof Dialog, DialogProps>;
@@ -2,20 +2,19 @@ import { createRef, PureComponent } from 'react';
2
2
  import * as React from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import classNames from 'classnames';
5
- import closeIcon from '@jetbrains/icons/close';
6
5
  import { AdaptiveIsland } from '../island/island';
7
6
  import getUID from '../global/get-uid';
8
7
  import dataTests from '../global/data-tests';
9
8
  import Shortcuts from '../shortcuts/shortcuts';
10
9
  import TabTrap from '../tab-trap/tab-trap';
11
- import Button from '../button/button';
12
10
  import { normalizePopupTarget, PopupTarget, PopupTargetContext } from '../popup/popup.target';
13
11
  import { getPopupContainer } from '../popup/popup';
12
+ import getViewportSize from '../global/get-viewport-size';
14
13
  import { preventerFactory as scrollPreventerFactory } from './dialog-body-scroll-preventer';
14
+ import DialogControls from './dialog-controls';
15
+ import { fitGeometry, getNativeTabIndex, getResizeMinimum, getTrapDisabled, moveGeometry, resizeGeometry, } from './dialog-geometry';
15
16
  import styles from './dialog.css';
16
- /**
17
- * @name Dialog
18
- */
17
+ /** @name Dialog */
19
18
  function noop() { }
20
19
  export default class Dialog extends PureComponent {
21
20
  static defaultProps = {
@@ -25,21 +24,22 @@ export default class Dialog extends PureComponent {
25
24
  onCloseClick: noop,
26
25
  onCloseAttempt: noop,
27
26
  showCloseButton: false,
28
- closeButtonInside: false,
27
+ movable: true,
28
+ resizable: true,
29
29
  shortcutOptions: { modal: false },
30
30
  trapFocus: false,
31
31
  autoFocusFirst: true,
32
+ native: true,
32
33
  modal: true,
33
34
  preventBodyScroll: true,
34
35
  };
35
- state = {
36
- shortcutsScope: getUID('ring-dialog-'),
37
- };
36
+ state = { shortcutsScope: getUID('ring-dialog-'), geometry: null, resized: false };
38
37
  componentDidMount() {
39
38
  const { show, native } = this.props;
40
39
  if (native && show) {
41
40
  this.toggleNativeDialog();
42
41
  }
42
+ window.addEventListener('resize', this.fitGeometryToViewport);
43
43
  this.toggleScrollPreventer();
44
44
  }
45
45
  componentDidUpdate(prevProps) {
@@ -49,15 +49,23 @@ export default class Dialog extends PureComponent {
49
49
  }
50
50
  if (prevProps.show !== this.props.show) {
51
51
  this.toggleScrollPreventer();
52
+ if (show) {
53
+ this.fitGeometryToViewport();
54
+ }
55
+ else {
56
+ this.interaction = null;
57
+ }
52
58
  }
53
59
  }
54
60
  componentWillUnmount() {
61
+ window.removeEventListener('resize', this.fitGeometryToViewport);
62
+ this.interaction = null;
55
63
  this.scrollPreventer.reset();
56
64
  }
57
65
  scrollPreventer = scrollPreventerFactory(getUID('preventer-'));
58
66
  uid = getUID('dialog-');
59
67
  toggleNativeDialog() {
60
- const { show, modal } = this.props;
68
+ const { show, modal, autoFocusFirst } = this.props;
61
69
  if (this.nativeDialog.current) {
62
70
  if (show) {
63
71
  this.nativeDialog.current.removeAttribute('open');
@@ -67,6 +75,9 @@ export default class Dialog extends PureComponent {
67
75
  else {
68
76
  this.nativeDialog.current.show();
69
77
  }
78
+ if (!autoFocusFirst && !this.nativeDialog.current.querySelector('[autofocus]')) {
79
+ this.nativeDialog.current.focus();
80
+ }
70
81
  }
71
82
  else {
72
83
  this.nativeDialog.current.close();
@@ -99,42 +110,112 @@ export default class Dialog extends PureComponent {
99
110
  this.props.onCloseAttempt(event);
100
111
  }
101
112
  };
102
- return {
103
- esc: onEscape,
104
- };
113
+ return { esc: onEscape };
105
114
  };
106
115
  dialog;
107
116
  dialogRef = (tabTrapObj) => {
108
117
  this.dialog = tabTrapObj && tabTrapObj.node;
109
118
  };
110
119
  nativeDialog = createRef();
120
+ innerContainer = createRef();
121
+ interaction = null;
122
+ resizeMinimum = null;
123
+ fitGeometryToViewport = () => {
124
+ this.setState(({ geometry }) => {
125
+ if (!geometry) {
126
+ return null;
127
+ }
128
+ const { width, height } = getViewportSize();
129
+ return { geometry: fitGeometry(geometry, width, height) };
130
+ });
131
+ };
132
+ startInteraction = (event, direction) => {
133
+ if ((event.button ?? 0) !== 0 || !this.innerContainer.current) {
134
+ return;
135
+ }
136
+ const { left, top, width, height } = this.innerContainer.current.getBoundingClientRect();
137
+ const resizeMinimum = this.resizeMinimum ?? getResizeMinimum(this.innerContainer.current.firstElementChild);
138
+ if (direction !== 'move') {
139
+ this.resizeMinimum = resizeMinimum;
140
+ }
141
+ this.interaction = {
142
+ pointerId: event.pointerId,
143
+ direction,
144
+ startX: event.clientX,
145
+ startY: event.clientY,
146
+ ...resizeMinimum,
147
+ geometry: { left, top, width, height },
148
+ };
149
+ this.setState(({ resized }) => ({ geometry: { left, top, width, height }, resized: resized || direction !== 'move' }));
150
+ event.currentTarget.setPointerCapture?.(event.pointerId);
151
+ event.preventDefault();
152
+ };
153
+ continueInteraction = (event) => {
154
+ const interaction = this.interaction;
155
+ if (!interaction || interaction.pointerId !== event.pointerId) {
156
+ return;
157
+ }
158
+ const dx = event.clientX - interaction.startX;
159
+ const dy = event.clientY - interaction.startY;
160
+ const { width: viewportWidth, height: viewportHeight } = getViewportSize();
161
+ const start = interaction.geometry;
162
+ if (interaction.direction === 'move') {
163
+ this.setState({ geometry: moveGeometry(start, dx, dy, viewportWidth, viewportHeight) });
164
+ return;
165
+ }
166
+ this.setState({
167
+ geometry: resizeGeometry(interaction, dx, dy, viewportWidth, viewportHeight),
168
+ resized: true,
169
+ });
170
+ };
171
+ stopInteraction = (event) => {
172
+ if (this.interaction?.pointerId !== event.pointerId) {
173
+ return;
174
+ }
175
+ this.interaction = null;
176
+ if (event.currentTarget.hasPointerCapture?.(event.pointerId)) {
177
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
178
+ }
179
+ };
180
+ startMove = (event) => {
181
+ const target = event.target;
182
+ if (!this.props.movable || !(target instanceof Element)) {
183
+ return;
184
+ }
185
+ const handle = target.closest('[data-ring-dialog-move-handle], [data-ring-island-header]');
186
+ if (handle?.parentElement === event.currentTarget) {
187
+ this.startInteraction(event, 'move');
188
+ }
189
+ };
111
190
  render() {
112
- const { show, showCloseButton, onOverlayClick, onCloseAttempt, onEscPress, onCloseClick, children, className, contentClassName, trapFocus, 'data-test': dataTest, closeButtonInside, portalTarget, label, closeButtonTitle, dense, shortcutOptions, native, modal, preventBodyScroll, ...restProps } = this.props;
191
+ const { show, showCloseButton, onOverlayClick, onCloseAttempt, onEscPress, onCloseClick, children, className, contentClassName, trapFocus, 'data-test': dataTest, portalTarget, label, closeButtonTitle, dense, shortcutOptions, native, modal, preventBodyScroll, movable, resizable, autoFocusFirst, focusBackOnClose, focusBackOnExit, trapDisabled, ...restProps } = this.props;
113
192
  const classes = classNames(styles.container, className);
114
193
  const shortcutsMap = this.getShortcutsMap();
115
194
  const content = (<>
116
- <Shortcuts map={shortcutsMap} scope={this.state.shortcutsScope} options={this.props.shortcutOptions}/>
195
+ <Shortcuts disabled={!show} map={shortcutsMap} scope={this.state.shortcutsScope} options={shortcutOptions}/>
117
196
  {(onOverlayClick !== noop || onCloseAttempt !== noop) && (<div
118
197
  // click handler is duplicated in close button
119
198
  role='presentation' className={styles.clickableOverlay} onClick={this.handleClick} data-test='ring-dialog-overlay'/>)}
120
- <div className={styles.innerContainer}>
199
+ <div ref={this.innerContainer} data-test='ring-dialog-inner-container' className={classNames(styles.innerContainer, { [styles.resized]: this.state.resized })} style={this.state.geometry
200
+ ? {
201
+ position: 'fixed',
202
+ left: this.state.geometry.left,
203
+ top: this.state.geometry.top,
204
+ width: this.state.resized ? this.state.geometry.width : undefined,
205
+ height: this.state.resized ? this.state.geometry.height : undefined,
206
+ }
207
+ : undefined}>
121
208
  <AdaptiveIsland className={classNames(styles.content, contentClassName, {
122
209
  [styles.dense]: dense,
123
- [styles.contentCloseButtonOutside]: !closeButtonInside,
124
- [styles.contentCloseButtonInside]: closeButtonInside,
125
- })} data-test='ring-dialog' role='dialog' aria-label={label}>
210
+ [styles.movable]: movable,
211
+ })} data-test='ring-dialog' role={native ? undefined : 'dialog'} aria-label={native ? undefined : label} onPointerDown={this.startMove} onPointerMove={this.continueInteraction} onPointerUp={this.stopInteraction} onPointerCancel={this.stopInteraction} onLostPointerCapture={this.stopInteraction}>
126
212
  {children}
127
- {showCloseButton && (<Button icon={closeIcon} data-test='ring-dialog-close-button' className={classNames(styles.closeButton, {
128
- [styles.closeButtonOutside]: !closeButtonInside,
129
- [styles.closeButtonInside]: closeButtonInside,
130
- })} iconClassName={classNames(styles.closeIcon, {
131
- [styles.closeIconOutside]: !closeButtonInside,
132
- })} onClick={this.onCloseClick} title={closeButtonTitle} aria-label={closeButtonTitle || 'close dialog'}/>)}
213
+ <DialogControls movable={movable} resizable={resizable} showCloseButton={showCloseButton} closeButtonTitle={closeButtonTitle} onCloseClick={this.onCloseClick} onStartInteraction={this.startInteraction} onStopInteraction={this.stopInteraction}/>
133
214
  </AdaptiveIsland>
134
215
  </div>
135
216
  </>);
136
217
  if (native) {
137
- return (<dialog className={classNames(styles.nativeDialog, className)} ref={this.nativeDialog} data-rg-modal-dialog-container={modal ? '' : undefined}>
218
+ return (<dialog {...restProps} aria-label={label} className={classNames(styles.nativeDialog, className)} data-test={dataTests('ring-dialog-container', dataTest)} ref={this.nativeDialog} tabIndex={getNativeTabIndex(autoFocusFirst)} onCancel={event => event.preventDefault()} data-rg-modal-dialog-container={modal ? '' : undefined}>
138
219
  <PopupTarget id={this.uid} className={styles.popupTarget}>
139
220
  {target => (<>
140
221
  {content}
@@ -157,7 +238,7 @@ export default class Dialog extends PureComponent {
157
238
  }
158
239
  }
159
240
  return createPortal(<PopupTarget id={this.uid} className={styles.popupTarget}>
160
- {target => (<TabTrap trapDisabled={!trapFocus} data-test={dataTests('ring-dialog-container', dataTest)} data-rg-modal-dialog-container='' ref={this.dialogRef} className={classes} role='presentation' {...restProps}>
241
+ {target => (<TabTrap trapDisabled={getTrapDisabled(trapFocus, trapDisabled)} autoFocusFirst={autoFocusFirst} focusBackOnClose={focusBackOnClose} focusBackOnExit={focusBackOnExit} data-test={dataTests('ring-dialog-container', dataTest)} data-rg-modal-dialog-container='' ref={this.dialogRef} className={classes} role='presentation' {...restProps}>
161
242
  {content}
162
243
  {target}
163
244
  </TabTrap>)}
@@ -0,0 +1,5 @@
1
+ declare const getViewportSize: () => {
2
+ width: number;
3
+ height: number;
4
+ };
5
+ export default getViewportSize;
@@ -0,0 +1,5 @@
1
+ const getViewportSize = () => ({
2
+ width: document.documentElement.clientWidth || window.innerWidth,
3
+ height: document.documentElement.clientHeight || window.innerHeight,
4
+ });
5
+ export default getViewportSize;
@@ -97,23 +97,12 @@ export default class HTTP {
97
97
  const contentType = response.headers.get('content-type');
98
98
  const isJson = contentType && contentType.indexOf('application/json') !== -1;
99
99
  if (!response.ok) {
100
- let resJson;
101
- try {
102
- resJson = await (isJson ? response.json() : response.text());
103
- }
104
- catch (err) {
105
- // noop
106
- }
100
+ const resJson = await (isJson ? response.json() : response.text());
107
101
  throw new HTTPError(response, resJson);
108
102
  }
109
- try {
110
- const parsedResponse = await (isJson ? response.json() : { data: await response.text() });
111
- this._storeRequestMeta(parsedResponse, response);
112
- return parsedResponse;
113
- }
114
- catch (err) {
115
- return response;
116
- }
103
+ const parsedResponse = await (isJson ? response.json() : { data: await response.text() });
104
+ this._storeRequestMeta(parsedResponse, response);
105
+ return parsedResponse;
117
106
  }
118
107
  fetch = async (url, params = {}) => {
119
108
  const { body, query = {}, ...fetchConfig } = params;
@@ -56,7 +56,7 @@ class Header extends Component {
56
56
  }
57
57
  : undefined;
58
58
  const titleStyle = this.getTitleStyle(phase);
59
- return (<div {...restProps} data-test='ring-island-header' className={classes} style={headerStyle}>
59
+ return (<div {...restProps} data-ring-island-header='' data-test='ring-island-header' className={classes} style={headerStyle}>
60
60
  {wrapWithTitle && (<h2 className={styles.title} style={titleStyle}>
61
61
  {children}
62
62
  </h2>)}
@@ -7,6 +7,9 @@ const reactRoot = createRoot(containerElement);
7
7
  * Renders LoginDialog into virtual node to skip maintaining container
8
8
  */
9
9
  function renderLoginDialog(props) {
10
+ if (!containerElement.isConnected) {
11
+ document.body.appendChild(containerElement);
12
+ }
10
13
  reactRoot.render(<ControlsHeightContext value={getGlobalControlsHeight()}>
11
14
  <LoginDialog {...props}/>
12
15
  </ControlsHeightContext>);
@@ -13,6 +13,6 @@
13
13
  }
14
14
 
15
15
  & > button:not(:last-child) {
16
- margin-right: var(--ring-unit);
16
+ margin-inline-end: var(--ring-unit);
17
17
  }
18
18
  }
@@ -188,6 +188,9 @@ export default class UserAgreementService {
188
188
  preview,
189
189
  ...restOptions,
190
190
  };
191
+ if (!this.container.isConnected) {
192
+ document.body.appendChild(this.container);
193
+ }
191
194
  this.reactRoot.render(<ControlsHeightContext value={getGlobalControlsHeight()}>
192
195
  <UserAgreement {...props}/>
193
196
  </ControlsHeightContext>);
@@ -2,10 +2,6 @@
2
2
 
3
3
  @value extra-small-screen-media from '../global/global.css';
4
4
 
5
- .agreementDialog {
6
- padding-bottom: 80px;
7
- }
8
-
9
5
  .dialogContent {
10
6
  width: 600px;
11
7
 
@@ -17,13 +13,11 @@
17
13
  }
18
14
 
19
15
  .suggestion {
16
+ margin-inline-end: var(--ring-unit);
17
+
20
18
  padding-bottom: var(--ring-unit);
21
19
 
22
20
  white-space: nowrap;
23
21
 
24
22
  font-size: 12px;
25
23
  }
26
-
27
- .remindLaterButton {
28
- float: right;
29
- }
@@ -2,7 +2,6 @@
2
2
  * @name User Agreement
3
3
  */
4
4
  import { PureComponent } from 'react';
5
- import classNames from 'classnames';
6
5
  import Dialog from '../dialog/dialog';
7
6
  import { Content, Header } from '../island/island';
8
7
  import Panel from '../panel/panel';
@@ -29,26 +28,25 @@ export default class UserAgreement extends PureComponent {
29
28
  const { translations, onAccept, onDecline, onClose, onRemindLater, children, show, preview, className } = this.props;
30
29
  return (<I18nContext.Consumer>
31
30
  {/* eslint-disable-next-line complexity */}
32
- {({ translate }) => (<Dialog label={translations?.userAgreement ?? translate('userAgreement')} show={show} className={classNames(style.agreementDialog, className)} contentClassName={style.dialogContent} trapFocus autoFocusFirst={false} data-test='user-agreement'>
31
+ {({ translate }) => (<Dialog label={translations?.userAgreement ?? translate('userAgreement')} show={show} className={className ?? undefined} contentClassName={style.dialogContent} trapFocus autoFocusFirst={false} data-test='user-agreement'>
33
32
  <Header>{translations?.userAgreement ?? translate('userAgreement')}</Header>
34
33
  <Content tabIndex={0} fade onScrollToBottom={this.onScrollToBottom}>
35
34
  {children}
36
35
  </Content>
37
36
  {!preview && (<Panel>
38
37
  {onRemindLater && !scrolledDown && (<div className={style.suggestion}>{translations?.scrollToAccept ?? translate('scrollToAccept')}</div>)}
39
- <Button primary disabled={!scrolledDown} onClick={onAccept} data-test='accept'>
40
- {translations?.accept ?? translate('accept')}
41
- </Button>
42
- <Button onClick={onDecline} autoFocus data-test='decline'>
43
- {translations?.decline ?? translate('decline')}
44
- </Button>
45
-
46
38
  {!onRemindLater && !scrolledDown && (<span className={style.suggestion}>
47
39
  {translations?.scrollToAccept ?? translate('scrollToAccept')}
48
40
  </span>)}
49
- {onRemindLater && (<Button className={style.remindLaterButton} onClick={onRemindLater} data-test='later'>
41
+ <Button onClick={onDecline} autoFocus data-test='decline'>
42
+ {translations?.decline ?? translate('decline')}
43
+ </Button>
44
+ {onRemindLater && (<Button onClick={onRemindLater} data-test='later'>
50
45
  {translations?.remindLater ?? translate('remindLater')}
51
46
  </Button>)}
47
+ <Button primary disabled={!scrolledDown} onClick={onAccept} data-test='accept'>
48
+ {translations?.accept ?? translate('accept')}
49
+ </Button>
52
50
  </Panel>)}
53
51
  {preview && (<Panel>
54
52
  <Button onClick={onClose} autoFocus data-test='close'>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetbrains/ring-ui",
3
- "version": "8.0.0-beta.10",
3
+ "version": "8.0.0-beta.11",
4
4
  "description": "JetBrains UI library",
5
5
  "author": {
6
6
  "name": "JetBrains"
@@ -218,7 +218,7 @@
218
218
  "@babel/core": "^7.29.7",
219
219
  "@babel/preset-typescript": "^8.0.1",
220
220
  "@jetbrains/babel-preset-jetbrains": "^2.4.0",
221
- "@jetbrains/icons": "^5.23.0",
221
+ "@jetbrains/icons": "^5.23.3",
222
222
  "@jetbrains/postcss-require-hover": "^0.2.0",
223
223
  "@types/combokeys": "^2.4.9",
224
224
  "@types/element-resize-detector": "^1.1.6",
@@ -240,8 +240,8 @@
240
240
  "highlight.js": "^10.7.2",
241
241
  "just-debounce-it": "^3.2.0",
242
242
  "memoize-one": "^6.0.0",
243
- "postcss": "^8.5.15",
244
- "postcss-calc": "^10.1.1",
243
+ "postcss": "^8.5.25",
244
+ "postcss-calc": "^11.0.1",
245
245
  "postcss-font-family-system-ui": "^5.0.0",
246
246
  "postcss-loader": "^8.2.1",
247
247
  "postcss-modules-values-replace": "^4.2.2",
@@ -258,7 +258,7 @@
258
258
  "util-deprecate": "^1.0.2"
259
259
  },
260
260
  "engines": {
261
- "node": ">=14.0",
261
+ "node": "^22.22.3 || ^24.15 || >=26.0",
262
262
  "npm": ">=6.0.0"
263
263
  },
264
264
  "bugs": {