@instructure/ui-modal 11.5.0 → 11.5.1-snapshot-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [11.5.1-snapshot-1](https://github.com/instructure/instructure-ui/compare/v11.5.0...v11.5.1-snapshot-1) (2026-02-18)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **ui-modal:** voiceOver in Chrome treats scrollable modal body as single interactive object preventing line-by-line navigation ([0be3fb1](https://github.com/instructure/instructure-ui/commit/0be3fb1dee541a37c2d38ae6656fd0330f651265))
12
+
13
+
14
+
15
+
16
+
6
17
  # [11.5.0](https://github.com/instructure/instructure-ui/compare/v11.4.0...v11.5.0) (2026-02-03)
7
18
 
8
19
  **Note:** Version bump only for package @instructure/ui-modal
@@ -33,13 +33,14 @@ import { withStyle } from '@instructure/emotion';
33
33
  import generateStyle from "./styles.js";
34
34
  import generateComponentTheme from "./theme.js";
35
35
  import { allowedProps } from "./props.js";
36
- import { jsx as _jsx } from "@emotion/react/jsx-runtime";
36
+ import ModalContext from "../ModalContext.js";
37
37
  /**
38
38
  ---
39
39
  parent: Modal
40
40
  id: Modal.Body
41
41
  ---
42
42
  **/
43
+ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
43
44
  let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_class = (_ModalBody = class ModalBody extends Component {
44
45
  constructor(props) {
45
46
  super(props);
@@ -82,7 +83,7 @@ let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_
82
83
  return void 0;
83
84
  }
84
85
  render() {
85
- var _this$props$styles, _finalRef$scrollHeigh, _finalRef$getBounding, _finalRef$getBounding2;
86
+ var _finalRef$scrollHeigh, _finalRef$getBounding, _finalRef$getBounding2;
86
87
  const _this$props3 = this.props,
87
88
  as = _this$props3.as,
88
89
  elementRef = _this$props3.elementRef,
@@ -97,23 +98,29 @@ let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_
97
98
  // TODO rethink, the 'as' prop, likely its not a good idea to allow React
98
99
  // components. See INSTUI-4674
99
100
  const finalRef = this.getFinalRef(this.ref);
100
- return _jsx(View, {
101
- ...passthroughProps,
102
- display: "block",
103
- "data-cid": "ModalBody",
104
- width: isFit ? '100%' : void 0,
105
- height: isFit ? '100%' : void 0,
106
- elementRef: this.handleRef,
107
- as: as,
108
- css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.modalBody,
109
- padding: padding
110
- // check if there is a scrollbar, if so, the element has to be tabbable to be able to scroll with keyboard only
111
- // epsilon tolerance is used to avoid scrollbar for rounding errors
112
- ,
113
- ...(finalRef && Math.abs(((_finalRef$scrollHeigh = finalRef.scrollHeight) !== null && _finalRef$scrollHeigh !== void 0 ? _finalRef$scrollHeigh : 0) - ((_finalRef$getBounding = (_finalRef$getBounding2 = finalRef.getBoundingClientRect()) === null || _finalRef$getBounding2 === void 0 ? void 0 : _finalRef$getBounding2.height) !== null && _finalRef$getBounding !== void 0 ? _finalRef$getBounding : 0)) > 1 ? {
114
- tabIndex: 0
115
- } : {}),
116
- children: children
101
+ const hasScrollbar = finalRef && Math.abs(((_finalRef$scrollHeigh = finalRef.scrollHeight) !== null && _finalRef$scrollHeigh !== void 0 ? _finalRef$scrollHeigh : 0) - ((_finalRef$getBounding = (_finalRef$getBounding2 = finalRef.getBoundingClientRect()) === null || _finalRef$getBounding2 === void 0 ? void 0 : _finalRef$getBounding2.height) !== null && _finalRef$getBounding !== void 0 ? _finalRef$getBounding : 0)) > 1;
102
+ return _jsx(ModalContext.Consumer, {
103
+ children: value => {
104
+ var _this$props$styles;
105
+ return _jsx(View, {
106
+ ...passthroughProps,
107
+ display: "block",
108
+ "data-cid": "ModalBody",
109
+ width: isFit ? '100%' : void 0,
110
+ height: isFit ? '100%' : void 0,
111
+ elementRef: this.handleRef,
112
+ as: as,
113
+ css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.modalBody,
114
+ padding: padding
115
+ // check if there is a scrollbar, if so, the element has to be tabbable
116
+ ,
117
+ ...(hasScrollbar ? {
118
+ tabIndex: 0,
119
+ 'aria-label': value.bodyScrollAriaLabel
120
+ } : {}),
121
+ children: children
122
+ });
123
+ }
117
124
  });
118
125
  }
119
126
  }, _ModalBody.displayName = "ModalBody", _ModalBody.componentId = 'Modal.Body', _ModalBody.allowedProps = allowedProps, _ModalBody.defaultProps = {
@@ -0,0 +1,34 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { createContext } from 'react';
26
+ /**
27
+ * React context created by the `Modal` component
28
+ * @private
29
+ */
30
+ const ModalContext = /*#__PURE__*/createContext({
31
+ bodyScrollAriaLabel: void 0
32
+ });
33
+ export default ModalContext;
34
+ export { ModalContext };
@@ -32,6 +32,7 @@ import { CloseButton } from '@instructure/ui-buttons';
32
32
  import generateStyle from "./styles.js";
33
33
  import generateComponentTheme from "./theme.js";
34
34
  import { allowedProps } from "./props.js";
35
+ import ModalContext from "../ModalContext.js";
35
36
  import { jsx as _jsx } from "@emotion/react/jsx-runtime";
36
37
  /**
37
38
  ---
@@ -43,8 +44,30 @@ let ModalHeader = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
43
44
  constructor(...args) {
44
45
  super(...args);
45
46
  this.ref = null;
47
+ /**
48
+ * Gets all text in a DOM subtree, text under <button> nodes is excluded
49
+ */
50
+ this.getTextExcludingButtons = root => {
51
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
52
+ acceptNode(node) {
53
+ var _node$parentElement;
54
+ return (_node$parentElement = node.parentElement) !== null && _node$parentElement !== void 0 && _node$parentElement.closest('button') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
55
+ }
56
+ });
57
+ let text = '';
58
+ let current;
59
+ while (current = walker.nextNode()) {
60
+ text += current.nodeValue;
61
+ }
62
+ return text;
63
+ };
46
64
  this.handleRef = el => {
47
65
  this.ref = el;
66
+ if (el) {
67
+ var _this$context$setBody, _this$context;
68
+ const txt = this.getTextExcludingButtons(el);
69
+ (_this$context$setBody = (_this$context = this.context).setBodyScrollAriaLabel) === null || _this$context$setBody === void 0 ? void 0 : _this$context$setBody.call(_this$context, txt);
70
+ }
48
71
  };
49
72
  this.makeStyleProps = () => {
50
73
  return {
@@ -85,6 +108,6 @@ let ModalHeader = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
85
108
  }, _ModalHeader.displayName = "ModalHeader", _ModalHeader.componentId = 'Modal.Header', _ModalHeader.allowedProps = allowedProps, _ModalHeader.defaultProps = {
86
109
  variant: 'default',
87
110
  spacing: 'default'
88
- }, _ModalHeader)) || _class);
111
+ }, _ModalHeader.contextType = ModalContext, _ModalHeader)) || _class);
89
112
  export default ModalHeader;
