@ultraviolet/ui 1.11.0 → 1.11.2

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/dist/index.d.ts CHANGED
@@ -24,11 +24,10 @@ type ActionBarProps = {
24
24
  */
25
25
  rank?: number;
26
26
  role?: string;
27
- 'aria-modal'?: 'true' | 'false';
28
27
  className?: string;
29
28
  'data-testid'?: string;
30
29
  };
31
- declare const ActionBar: ({ children, role, rank, "aria-modal": ariaModal, className, "data-testid": dataTestId, }: ActionBarProps) => react.ReactPortal;
30
+ declare const ActionBar: ({ children, role, rank, className, "data-testid": dataTestId, }: ActionBarProps) => react.ReactPortal;
32
31
 
33
32
  type ScreenSize = keyof typeof consoleLightTheme.screens;
34
33
  type SCWUITheme = typeof consoleLightTheme;
@@ -33,14 +33,12 @@ const ActionBar = _ref5 => {
33
33
  children,
34
34
  role = 'dialog',
35
35
  rank = 0,
36
- 'aria-modal': ariaModal = 'true',
37
36
  className,
38
37
  'data-testid': dataTestId
39
38
  } = _ref5;
40
39
  return /*#__PURE__*/createPortal(jsx(StyledDiv, {
41
40
  rank: rank,
42
41
  role: role,
43
- "aria-modal": ariaModal,
44
42
  className: className,
45
43
  "data-testid": dataTestId,
46
44
  children: children
@@ -73,6 +73,7 @@ const Dialog = _ref9 => {
73
73
  backdropCss
74
74
  } = _ref9;
75
75
  const containerRef = useRef(document.createElement('div'));
76
+ const dialogRef = useRef(null);
76
77
  const onCloseRef = useRef(onClose);
77
78
 
78
79
  // Portal to put the modal in
@@ -97,14 +98,27 @@ const Dialog = _ref9 => {
97
98
  useEffect(() => {
98
99
  const handleEscPress = event => {
99
100
  if (event.key === 'Escape' && hideOnEsc) {
101
+ event.preventDefault();
102
+ event.stopPropagation();
100
103
  onCloseRef.current();
101
104
  }
102
105
  };
103
106
  if (open) {
104
- document.addEventListener('keyup', handleEscPress);
107
+ dialogRef.current?.focus();
108
+ document.body.addEventListener('keyup', handleEscPress, {
109
+ capture: true
110
+ });
111
+ document.body.addEventListener('keydown', handleEscPress, {
112
+ capture: true
113
+ });
105
114
  }
106
115
  return () => {
107
- document.removeEventListener('keyup', handleEscPress);
116
+ document.body.removeEventListener('keyup', handleEscPress, {
117
+ capture: true
118
+ });
119
+ document.body.removeEventListener('keydown', handleEscPress, {
120
+ capture: true
121
+ });
108
122
  };
109
123
  }, [open, onCloseRef, hideOnEsc]);
110
124
 
@@ -117,6 +131,11 @@ const Dialog = _ref9 => {
117
131
  }
118
132
  }, [preventBodyScroll, open]);
119
133
 
134
+ // Stop focus to prevent unexpected body loose focus
135
+ const stopFocus = useCallback(event => {
136
+ event.stopPropagation();
137
+ }, []);
138
+
120
139
  // Stop click to prevent unexpected dialog close
121
140
  const stopClick = useCallback(event => {
122
141
  event.stopPropagation();
@@ -126,15 +145,53 @@ const Dialog = _ref9 => {
126
145
  const stopKeyUp = useCallback(event => {
127
146
  event.stopPropagation();
128
147
  }, []);
148
+
149
+ // Enable focus trap inside the modal
150
+ const handleFocusTrap = useCallback(event => {
151
+ event.stopPropagation();
152
+ if (event.key === 'Escape') {
153
+ event.preventDefault();
154
+ return;
155
+ }
156
+ const isTabPressed = event.key === 'Tab';
157
+ if (!isTabPressed) {
158
+ return;
159
+ }
160
+ const focusableEls = dialogRef.current?.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled])') ?? [];
161
+
162
+ // Handle case when no interactive element are within the modal (including close icon)
163
+ if (focusableEls.length === 0) {
164
+ event.preventDefault();
165
+ }
166
+ const firstFocusableEl = focusableEls[0];
167
+ const lastFocusableEl = focusableEls[focusableEls.length - 1];
168
+ if (event.shiftKey) {
169
+ if (document.activeElement === firstFocusableEl || document.activeElement === dialogRef.current) {
170
+ lastFocusableEl.focus();
171
+ event.preventDefault();
172
+ }
173
+ } else if (document.activeElement === lastFocusableEl || document.activeElement === dialogRef.current) {
174
+ firstFocusableEl.focus();
175
+ event.preventDefault();
176
+ }
177
+ }, []);
178
+
179
+ // Prevent default behaviour on Escape
180
+ const stopCancel = event => {
181
+ event.preventDefault();
182
+ event.stopPropagation();
183
+ };
129
184
  return /*#__PURE__*/createPortal(jsx(StyledBackdrop, {
130
185
  "data-open": open,
131
186
  onClick: hideOnClickOutside ? onClose : undefined,
132
187
  className: backdropClassName,
133
188
  css: backdropCss,
134
189
  "data-testid": dataTestId ? `${dataTestId}-backdrop` : undefined,
190
+ onFocus: stopFocus,
135
191
  children: jsx(StyledDialog, {
136
192
  css: dialogCss,
137
193
  onKeyUp: stopKeyUp,
194
+ onKeyDown: handleFocusTrap,
138
195
  className: className,
139
196
  id: id,
140
197
  "data-testid": dataTestId,
@@ -143,6 +200,11 @@ const Dialog = _ref9 => {
143
200
  "data-size": size,
144
201
  open: open,
145
202
  onClick: stopClick,
203
+ onCancel: stopCancel,
204
+ onClose: stopCancel,
205
+ "aria-modal": true,
206
+ ref: dialogRef,
207
+ tabIndex: 0,
146
208
  children: open ? children : null
147
209
  })
148
210
  }), containerRef.current);
@@ -30,7 +30,9 @@ const Disclosure = _ref => {
30
30
  if ( /*#__PURE__*/isValidElement(disclosure)) {
31
31
  return /*#__PURE__*/cloneElement(disclosure, {
32
32
  ...disclosure.props,
33
- ref: disclosureRef
33
+ ref: disclosureRef,
34
+ 'aria-controls': id,
35
+ 'aria-haspopup': 'dialog'
34
36
  });
35
37
  }
36
38
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultraviolet/ui",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "Ultraviolet UI",
5
5
  "homepage": "https://github.com/scaleway/ultraviolet#readme",
6
6
  "repository": {
@@ -39,13 +39,13 @@
39
39
  "react-dom": "18.2.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@babel/core": "7.22.11",
42
+ "@babel/core": "7.22.15",
43
43
  "@emotion/babel-plugin": "11.11.0",
44
44
  "@emotion/react": "11.11.1",
45
45
  "@emotion/styled": "11.11.0",
46
- "@types/react": "18.2.14",
46
+ "@types/react": "18.2.21",
47
47
  "@types/react-datepicker": "4.15.0",
48
- "@types/react-dom": "18.2.6",
48
+ "@types/react-dom": "18.2.7",
49
49
  "react": "18.2.0",
50
50
  "react-dom": "18.2.0"
51
51
  },
@@ -68,6 +68,6 @@
68
68
  "react-use-clipboard": "1.0.9",
69
69
  "reakit": "1.3.11",
70
70
  "@ultraviolet/themes": "1.2.1",
71
- "@ultraviolet/icons": "1.3.1"
71
+ "@ultraviolet/icons": "2.0.1"
72
72
  }
73
73
  }