@instructure/ui-modal 11.7.6 → 11.7.7-snapshot-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/CHANGELOG.md +7 -0
- package/es/Modal/v1/ModalBody/index.js +34 -9
- package/es/Modal/v2/ModalBody/index.js +34 -9
- package/lib/Modal/v1/ModalBody/index.js +34 -9
- package/lib/Modal/v2/ModalBody/index.js +34 -9
- package/package.json +18 -18
- package/src/Modal/v1/ModalBody/index.tsx +35 -16
- package/src/Modal/v1/ModalBody/props.ts +1 -0
- package/src/Modal/v2/ModalBody/index.tsx +35 -16
- package/src/Modal/v2/ModalBody/props.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/Modal/v1/ModalBody/index.d.ts +5 -2
- package/types/Modal/v1/ModalBody/index.d.ts.map +1 -1
- package/types/Modal/v1/ModalBody/props.d.ts +1 -0
- package/types/Modal/v1/ModalBody/props.d.ts.map +1 -1
- package/types/Modal/v2/ModalBody/index.d.ts +5 -2
- package/types/Modal/v2/ModalBody/index.d.ts.map +1 -1
- package/types/Modal/v2/ModalBody/props.d.ts +1 -0
- package/types/Modal/v2/ModalBody/props.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.7.7-snapshot-0](https://github.com/instructure/instructure-ui/compare/v11.7.6...v11.7.7-snapshot-0) (2026-09-18)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **ui-modal:** avoid redundant Modal.Body re-renders from its observers ([bccca5c](https://github.com/instructure/instructure-ui/commit/bccca5cc1f4bc10001a344b9142d2122a265a573))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
## [11.7.6](https://github.com/instructure/instructure-ui/compare/v11.7.5...v11.7.6) (2026-09-14)
|
|
7
14
|
|
|
8
15
|
**Note:** Version bump only for package @instructure/ui-modal
|
|
@@ -49,6 +49,13 @@ let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_
|
|
|
49
49
|
as: 'div',
|
|
50
50
|
variant: 'default'
|
|
51
51
|
};
|
|
52
|
+
state = {
|
|
53
|
+
isFirefox: false,
|
|
54
|
+
needsTabIndex: false
|
|
55
|
+
};
|
|
56
|
+
// mirrors state.needsTabIndex, but readable synchronously in the observer
|
|
57
|
+
// callbacks below, where a pending setState would make this.state stale
|
|
58
|
+
lastNeedsTabIndex = false;
|
|
52
59
|
ref = null;
|
|
53
60
|
resizeObserver;
|
|
54
61
|
mutationObserver;
|
|
@@ -77,9 +84,10 @@ let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_
|
|
|
77
84
|
}
|
|
78
85
|
const finalRef = this.getFinalRef(this.ref);
|
|
79
86
|
if (finalRef && typeof ResizeObserver !== 'undefined') {
|
|
80
|
-
this.
|
|
87
|
+
this.syncTabIndex();
|
|
88
|
+
this.resizeObserver = new ResizeObserver(this.syncTabIndex);
|
|
81
89
|
this.resizeObserver.observe(finalRef);
|
|
82
|
-
this.mutationObserver = new MutationObserver(
|
|
90
|
+
this.mutationObserver = new MutationObserver(this.syncTabIndex);
|
|
83
91
|
this.mutationObserver.observe(finalRef, {
|
|
84
92
|
childList: true,
|
|
85
93
|
subtree: true,
|
|
@@ -90,11 +98,32 @@ let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_
|
|
|
90
98
|
}
|
|
91
99
|
componentDidUpdate() {
|
|
92
100
|
this.props.makeStyles?.();
|
|
101
|
+
// The observers miss some changes, e.g. a text-only edit that makes the
|
|
102
|
+
// body overflow without resizing it. syncTabIndex returns early if
|
|
103
|
+
// nothing changed.
|
|
104
|
+
this.syncTabIndex();
|
|
93
105
|
}
|
|
94
106
|
componentWillUnmount() {
|
|
95
107
|
this.resizeObserver?.disconnect();
|
|
96
108
|
this.mutationObserver?.disconnect();
|
|
97
109
|
}
|
|
110
|
+
|
|
111
|
+
// The body is a tab stop only while it can be scrolled but holds nothing
|
|
112
|
+
// focusable.
|
|
113
|
+
syncTabIndex = () => {
|
|
114
|
+
const finalRef = this.getFinalRef(this.ref);
|
|
115
|
+
const hasScrollbar = !!finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
116
|
+
const needsTabIndex = hasScrollbar && findTabbable(finalRef).length === 0;
|
|
117
|
+
if (needsTabIndex === this.lastNeedsTabIndex) return;
|
|
118
|
+
this.lastNeedsTabIndex = needsTabIndex;
|
|
119
|
+
this.setState({
|
|
120
|
+
needsTabIndex
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// this recursive function is needed because `ref` can be a React component.
|
|
125
|
+
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
126
|
+
// components. See INSTUI-4674
|
|
98
127
|
getFinalRef(el) {
|
|
99
128
|
if (!el) {
|
|
100
129
|
return undefined;
|
|
@@ -119,13 +148,9 @@ let ModalBody = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_
|
|
|
119
148
|
} = this.props;
|
|
120
149
|
const passthroughProps = View.omitViewProps(omitProps(rest, ModalBody.allowedProps), ModalBody);
|
|
121
150
|
const isFit = overflow === 'fit';
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const finalRef = this.getFinalRef(this.ref);
|
|
126
|
-
const hasScrollbar = finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
127
|
-
const hasTabbableChildren = !!finalRef && findTabbable(finalRef).length > 0;
|
|
128
|
-
const needsTabIndex = hasScrollbar && !hasTabbableChildren;
|
|
151
|
+
const {
|
|
152
|
+
needsTabIndex
|
|
153
|
+
} = this.state;
|
|
129
154
|
return _jsx(ModalContext.Consumer, {
|
|
130
155
|
children: value => _jsx(View, {
|
|
131
156
|
...passthroughProps,
|
|
@@ -49,6 +49,13 @@ let ModalBody = (_dec = withStyleNew(generateStyle, 'ModalBody'), _dec(_class =
|
|
|
49
49
|
as: 'div',
|
|
50
50
|
variant: 'default'
|
|
51
51
|
};
|
|
52
|
+
state = {
|
|
53
|
+
isFirefox: false,
|
|
54
|
+
needsTabIndex: false
|
|
55
|
+
};
|
|
56
|
+
// mirrors state.needsTabIndex, but readable synchronously in the observer
|
|
57
|
+
// callbacks below, where a pending setState would make this.state stale
|
|
58
|
+
lastNeedsTabIndex = false;
|
|
52
59
|
ref = null;
|
|
53
60
|
resizeObserver;
|
|
54
61
|
mutationObserver;
|
|
@@ -77,9 +84,10 @@ let ModalBody = (_dec = withStyleNew(generateStyle, 'ModalBody'), _dec(_class =
|
|
|
77
84
|
}
|
|
78
85
|
const finalRef = this.getFinalRef(this.ref);
|
|
79
86
|
if (finalRef && typeof ResizeObserver !== 'undefined') {
|
|
80
|
-
this.
|
|
87
|
+
this.syncTabIndex();
|
|
88
|
+
this.resizeObserver = new ResizeObserver(this.syncTabIndex);
|
|
81
89
|
this.resizeObserver.observe(finalRef);
|
|
82
|
-
this.mutationObserver = new MutationObserver(
|
|
90
|
+
this.mutationObserver = new MutationObserver(this.syncTabIndex);
|
|
83
91
|
this.mutationObserver.observe(finalRef, {
|
|
84
92
|
childList: true,
|
|
85
93
|
subtree: true,
|
|
@@ -90,11 +98,32 @@ let ModalBody = (_dec = withStyleNew(generateStyle, 'ModalBody'), _dec(_class =
|
|
|
90
98
|
}
|
|
91
99
|
componentDidUpdate() {
|
|
92
100
|
this.props.makeStyles?.();
|
|
101
|
+
// The observers miss some changes, e.g. a text-only edit that makes the
|
|
102
|
+
// body overflow without resizing it. syncTabIndex returns early if
|
|
103
|
+
// nothing changed.
|
|
104
|
+
this.syncTabIndex();
|
|
93
105
|
}
|
|
94
106
|
componentWillUnmount() {
|
|
95
107
|
this.resizeObserver?.disconnect();
|
|
96
108
|
this.mutationObserver?.disconnect();
|
|
97
109
|
}
|
|
110
|
+
|
|
111
|
+
// The body is a tab stop only while it can be scrolled but holds nothing
|
|
112
|
+
// focusable.
|
|
113
|
+
syncTabIndex = () => {
|
|
114
|
+
const finalRef = this.getFinalRef(this.ref);
|
|
115
|
+
const hasScrollbar = !!finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
116
|
+
const needsTabIndex = hasScrollbar && findTabbable(finalRef).length === 0;
|
|
117
|
+
if (needsTabIndex === this.lastNeedsTabIndex) return;
|
|
118
|
+
this.lastNeedsTabIndex = needsTabIndex;
|
|
119
|
+
this.setState({
|
|
120
|
+
needsTabIndex
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// this recursive function is needed because `ref` can be a React component.
|
|
125
|
+
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
126
|
+
// components. See INSTUI-4674
|
|
98
127
|
getFinalRef(el) {
|
|
99
128
|
if (!el) {
|
|
100
129
|
return undefined;
|
|
@@ -120,13 +149,9 @@ let ModalBody = (_dec = withStyleNew(generateStyle, 'ModalBody'), _dec(_class =
|
|
|
120
149
|
} = this.props;
|
|
121
150
|
const passthroughProps = View.omitViewProps(omitProps(rest, ModalBody.allowedProps), ModalBody);
|
|
122
151
|
const isFit = overflow === 'fit';
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const finalRef = this.getFinalRef(this.ref);
|
|
127
|
-
const hasScrollbar = finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
128
|
-
const hasTabbableChildren = !!finalRef && findTabbable(finalRef).length > 0;
|
|
129
|
-
const needsTabIndex = hasScrollbar && !hasTabbableChildren;
|
|
152
|
+
const {
|
|
153
|
+
needsTabIndex
|
|
154
|
+
} = this.state;
|
|
130
155
|
return _jsx(ModalContext.Consumer, {
|
|
131
156
|
children: value => _jsx(View, {
|
|
132
157
|
...passthroughProps,
|
|
@@ -55,6 +55,13 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyle)(_styles.defa
|
|
|
55
55
|
as: 'div',
|
|
56
56
|
variant: 'default'
|
|
57
57
|
};
|
|
58
|
+
state = {
|
|
59
|
+
isFirefox: false,
|
|
60
|
+
needsTabIndex: false
|
|
61
|
+
};
|
|
62
|
+
// mirrors state.needsTabIndex, but readable synchronously in the observer
|
|
63
|
+
// callbacks below, where a pending setState would make this.state stale
|
|
64
|
+
lastNeedsTabIndex = false;
|
|
58
65
|
ref = null;
|
|
59
66
|
resizeObserver;
|
|
60
67
|
mutationObserver;
|
|
@@ -83,9 +90,10 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyle)(_styles.defa
|
|
|
83
90
|
}
|
|
84
91
|
const finalRef = this.getFinalRef(this.ref);
|
|
85
92
|
if (finalRef && typeof ResizeObserver !== 'undefined') {
|
|
86
|
-
this.
|
|
93
|
+
this.syncTabIndex();
|
|
94
|
+
this.resizeObserver = new ResizeObserver(this.syncTabIndex);
|
|
87
95
|
this.resizeObserver.observe(finalRef);
|
|
88
|
-
this.mutationObserver = new MutationObserver(
|
|
96
|
+
this.mutationObserver = new MutationObserver(this.syncTabIndex);
|
|
89
97
|
this.mutationObserver.observe(finalRef, {
|
|
90
98
|
childList: true,
|
|
91
99
|
subtree: true,
|
|
@@ -96,11 +104,32 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyle)(_styles.defa
|
|
|
96
104
|
}
|
|
97
105
|
componentDidUpdate() {
|
|
98
106
|
this.props.makeStyles?.();
|
|
107
|
+
// The observers miss some changes, e.g. a text-only edit that makes the
|
|
108
|
+
// body overflow without resizing it. syncTabIndex returns early if
|
|
109
|
+
// nothing changed.
|
|
110
|
+
this.syncTabIndex();
|
|
99
111
|
}
|
|
100
112
|
componentWillUnmount() {
|
|
101
113
|
this.resizeObserver?.disconnect();
|
|
102
114
|
this.mutationObserver?.disconnect();
|
|
103
115
|
}
|
|
116
|
+
|
|
117
|
+
// The body is a tab stop only while it can be scrolled but holds nothing
|
|
118
|
+
// focusable.
|
|
119
|
+
syncTabIndex = () => {
|
|
120
|
+
const finalRef = this.getFinalRef(this.ref);
|
|
121
|
+
const hasScrollbar = !!finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
122
|
+
const needsTabIndex = hasScrollbar && (0, _findTabbable.findTabbable)(finalRef).length === 0;
|
|
123
|
+
if (needsTabIndex === this.lastNeedsTabIndex) return;
|
|
124
|
+
this.lastNeedsTabIndex = needsTabIndex;
|
|
125
|
+
this.setState({
|
|
126
|
+
needsTabIndex
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// this recursive function is needed because `ref` can be a React component.
|
|
131
|
+
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
132
|
+
// components. See INSTUI-4674
|
|
104
133
|
getFinalRef(el) {
|
|
105
134
|
if (!el) {
|
|
106
135
|
return undefined;
|
|
@@ -125,13 +154,9 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyle)(_styles.defa
|
|
|
125
154
|
} = this.props;
|
|
126
155
|
const passthroughProps = _v11_.View.omitViewProps((0, _omitProps.omitProps)(rest, ModalBody.allowedProps), ModalBody);
|
|
127
156
|
const isFit = overflow === 'fit';
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const finalRef = this.getFinalRef(this.ref);
|
|
132
|
-
const hasScrollbar = finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
133
|
-
const hasTabbableChildren = !!finalRef && (0, _findTabbable.findTabbable)(finalRef).length > 0;
|
|
134
|
-
const needsTabIndex = hasScrollbar && !hasTabbableChildren;
|
|
157
|
+
const {
|
|
158
|
+
needsTabIndex
|
|
159
|
+
} = this.state;
|
|
135
160
|
return (0, _jsxRuntime.jsx)(_ModalContext.default.Consumer, {
|
|
136
161
|
children: value => (0, _jsxRuntime.jsx)(_v11_.View, {
|
|
137
162
|
...passthroughProps,
|
|
@@ -55,6 +55,13 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyleNew)(_styles.d
|
|
|
55
55
|
as: 'div',
|
|
56
56
|
variant: 'default'
|
|
57
57
|
};
|
|
58
|
+
state = {
|
|
59
|
+
isFirefox: false,
|
|
60
|
+
needsTabIndex: false
|
|
61
|
+
};
|
|
62
|
+
// mirrors state.needsTabIndex, but readable synchronously in the observer
|
|
63
|
+
// callbacks below, where a pending setState would make this.state stale
|
|
64
|
+
lastNeedsTabIndex = false;
|
|
58
65
|
ref = null;
|
|
59
66
|
resizeObserver;
|
|
60
67
|
mutationObserver;
|
|
@@ -83,9 +90,10 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyleNew)(_styles.d
|
|
|
83
90
|
}
|
|
84
91
|
const finalRef = this.getFinalRef(this.ref);
|
|
85
92
|
if (finalRef && typeof ResizeObserver !== 'undefined') {
|
|
86
|
-
this.
|
|
93
|
+
this.syncTabIndex();
|
|
94
|
+
this.resizeObserver = new ResizeObserver(this.syncTabIndex);
|
|
87
95
|
this.resizeObserver.observe(finalRef);
|
|
88
|
-
this.mutationObserver = new MutationObserver(
|
|
96
|
+
this.mutationObserver = new MutationObserver(this.syncTabIndex);
|
|
89
97
|
this.mutationObserver.observe(finalRef, {
|
|
90
98
|
childList: true,
|
|
91
99
|
subtree: true,
|
|
@@ -96,11 +104,32 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyleNew)(_styles.d
|
|
|
96
104
|
}
|
|
97
105
|
componentDidUpdate() {
|
|
98
106
|
this.props.makeStyles?.();
|
|
107
|
+
// The observers miss some changes, e.g. a text-only edit that makes the
|
|
108
|
+
// body overflow without resizing it. syncTabIndex returns early if
|
|
109
|
+
// nothing changed.
|
|
110
|
+
this.syncTabIndex();
|
|
99
111
|
}
|
|
100
112
|
componentWillUnmount() {
|
|
101
113
|
this.resizeObserver?.disconnect();
|
|
102
114
|
this.mutationObserver?.disconnect();
|
|
103
115
|
}
|
|
116
|
+
|
|
117
|
+
// The body is a tab stop only while it can be scrolled but holds nothing
|
|
118
|
+
// focusable.
|
|
119
|
+
syncTabIndex = () => {
|
|
120
|
+
const finalRef = this.getFinalRef(this.ref);
|
|
121
|
+
const hasScrollbar = !!finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
122
|
+
const needsTabIndex = hasScrollbar && (0, _findTabbable.findTabbable)(finalRef).length === 0;
|
|
123
|
+
if (needsTabIndex === this.lastNeedsTabIndex) return;
|
|
124
|
+
this.lastNeedsTabIndex = needsTabIndex;
|
|
125
|
+
this.setState({
|
|
126
|
+
needsTabIndex
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// this recursive function is needed because `ref` can be a React component.
|
|
131
|
+
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
132
|
+
// components. See INSTUI-4674
|
|
104
133
|
getFinalRef(el) {
|
|
105
134
|
if (!el) {
|
|
106
135
|
return undefined;
|
|
@@ -126,13 +155,9 @@ let ModalBody = exports.ModalBody = (_dec = (0, _emotion.withStyleNew)(_styles.d
|
|
|
126
155
|
} = this.props;
|
|
127
156
|
const passthroughProps = _latest.View.omitViewProps((0, _omitProps.omitProps)(rest, ModalBody.allowedProps), ModalBody);
|
|
128
157
|
const isFit = overflow === 'fit';
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const finalRef = this.getFinalRef(this.ref);
|
|
133
|
-
const hasScrollbar = finalRef && Math.abs((finalRef.scrollHeight ?? 0) - (finalRef.getBoundingClientRect()?.height ?? 0)) > 1;
|
|
134
|
-
const hasTabbableChildren = !!finalRef && (0, _findTabbable.findTabbable)(finalRef).length > 0;
|
|
135
|
-
const needsTabIndex = hasScrollbar && !hasTabbableChildren;
|
|
158
|
+
const {
|
|
159
|
+
needsTabIndex
|
|
160
|
+
} = this.state;
|
|
136
161
|
return (0, _jsxRuntime.jsx)(_ModalContext.default.Consumer, {
|
|
137
162
|
children: value => (0, _jsxRuntime.jsx)(_latest.View, {
|
|
138
163
|
...passthroughProps,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/ui-modal",
|
|
3
|
-
"version": "11.7.
|
|
3
|
+
"version": "11.7.7-snapshot-0",
|
|
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,26 +15,26 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/runtime": "^7.29.7",
|
|
18
|
-
"@instructure/
|
|
19
|
-
"@instructure/
|
|
20
|
-
"@instructure/
|
|
21
|
-
"@instructure/ui-dialog": "11.7.
|
|
22
|
-
"@instructure/
|
|
23
|
-
"@instructure/ui-dom-utils": "11.7.
|
|
24
|
-
"@instructure/ui-
|
|
25
|
-
"@instructure/ui-
|
|
26
|
-
"@instructure/ui-portal": "11.7.
|
|
27
|
-
"@instructure/ui-react-utils": "11.7.
|
|
28
|
-
"@instructure/ui-themes": "11.7.
|
|
29
|
-
"@instructure/ui-view": "11.7.
|
|
30
|
-
"@instructure/ui-utils": "11.7.
|
|
18
|
+
"@instructure/emotion": "11.7.7-snapshot-0",
|
|
19
|
+
"@instructure/console": "11.7.7-snapshot-0",
|
|
20
|
+
"@instructure/shared-types": "11.7.7-snapshot-0",
|
|
21
|
+
"@instructure/ui-dialog": "11.7.7-snapshot-0",
|
|
22
|
+
"@instructure/ui-buttons": "11.7.7-snapshot-0",
|
|
23
|
+
"@instructure/ui-dom-utils": "11.7.7-snapshot-0",
|
|
24
|
+
"@instructure/ui-overlays": "11.7.7-snapshot-0",
|
|
25
|
+
"@instructure/ui-motion": "11.7.7-snapshot-0",
|
|
26
|
+
"@instructure/ui-portal": "11.7.7-snapshot-0",
|
|
27
|
+
"@instructure/ui-react-utils": "11.7.7-snapshot-0",
|
|
28
|
+
"@instructure/ui-themes": "11.7.7-snapshot-0",
|
|
29
|
+
"@instructure/ui-view": "11.7.7-snapshot-0",
|
|
30
|
+
"@instructure/ui-utils": "11.7.7-snapshot-0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"vitest": "^5.0.0",
|
|
34
|
-
"@instructure/ui-
|
|
35
|
-
"@instructure/ui-
|
|
36
|
-
"@instructure/ui-
|
|
37
|
-
"@instructure/ui-tooltip": "11.7.
|
|
34
|
+
"@instructure/ui-babel-preset": "11.7.7-snapshot-0",
|
|
35
|
+
"@instructure/ui-color-utils": "11.7.7-snapshot-0",
|
|
36
|
+
"@instructure/ui-position": "11.7.7-snapshot-0",
|
|
37
|
+
"@instructure/ui-tooltip": "11.7.7-snapshot-0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"react": ">=18 <=19"
|
|
@@ -33,7 +33,7 @@ import generateStyle from './styles.js'
|
|
|
33
33
|
import generateComponentTheme from './theme.js'
|
|
34
34
|
|
|
35
35
|
import { allowedProps } from './props.js'
|
|
36
|
-
import type { ModalBodyProps } from './props'
|
|
36
|
+
import type { ModalBodyProps, ModalBodyState } from './props'
|
|
37
37
|
import { UIElement } from '@instructure/shared-types'
|
|
38
38
|
import ModalContext from '../ModalContext.js'
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@ id: Modal.Body
|
|
|
44
44
|
---
|
|
45
45
|
**/
|
|
46
46
|
@withStyle(generateStyle, generateComponentTheme)
|
|
47
|
-
class ModalBody extends Component<ModalBodyProps> {
|
|
47
|
+
class ModalBody extends Component<ModalBodyProps, ModalBodyState> {
|
|
48
48
|
static displayName = 'ModalBody'
|
|
49
49
|
static readonly componentId = 'Modal.Body'
|
|
50
50
|
|
|
@@ -55,6 +55,11 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
55
55
|
variant: 'default'
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
state: ModalBodyState = { isFirefox: false, needsTabIndex: false }
|
|
59
|
+
// mirrors state.needsTabIndex, but readable synchronously in the observer
|
|
60
|
+
// callbacks below, where a pending setState would make this.state stale
|
|
61
|
+
private lastNeedsTabIndex = false
|
|
62
|
+
|
|
58
63
|
ref: UIElement | null = null
|
|
59
64
|
private resizeObserver?: ResizeObserver
|
|
60
65
|
private mutationObserver?: MutationObserver
|
|
@@ -92,9 +97,10 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
92
97
|
|
|
93
98
|
const finalRef = this.getFinalRef(this.ref)
|
|
94
99
|
if (finalRef && typeof ResizeObserver !== 'undefined') {
|
|
95
|
-
this.
|
|
100
|
+
this.syncTabIndex()
|
|
101
|
+
this.resizeObserver = new ResizeObserver(this.syncTabIndex)
|
|
96
102
|
this.resizeObserver.observe(finalRef)
|
|
97
|
-
this.mutationObserver = new MutationObserver(
|
|
103
|
+
this.mutationObserver = new MutationObserver(this.syncTabIndex)
|
|
98
104
|
this.mutationObserver.observe(finalRef, {
|
|
99
105
|
childList: true,
|
|
100
106
|
subtree: true,
|
|
@@ -115,6 +121,10 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
115
121
|
|
|
116
122
|
componentDidUpdate() {
|
|
117
123
|
this.props.makeStyles?.()
|
|
124
|
+
// The observers miss some changes, e.g. a text-only edit that makes the
|
|
125
|
+
// body overflow without resizing it. syncTabIndex returns early if
|
|
126
|
+
// nothing changed.
|
|
127
|
+
this.syncTabIndex()
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
componentWillUnmount() {
|
|
@@ -122,6 +132,26 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
122
132
|
this.mutationObserver?.disconnect()
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
// The body is a tab stop only while it can be scrolled but holds nothing
|
|
136
|
+
// focusable.
|
|
137
|
+
syncTabIndex = () => {
|
|
138
|
+
const finalRef = this.getFinalRef(this.ref)
|
|
139
|
+
const hasScrollbar =
|
|
140
|
+
!!finalRef &&
|
|
141
|
+
Math.abs(
|
|
142
|
+
(finalRef.scrollHeight ?? 0) -
|
|
143
|
+
(finalRef.getBoundingClientRect()?.height ?? 0)
|
|
144
|
+
) > 1
|
|
145
|
+
const needsTabIndex = hasScrollbar && findTabbable(finalRef).length === 0
|
|
146
|
+
|
|
147
|
+
if (needsTabIndex === this.lastNeedsTabIndex) return
|
|
148
|
+
this.lastNeedsTabIndex = needsTabIndex
|
|
149
|
+
this.setState({ needsTabIndex })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// this recursive function is needed because `ref` can be a React component.
|
|
153
|
+
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
154
|
+
// components. See INSTUI-4674
|
|
125
155
|
getFinalRef(el: UIElement): Element | undefined {
|
|
126
156
|
if (!el) {
|
|
127
157
|
return undefined
|
|
@@ -144,18 +174,7 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
144
174
|
ModalBody
|
|
145
175
|
)
|
|
146
176
|
const isFit = overflow === 'fit'
|
|
147
|
-
|
|
148
|
-
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
149
|
-
// components. See INSTUI-4674
|
|
150
|
-
const finalRef = this.getFinalRef(this.ref)
|
|
151
|
-
const hasScrollbar =
|
|
152
|
-
finalRef &&
|
|
153
|
-
Math.abs(
|
|
154
|
-
(finalRef.scrollHeight ?? 0) -
|
|
155
|
-
(finalRef.getBoundingClientRect()?.height ?? 0)
|
|
156
|
-
) > 1
|
|
157
|
-
const hasTabbableChildren = !!finalRef && findTabbable(finalRef).length > 0
|
|
158
|
-
const needsTabIndex = hasScrollbar && !hasTabbableChildren
|
|
177
|
+
const { needsTabIndex } = this.state
|
|
159
178
|
return (
|
|
160
179
|
<ModalContext.Consumer>
|
|
161
180
|
{(value) => (
|
|
@@ -32,7 +32,7 @@ import { withStyleNew } from '@instructure/emotion'
|
|
|
32
32
|
import generateStyle from './styles.js'
|
|
33
33
|
|
|
34
34
|
import { allowedProps } from './props.js'
|
|
35
|
-
import type { ModalBodyProps } from './props'
|
|
35
|
+
import type { ModalBodyProps, ModalBodyState } from './props'
|
|
36
36
|
import { UIElement } from '@instructure/shared-types'
|
|
37
37
|
import ModalContext from '../ModalContext.js'
|
|
38
38
|
|
|
@@ -43,7 +43,7 @@ id: Modal.Body
|
|
|
43
43
|
---
|
|
44
44
|
**/
|
|
45
45
|
@withStyleNew(generateStyle, 'ModalBody')
|
|
46
|
-
class ModalBody extends Component<ModalBodyProps> {
|
|
46
|
+
class ModalBody extends Component<ModalBodyProps, ModalBodyState> {
|
|
47
47
|
static displayName = 'ModalBody'
|
|
48
48
|
static readonly componentId = 'Modal.Body'
|
|
49
49
|
static readonly themeId = 'ModalBody'
|
|
@@ -55,6 +55,11 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
55
55
|
variant: 'default'
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
state: ModalBodyState = { isFirefox: false, needsTabIndex: false }
|
|
59
|
+
// mirrors state.needsTabIndex, but readable synchronously in the observer
|
|
60
|
+
// callbacks below, where a pending setState would make this.state stale
|
|
61
|
+
private lastNeedsTabIndex = false
|
|
62
|
+
|
|
58
63
|
ref: UIElement | null = null
|
|
59
64
|
private resizeObserver?: ResizeObserver
|
|
60
65
|
private mutationObserver?: MutationObserver
|
|
@@ -92,9 +97,10 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
92
97
|
|
|
93
98
|
const finalRef = this.getFinalRef(this.ref)
|
|
94
99
|
if (finalRef && typeof ResizeObserver !== 'undefined') {
|
|
95
|
-
this.
|
|
100
|
+
this.syncTabIndex()
|
|
101
|
+
this.resizeObserver = new ResizeObserver(this.syncTabIndex)
|
|
96
102
|
this.resizeObserver.observe(finalRef)
|
|
97
|
-
this.mutationObserver = new MutationObserver(
|
|
103
|
+
this.mutationObserver = new MutationObserver(this.syncTabIndex)
|
|
98
104
|
this.mutationObserver.observe(finalRef, {
|
|
99
105
|
childList: true,
|
|
100
106
|
subtree: true,
|
|
@@ -115,6 +121,10 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
115
121
|
|
|
116
122
|
componentDidUpdate() {
|
|
117
123
|
this.props.makeStyles?.()
|
|
124
|
+
// The observers miss some changes, e.g. a text-only edit that makes the
|
|
125
|
+
// body overflow without resizing it. syncTabIndex returns early if
|
|
126
|
+
// nothing changed.
|
|
127
|
+
this.syncTabIndex()
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
componentWillUnmount() {
|
|
@@ -122,6 +132,26 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
122
132
|
this.mutationObserver?.disconnect()
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
// The body is a tab stop only while it can be scrolled but holds nothing
|
|
136
|
+
// focusable.
|
|
137
|
+
syncTabIndex = () => {
|
|
138
|
+
const finalRef = this.getFinalRef(this.ref)
|
|
139
|
+
const hasScrollbar =
|
|
140
|
+
!!finalRef &&
|
|
141
|
+
Math.abs(
|
|
142
|
+
(finalRef.scrollHeight ?? 0) -
|
|
143
|
+
(finalRef.getBoundingClientRect()?.height ?? 0)
|
|
144
|
+
) > 1
|
|
145
|
+
const needsTabIndex = hasScrollbar && findTabbable(finalRef).length === 0
|
|
146
|
+
|
|
147
|
+
if (needsTabIndex === this.lastNeedsTabIndex) return
|
|
148
|
+
this.lastNeedsTabIndex = needsTabIndex
|
|
149
|
+
this.setState({ needsTabIndex })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// this recursive function is needed because `ref` can be a React component.
|
|
153
|
+
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
154
|
+
// components. See INSTUI-4674
|
|
125
155
|
getFinalRef(el: UIElement): Element | undefined {
|
|
126
156
|
if (!el) {
|
|
127
157
|
return undefined
|
|
@@ -152,18 +182,7 @@ class ModalBody extends Component<ModalBodyProps> {
|
|
|
152
182
|
ModalBody
|
|
153
183
|
)
|
|
154
184
|
const isFit = overflow === 'fit'
|
|
155
|
-
|
|
156
|
-
// TODO rethink, the 'as' prop, likely its not a good idea to allow React
|
|
157
|
-
// components. See INSTUI-4674
|
|
158
|
-
const finalRef = this.getFinalRef(this.ref)
|
|
159
|
-
const hasScrollbar =
|
|
160
|
-
finalRef &&
|
|
161
|
-
Math.abs(
|
|
162
|
-
(finalRef.scrollHeight ?? 0) -
|
|
163
|
-
(finalRef.getBoundingClientRect()?.height ?? 0)
|
|
164
|
-
) > 1
|
|
165
|
-
const hasTabbableChildren = !!finalRef && findTabbable(finalRef).length > 0
|
|
166
|
-
const needsTabIndex = hasScrollbar && !hasTabbableChildren
|
|
185
|
+
const { needsTabIndex } = this.state
|
|
167
186
|
return (
|
|
168
187
|
<ModalContext.Consumer>
|
|
169
188
|
{(value) => (
|