90
113
  export { ModalHeader };
package/es/Modal/index.js CHANGED
@@ -39,13 +39,14 @@ import { withStyle } from '@instructure/emotion';
39
39
  import generateStyle from "./styles.js";
40
40
  import generateComponentTheme from "./theme.js";
41
41
  import { allowedProps } from "./props.js";
42
- import { jsx as _jsx } from "@emotion/react/jsx-runtime";
42
+ import ModalContext from "./ModalContext.js";
43
43
  /**
44
44
  ---
45
45
  category: components
46
46
  tags: overlay, portal, dialog
47
47
  ---
48
48
  **/
49
+ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
49
50
  let Modal = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_class = (_Modal = class Modal extends Component {
50
51
  constructor(props) {
51
52
  var _props$open;
@@ -78,7 +79,8 @@ let Modal = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
78
79
  this.state = {
79
80
  transitioning: false,
80
81
  open: (_props$open = props.open) !== null && _props$open !== void 0 ? _props$open : false,
81
- windowHeight: 99999
82
+ windowHeight: 99999,
83
+ bodyScrollAriaLabel: void 0
82
84
  };
83
85
  }
84
86
  componentDidMount() {
@@ -228,10 +230,18 @@ let Modal = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_clas
228
230
  onExit: onExit,
229
231
  onExiting: onExiting,
230
232
  onExited: createChainedFunction(this.handleTransitionComplete, onExited, onClose),
231
- children: constrain === 'parent' ? _jsx("span", {
232
- css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.constrainContext,
233
- children: this.renderDialog(passthroughProps)
234
- }) : this.renderDialog(passthroughProps)
233
+ children: _jsx(ModalContext.Provider, {
234
+ value: {
235
+ bodyScrollAriaLabel: this.state.bodyScrollAriaLabel,
236
+ setBodyScrollAriaLabel: txt => this.setState({
237
+ bodyScrollAriaLabel: txt
238
+ })
239
+ },
240
+ children: constrain === 'parent' ? _jsx("span", {
241
+ css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.constrainContext,
242
+ children: this.renderDialog(passthroughProps)
243
+ }) : this.renderDialog(passthroughProps)
244
+ })
235
245
  })
236
246
  });
237
247
  }
@@ -14,6 +14,7 @@ var _emotion = require("@instructure/emotion");
14
14
  var _styles = _interopRequireDefault(require("./styles"));
15
15
  var _theme = _interopRequireDefault(require("./theme"));
16
16
  var _props = require("./props");
17
+ var _ModalContext = _interopRequireDefault(require("../ModalContext"));
17
18
  var _jsxRuntime = require("@emotion/react/jsx-runtime");
18
19
  const _excluded = ["as", "elementRef", "overflow", "variant", "padding", "children"];
19
20
  var _dec, _class, _ModalBody;
@@ -88,7 +89,7 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyle)(_styles.defa
88
89
  return void 0;
89
90
  }
90
91
  render() {
91
- var _this$props$styles, _finalRef$scrollHeigh, _finalRef$getBounding, _finalRef$getBounding2;
92
+ var _finalRef$scrollHeigh, _finalRef$getBounding, _finalRef$getBounding2;
92
93
  const _this$props3 = this.props,
93
94
  as = _this$props3.as,
94
95
  elementRef = _this$props3.elementRef,
@@ -103,23 +104,29 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyle)(_styles.defa
103
104
  // TODO rethink, the 'as' prop, likely its not a good idea to allow React
104
105
  // components. See INSTUI-4674
105
106
  const finalRef = this.getFinalRef(this.ref);
106
- return (0, _jsxRuntime.jsx)(_View.View, {
107
- ...passthroughProps,
108
- display: "block",
109
- "data-cid": "ModalBody",
110
- width: isFit ? '100%' : void 0,
111
- height: isFit ? '100%' : void 0,
112
- elementRef: this.handleRef,
113
- as: as,
114
- css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.modalBody,
115
- padding: padding
116
- // check if there is a scrollbar, if so, the element has to be tabbable to be able to scroll with keyboard only
117
- // epsilon tolerance is used to avoid scrollbar for rounding errors
118
- ,
119
- ...(finalRef && Math.abs(((_finalRef$scrollHeigh = finalRef.scrollHeight) !== null && _finalRef$scrollHeigh !== void 0 ? _finalRef$scrollHeigh : 0) - ((_finalRef$getBounding = (_finalRef$getBounding2 = finalRef.getBoundingClientRect()) === null || _finalRef$getBounding2 === void 0 ? void 0 : _finalRef$getBounding2.height) !== null && _finalRef$getBounding !== void 0 ? _finalRef$getBounding : 0)) > 1 ? {
120
- tabIndex: 0
121
- } : {}),
122
- children: children
107
+ const hasScrollbar = finalRef && Math.abs(((_finalRef$scrollHeigh = finalRef.scrollHeight) !== null && _finalRef$scrollHeigh !== void 0 ? _finalRef$scrollHeigh : 0) - ((_finalRef$getBounding = (_finalRef$getBounding2 = finalRef.getBoundingClientRect()) === null || _finalRef$getBounding2 === void 0 ? void 0 : _finalRef$getBounding2.height) !== null && _finalRef$getBounding !== void 0 ? _finalRef$getBounding : 0)) > 1;
108
+ return (0, _jsxRuntime.jsx)(_ModalContext.default.Consumer, {
109
+ children: value => {
110
+ var _this$props$styles;
111
+ return (0, _jsxRuntime.jsx)(_View.View, {
112
+ ...passthroughProps,
113
+ display: "block",
114
+ "data-cid": "ModalBody",
115
+ width: isFit ? '100%' : void 0,
116
+ height: isFit ? '100%' : void 0,
117
+ elementRef: this.handleRef,
118
+ as: as,
119
+ css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.modalBody,
120
+ padding: padding
121
+ // check if there is a scrollbar, if so, the element has to be tabbable
122
+ ,
123
+ ...(hasScrollbar ? {
124
+ tabIndex: 0,
125
+ 'aria-label': value.bodyScrollAriaLabel
126
+ } : {}),
127
+ children: children
128
+ });
129
+ }
123
130
  });
124
131
  }
125
132
  }, _ModalBody.displayName = "ModalBody", _ModalBody.componentId = 'Modal.Body', _ModalBody.allowedProps = _props.allowedProps, _ModalBody.defaultProps = {
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.ModalContext = void 0;
7
+ var _react = require("react");
8
+ /*
9
+ * The MIT License (MIT)
10
+ *
11
+ * Copyright (c) 2015 - present Instructure, Inc.
12
+ *
13
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ * of this software and associated documentation files (the "Software"), to deal
15
+ * in the Software without restriction, including without limitation the rights
16
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ * copies of the Software, and to permit persons to whom the Software is
18
+ * furnished to do so, subject to the following conditions:
19
+ *
20
+ * The above copyright notice and this permission notice shall be included in all
21
+ * copies or substantial portions of the Software.
22
+ *
23
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ * SOFTWARE.
30
+ */
31
+
32
+ /**
33
+ * React context created by the `Modal` component
34
+ * @private
35
+ */
36
+ const ModalContext = exports.ModalContext = /*#__PURE__*/(0, _react.createContext)({
37
+ bodyScrollAriaLabel: void 0
38
+ });
39
+ var _default = exports.default = ModalContext;
@@ -14,6 +14,7 @@ var _CloseButton = require("@instructure/ui-buttons/lib/CloseButton");
14
14
  var _styles = _interopRequireDefault(require("./styles"));
15
15
  var _theme = _interopRequireDefault(require("./theme"));
16
16
  var _props = require("./props");
17
+ var _ModalContext = _interopRequireDefault(require("../ModalContext"));
17
18
  var _jsxRuntime = require("@emotion/react/jsx-runtime");
18
19
  const _excluded = ["children"];
19
20
  var _dec, _class, _ModalHeader;
@@ -50,8 +51,30 @@ let ModalHeader = exports.ModalHeader = (_dec = (0, _emotion.withStyle)(_styles.
50
51
  constructor(...args) {
51
52
  super(...args);
52
53
  this.ref = null;
54
+ /**
55
+ * Gets all text in a DOM subtree, text under <button> nodes is excluded
56
+ */
57
+ this.getTextExcludingButtons = root => {
58
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
59
+ acceptNode(node) {
60
+ var _node$parentElement;
61
+ return (_node$parentElement = node.parentElement) !== null && _node$parentElement !== void 0 && _node$parentElement.closest('button') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
62
+ }
63
+ });
64
+ let text = '';
65
+ let current;
66
+ while (current = walker.nextNode()) {
67
+ text += current.nodeValue;
68
+ }
69
+ return text;
70
+ };
53
71
  this.handleRef = el => {
54
72
  this.ref = el;
73
+ if (el) {
74
+ var _this$context$setBody, _this$context;
75
+ const txt = this.getTextExcludingButtons(el);
76
+ (_this$context$setBody = (_this$context = this.context).setBodyScrollAriaLabel) === null || _this$context$setBody === void 0 ? void 0 : _this$context$setBody.call(_this$context, txt);
77
+ }
55
78
  };
56
79
  this.makeStyleProps = () => {
57
80
  return {
@@ -92,5 +115,5 @@ let ModalHeader = exports.ModalHeader = (_dec = (0, _emotion.withStyle)(_styles.
92
115
  }, _ModalHeader.displayName = "ModalHeader", _ModalHeader.componentId = 'Modal.Header', _ModalHeader.allowedProps = _props.allowedProps, _ModalHeader.defaultProps = {
93
116
  variant: 'default',
94
117
  spacing: 'default'
95
- }, _ModalHeader)) || _class);
118
+ }, _ModalHeader.contextType = _ModalContext.default, _ModalHeader)) || _class);
96
119
  var _default = exports.default = ModalHeader;
@@ -40,6 +40,7 @@ var _emotion = require("@instructure/emotion");
40
40
  var _styles = _interopRequireDefault(require("./styles"));
41
41
  var _theme = _interopRequireDefault(require("./theme"));
42
42
  var _props = require("./props");
43
+ var _ModalContext = _interopRequireDefault(require("./ModalContext"));
43
44
  var _jsxRuntime = require("@emotion/react/jsx-runtime");
44
45
  const _excluded = ["open", "onOpen", "onClose", "mountNode", "insertAt", "transition", "onEnter", "onEntering", "onEntered", "onExit", "onExiting", "onExited", "constrain", "overflow"];
45
46
  var _dec, _class, _Modal;
@@ -104,7 +105,8 @@ let Modal = exports.Modal = (_dec = (0, _emotion.withStyle)(_styles.default, _th
104
105
  this.state = {
105
106
  transitioning: false,
106
107
  open: (_props$open = props.open) !== null && _props$open !== void 0 ? _props$open : false,
107
- windowHeight: 99999
108
+ windowHeight: 99999,
109
+ bodyScrollAriaLabel: void 0
108
110
  };
109
111
  }
110
112
  componentDidMount() {
@@ -254,10 +256,18 @@ let Modal = exports.Modal = (_dec = (0, _emotion.withStyle)(_styles.default, _th
254
256
  onExit: onExit,
255
257
  onExiting: onExiting,
256
258
  onExited: (0, _createChainedFunction.createChainedFunction)(this.handleTransitionComplete, onExited, onClose),
257
- children: constrain === 'parent' ? (0, _jsxRuntime.jsx)("span", {
258
- css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.constrainContext,
259
- children: this.renderDialog(passthroughProps)
260
- }) : this.renderDialog(passthroughProps)
259
+ children: (0, _jsxRuntime.jsx)(_ModalContext.default.Provider, {
260
+ value: {
261
+ bodyScrollAriaLabel: this.state.bodyScrollAriaLabel,
262
+ setBodyScrollAriaLabel: txt => this.setState({
263
+ bodyScrollAriaLabel: txt
264
+ })
265
+ },
266
+ children: constrain === 'parent' ? (0, _jsxRuntime.jsx)("span", {
267
+ css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.constrainContext,
268
+ children: this.renderDialog(passthroughProps)
269
+ }) : this.renderDialog(passthroughProps)
270
+ })
261
271
  })
262
272
  });
263
273
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-modal",
3
- "version": "11.5.0",
3
+ "version": "11.5.1-snapshot-1",
4
4
  "description": "A component for displaying content in a dialog overlay",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -15,28 +15,28 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.27.6",
18
- "@instructure/console": "11.5.0",
19
- "@instructure/shared-types": "11.5.0",
20
- "@instructure/emotion": "11.5.0",
21
- "@instructure/ui-buttons": "11.5.0",
22
- "@instructure/ui-dialog": "11.5.0",
23
- "@instructure/ui-dom-utils": "11.5.0",
24
- "@instructure/ui-motion": "11.5.0",
25
- "@instructure/ui-overlays": "11.5.0",
26
- "@instructure/ui-portal": "11.5.0",
27
- "@instructure/ui-utils": "11.5.0",
28
- "@instructure/ui-react-utils": "11.5.0",
29
- "@instructure/ui-view": "11.5.0"
18
+ "@instructure/console": "11.5.1-snapshot-1",
19
+ "@instructure/emotion": "11.5.1-snapshot-1",
20
+ "@instructure/ui-buttons": "11.5.1-snapshot-1",
21
+ "@instructure/shared-types": "11.5.1-snapshot-1",
22
+ "@instructure/ui-dialog": "11.5.1-snapshot-1",
23
+ "@instructure/ui-dom-utils": "11.5.1-snapshot-1",
24
+ "@instructure/ui-motion": "11.5.1-snapshot-1",
25
+ "@instructure/ui-overlays": "11.5.1-snapshot-1",
26
+ "@instructure/ui-react-utils": "11.5.1-snapshot-1",
27
+ "@instructure/ui-portal": "11.5.1-snapshot-1",
28
+ "@instructure/ui-utils": "11.5.1-snapshot-1",
29
+ "@instructure/ui-view": "11.5.1-snapshot-1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@testing-library/jest-dom": "^6.6.3",
33
33
  "@testing-library/react": "15.0.7",
34
34
  "@testing-library/user-event": "^14.6.1",
35
35
  "vitest": "^3.2.2",
36
- "@instructure/ui-babel-preset": "11.5.0",
37
- "@instructure/ui-color-utils": "11.5.0",
38
- "@instructure/ui-position": "11.5.0",
39
- "@instructure/ui-themes": "11.5.0"
36
+ "@instructure/ui-position": "11.5.1-snapshot-1",
37
+ "@instructure/ui-babel-preset": "11.5.1-snapshot-1",
38
+ "@instructure/ui-color-utils": "11.5.1-snapshot-1",
39
+ "@instructure/ui-themes": "11.5.1-snapshot-1"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": ">=18 <=19"
@@ -35,6 +35,7 @@ import generateComponentTheme from './theme'
35
35
  import { allowedProps } from './props'
36
36
  import type { ModalBodyProps } from './props'
37
37
  import { UIElement } from '@instructure/shared-types'
38
+ import ModalContext from '../ModalContext'
38
39
 
39
40
  /**
40
41
  ---
@@ -117,30 +118,37 @@ class ModalBody extends Component<ModalBodyProps> {
117
118
  // TODO rethink, the 'as' prop, likely its not a good idea to allow React
118
119
  // components. See INSTUI-4674
119
120
  const finalRef = this.getFinalRef(this.ref)
120
-
121
+ const hasScrollbar =
122
+ finalRef &&
123
+ Math.abs(
124
+ (finalRef.scrollHeight ?? 0) -
125
+ (finalRef.getBoundingClientRect()?.height ?? 0)
126
+ ) > 1
121
127
  return (
122
- <View
123
- {...passthroughProps}
124
- display="block"
125
- data-cid="ModalBody"
126
- width={isFit ? '100%' : undefined}
127
- height={isFit ? '100%' : undefined}
128
- elementRef={this.handleRef}
129
- as={as}
130
- css={this.props.styles?.modalBody}
131
- padding={padding}
132
- // check if there is a scrollbar, if so, the element has to be tabbable to be able to scroll with keyboard only
133
- // epsilon tolerance is used to avoid scrollbar for rounding errors
134
- {...(finalRef &&
135
- Math.abs(
136
- (finalRef.scrollHeight ?? 0) -
137
- (finalRef.getBoundingClientRect()?.height ?? 0)
138
- ) > 1
139
- ? { tabIndex: 0 }
140
- : {})}
141
- >
142
- {children}
143
- </View>
128
+ <ModalContext.Consumer>
129
+ {(value) => (
130
+ <View
131
+ {...passthroughProps}
132
+ display="block"
133
+ data-cid="ModalBody"
134
+ width={isFit ? '100%' : undefined}
135
+ height={isFit ? '100%' : undefined}
136
+ elementRef={this.handleRef}
137
+ as={as}
138
+ css={this.props.styles?.modalBody}
139
+ padding={padding}
140
+ // check if there is a scrollbar, if so, the element has to be tabbable
141
+ {...(hasScrollbar
142
+ ? {
143
+ tabIndex: 0,
144
+ 'aria-label': value.bodyScrollAriaLabel
145
+ }
146
+ : {})}
147
+ >
148
+ {children}
149
+ </View>
150
+ )}
151
+ </ModalContext.Consumer>
144
152
  )
145
153
  }
146
154
  }
@@ -0,0 +1,46 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { createContext } from 'react'
26
+
27
+ type ModalContextType = {
28
+ /**
29
+ * Needed for setting the correct aria-label in the modal body when its scrollable.
30
+ * This is a value read from the Modal's Header.
31
+ */
32
+ bodyScrollAriaLabel?: string
33
+ setBodyScrollAriaLabel?: (txt: string) => void
34
+ }
35
+
36
+ /**
37
+ * React context created by the `Modal` component
38
+ * @private
39
+ */
40
+ const ModalContext = createContext<ModalContextType>({
41
+ bodyScrollAriaLabel: undefined
42
+ })
43
+
44
+ export default ModalContext
45
+ export { ModalContext }
46
+ export type { ModalContextType }
@@ -39,6 +39,7 @@ import generateComponentTheme from './theme'
39
39
 
40
40
  import { allowedProps } from './props'
41
41
  import type { ModalHeaderProps, ModalHeaderStyleProps } from './props'
42
+ import ModalContext from '../ModalContext'
42
43
 
43
44
  type CloseButtonChild = ComponentElement<CloseButtonProps, CloseButton>
44
45
 
@@ -59,9 +60,34 @@ class ModalHeader extends Component<ModalHeaderProps> {
59
60
  }
60
61
 
61
62
  ref: HTMLDivElement | null = null
63
+ declare context: React.ContextType<typeof ModalContext>
64
+ static contextType = ModalContext
65
+
66
+ /**
67
+ * Gets all text in a DOM subtree, text under <button> nodes is excluded
68
+ */
69
+ getTextExcludingButtons = (root: Node) => {
70
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
71
+ acceptNode(node) {
72
+ return node.parentElement?.closest('button')
73
+ ? NodeFilter.FILTER_REJECT
74
+ : NodeFilter.FILTER_ACCEPT
75
+ }
76
+ })
77
+ let text = ''
78
+ let current
79
+ while ((current = walker.nextNode())) {
80
+ text += current.nodeValue
81
+ }
82
+ return text
83
+ }
62
84
 
63
85
  handleRef = (el: HTMLDivElement | null) => {
64
86
  this.ref = el
87
+ if (el) {
88
+ const txt = this.getTextExcludingButtons(el)
89
+ this.context.setBodyScrollAriaLabel?.(txt)
90
+ }
65
91
  }
66
92
 
67
93
  componentDidMount() {