@instructure/ui-color-picker 10.26.2 → 10.26.3
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 +11 -0
- package/es/ColorPicker/index.js +76 -5
- package/es/ColorPicker/styles.js +10 -3
- package/lib/ColorPicker/index.js +76 -5
- package/lib/ColorPicker/styles.js +10 -3
- package/package.json +24 -24
- package/src/ColorPicker/index.tsx +94 -7
- package/src/ColorPicker/props.ts +2 -0
- package/src/ColorPicker/styles.ts +9 -3
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/ColorPicker/index.d.ts +5 -0
- package/types/ColorPicker/index.d.ts.map +1 -1
- package/types/ColorPicker/props.d.ts +2 -0
- package/types/ColorPicker/props.d.ts.map +1 -1
- package/types/ColorPicker/styles.d.ts.map +1 -1
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
|
+
## [10.26.3](https://github.com/instructure/instructure-ui/compare/v10.26.2...v10.26.3) (2025-11-19)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ui-color-picker:** fix popover scrolling and button alignment issues ([b7fbf0b](https://github.com/instructure/instructure-ui/commit/b7fbf0b807ddf480ab0f5fab35bf9a38666f8dd0))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [10.26.2](https://github.com/instructure/instructure-ui/compare/v10.26.1...v10.26.2) (2025-10-13)
|
|
7
18
|
|
|
8
19
|
**Note:** Version bump only for package @instructure/ui-color-picker
|
package/es/ColorPicker/index.js
CHANGED
|
@@ -64,7 +64,16 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
64
64
|
this.inputContainerRef = null;
|
|
65
65
|
this.handleInputContainerRef = el => {
|
|
66
66
|
this.inputContainerRef = el;
|
|
67
|
-
|
|
67
|
+
if (el) {
|
|
68
|
+
// Defer measurement until after layout is complete and CSS-in-JS styles are applied
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
this.setLabelHeight();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
this.popoverContentRef = null;
|
|
75
|
+
this.handlePopoverContentRef = el => {
|
|
76
|
+
this.popoverContentRef = el;
|
|
68
77
|
};
|
|
69
78
|
this.setLabelHeight = () => {
|
|
70
79
|
if (this.inputContainerRef) {
|
|
@@ -74,6 +83,56 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
74
83
|
});
|
|
75
84
|
}
|
|
76
85
|
};
|
|
86
|
+
// Calculate the maximum height the popover can have without extending beyond
|
|
87
|
+
// the viewport. This enables scrolling when the ColorPicker's content (all
|
|
88
|
+
// color mixing controls, presets, and contrast checker) would otherwise exceed
|
|
89
|
+
// the available viewport space. Without this calculation, the popover would
|
|
90
|
+
// render off-screen on smaller viewports.
|
|
91
|
+
this.handlePopoverPositioned = position => {
|
|
92
|
+
if (this.popoverContentRef) {
|
|
93
|
+
// Double requestAnimationFrame ensures measurements happen after all child components
|
|
94
|
+
// (ColorMixer, ColorPreset, ColorContrast) complete their mount lifecycle and Emotion
|
|
95
|
+
// finishes injecting CSS-in-JS styles. A single rAF was insufficient as styles are
|
|
96
|
+
// injected dynamically in componentDidMount(). This timing issue only manifested when
|
|
97
|
+
// StrictMode was disabled, since StrictMode's double-rendering provided an accidental
|
|
98
|
+
// second measurement pass.
|
|
99
|
+
requestAnimationFrame(() => {
|
|
100
|
+
// First frame: DOM structure is laid out
|
|
101
|
+
requestAnimationFrame(() => {
|
|
102
|
+
// Second frame: styles injected, child components mounted, dimensions stable
|
|
103
|
+
if (!this.popoverContentRef) return;
|
|
104
|
+
const rect = this.popoverContentRef.getBoundingClientRect();
|
|
105
|
+
const viewportHeight = window.innerHeight;
|
|
106
|
+
|
|
107
|
+
// Detect if popover is positioned above (top) or below (bottom) the trigger.
|
|
108
|
+
// The Position component provides placement strings like "top center" or "bottom center".
|
|
109
|
+
const placement = (position === null || position === void 0 ? void 0 : position.placement) || '';
|
|
110
|
+
const isPositionedAbove = placement.startsWith('top');
|
|
111
|
+
let availableHeight;
|
|
112
|
+
if (isPositionedAbove) {
|
|
113
|
+
// When opening upward: available space is from viewport top to popover bottom.
|
|
114
|
+
// This is the space where the popover can expand within the viewport.
|
|
115
|
+
availableHeight = rect.top + rect.height - 16;
|
|
116
|
+
} else {
|
|
117
|
+
// When opening downward: available space is from popover top to viewport bottom.
|
|
118
|
+
// Subtract a small buffer (16px) for padding/margin.
|
|
119
|
+
availableHeight = viewportHeight - rect.top - 16;
|
|
120
|
+
}
|
|
121
|
+
const propMaxHeight = this.props.popoverMaxHeight;
|
|
122
|
+
let calculatedMaxHeight = `${Math.max(100, availableHeight)}px`;
|
|
123
|
+
|
|
124
|
+
// If prop specifies a maxHeight, respect it as an additional constraint
|
|
125
|
+
if (propMaxHeight && propMaxHeight !== '100vh') {
|
|
126
|
+
calculatedMaxHeight = propMaxHeight;
|
|
127
|
+
}
|
|
128
|
+
this.setState({
|
|
129
|
+
calculatedPopoverMaxHeight: calculatedMaxHeight,
|
|
130
|
+
isHeightCalculated: true
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
77
136
|
this.checkSettings = () => {
|
|
78
137
|
if (this.props.children && this.props.colorMixerSettings) {
|
|
79
138
|
warn(false, 'You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.', '');
|
|
@@ -96,7 +155,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
96
155
|
onShowContent: () => {
|
|
97
156
|
this.setState({
|
|
98
157
|
openColorPicker: true,
|
|
99
|
-
mixedColor: this.state.hexCode
|
|
158
|
+
mixedColor: this.state.hexCode,
|
|
159
|
+
calculatedPopoverMaxHeight: void 0,
|
|
160
|
+
isHeightCalculated: false
|
|
100
161
|
});
|
|
101
162
|
},
|
|
102
163
|
onHideContent: () => {
|
|
@@ -110,8 +171,11 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
110
171
|
shouldReturnFocus: true,
|
|
111
172
|
shouldCloseOnDocumentClick: true,
|
|
112
173
|
offsetY: "10rem",
|
|
174
|
+
onPositioned: this.handlePopoverPositioned,
|
|
175
|
+
onPositionChanged: this.handlePopoverPositioned,
|
|
113
176
|
children: _jsx("div", {
|
|
114
177
|
css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.popoverContentContainer,
|
|
178
|
+
ref: this.handlePopoverContentRef,
|
|
115
179
|
children: this.isDefaultPopover ? this.renderDefaultPopoverContent() : this.renderCustomPopoverContent()
|
|
116
180
|
})
|
|
117
181
|
});
|
|
@@ -133,7 +197,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
133
197
|
onChange === null || onChange === void 0 ? void 0 : onChange(`#${this.mixedColorWithStrippedAlpha}`);
|
|
134
198
|
}, () => this.setState({
|
|
135
199
|
openColorPicker: false,
|
|
136
|
-
mixedColor: this.state.hexCode
|
|
200
|
+
mixedColor: this.state.hexCode,
|
|
201
|
+
calculatedPopoverMaxHeight: void 0,
|
|
202
|
+
isHeightCalculated: false
|
|
137
203
|
}))
|
|
138
204
|
});
|
|
139
205
|
};
|
|
@@ -186,7 +252,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
186
252
|
children: [_jsx(Button, {
|
|
187
253
|
onClick: () => this.setState({
|
|
188
254
|
openColorPicker: false,
|
|
189
|
-
mixedColor: this.state.hexCode
|
|
255
|
+
mixedColor: this.state.hexCode,
|
|
256
|
+
calculatedPopoverMaxHeight: void 0,
|
|
257
|
+
isHeightCalculated: false
|
|
190
258
|
}),
|
|
191
259
|
color: "secondary",
|
|
192
260
|
margin: "xx-small",
|
|
@@ -217,7 +285,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
217
285
|
showHelperErrorMessages: false,
|
|
218
286
|
openColorPicker: false,
|
|
219
287
|
mixedColor: '',
|
|
220
|
-
labelHeight: 0
|
|
288
|
+
labelHeight: 0,
|
|
289
|
+
calculatedPopoverMaxHeight: void 0,
|
|
290
|
+
isHeightCalculated: false
|
|
221
291
|
};
|
|
222
292
|
}
|
|
223
293
|
componentDidMount() {
|
|
@@ -467,6 +537,7 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
467
537
|
}), !this.isSimple && _jsx("div", {
|
|
468
538
|
css: (_this$props$styles7 = this.props.styles) === null || _this$props$styles7 === void 0 ? void 0 : _this$props$styles7.colorMixerButtonContainer,
|
|
469
539
|
style: {
|
|
540
|
+
alignSelf: this.state.labelHeight > 0 ? 'flex-start' : 'flex-end',
|
|
470
541
|
paddingTop: this.state.labelHeight
|
|
471
542
|
},
|
|
472
543
|
children: _jsx("div", {
|
package/es/ColorPicker/styles.js
CHANGED
|
@@ -42,7 +42,8 @@ const generateStyle = (componentTheme, props, state) => {
|
|
|
42
42
|
const checkContrast = props.checkContrast,
|
|
43
43
|
popoverMaxHeight = props.popoverMaxHeight,
|
|
44
44
|
margin = props.margin;
|
|
45
|
-
const isSimple = state.isSimple
|
|
45
|
+
const isSimple = state.isSimple,
|
|
46
|
+
calculatedPopoverMaxHeight = state.calculatedPopoverMaxHeight;
|
|
46
47
|
const cssMargin = mapSpacingToShorthand(margin, spacing);
|
|
47
48
|
return {
|
|
48
49
|
colorPicker: {
|
|
@@ -112,8 +113,14 @@ const generateStyle = (componentTheme, props, state) => {
|
|
|
112
113
|
},
|
|
113
114
|
popoverContentContainer: {
|
|
114
115
|
label: 'colorPicker__popoverContentContainer',
|
|
115
|
-
maxHeight: popoverMaxHeight || '100vh',
|
|
116
|
-
|
|
116
|
+
maxHeight: calculatedPopoverMaxHeight || popoverMaxHeight || '100vh',
|
|
117
|
+
overflowY: 'auto',
|
|
118
|
+
overflowX: 'hidden',
|
|
119
|
+
scrollbarGutter: 'stable',
|
|
120
|
+
display: 'flex',
|
|
121
|
+
flexDirection: 'column',
|
|
122
|
+
opacity: state.isHeightCalculated ? 1 : 0,
|
|
123
|
+
transition: 'opacity 150ms ease-in'
|
|
117
124
|
},
|
|
118
125
|
colorMixerButtonWrapper: {
|
|
119
126
|
label: 'colorPicker__colorMixerButtonWrapper',
|
package/lib/ColorPicker/index.js
CHANGED
|
@@ -75,7 +75,16 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
75
75
|
this.inputContainerRef = null;
|
|
76
76
|
this.handleInputContainerRef = el => {
|
|
77
77
|
this.inputContainerRef = el;
|
|
78
|
-
|
|
78
|
+
if (el) {
|
|
79
|
+
// Defer measurement until after layout is complete and CSS-in-JS styles are applied
|
|
80
|
+
requestAnimationFrame(() => {
|
|
81
|
+
this.setLabelHeight();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
this.popoverContentRef = null;
|
|
86
|
+
this.handlePopoverContentRef = el => {
|
|
87
|
+
this.popoverContentRef = el;
|
|
79
88
|
};
|
|
80
89
|
this.setLabelHeight = () => {
|
|
81
90
|
if (this.inputContainerRef) {
|
|
@@ -85,6 +94,56 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
85
94
|
});
|
|
86
95
|
}
|
|
87
96
|
};
|
|
97
|
+
// Calculate the maximum height the popover can have without extending beyond
|
|
98
|
+
// the viewport. This enables scrolling when the ColorPicker's content (all
|
|
99
|
+
// color mixing controls, presets, and contrast checker) would otherwise exceed
|
|
100
|
+
// the available viewport space. Without this calculation, the popover would
|
|
101
|
+
// render off-screen on smaller viewports.
|
|
102
|
+
this.handlePopoverPositioned = position => {
|
|
103
|
+
if (this.popoverContentRef) {
|
|
104
|
+
// Double requestAnimationFrame ensures measurements happen after all child components
|
|
105
|
+
// (ColorMixer, ColorPreset, ColorContrast) complete their mount lifecycle and Emotion
|
|
106
|
+
// finishes injecting CSS-in-JS styles. A single rAF was insufficient as styles are
|
|
107
|
+
// injected dynamically in componentDidMount(). This timing issue only manifested when
|
|
108
|
+
// StrictMode was disabled, since StrictMode's double-rendering provided an accidental
|
|
109
|
+
// second measurement pass.
|
|
110
|
+
requestAnimationFrame(() => {
|
|
111
|
+
// First frame: DOM structure is laid out
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
// Second frame: styles injected, child components mounted, dimensions stable
|
|
114
|
+
if (!this.popoverContentRef) return;
|
|
115
|
+
const rect = this.popoverContentRef.getBoundingClientRect();
|
|
116
|
+
const viewportHeight = window.innerHeight;
|
|
117
|
+
|
|
118
|
+
// Detect if popover is positioned above (top) or below (bottom) the trigger.
|
|
119
|
+
// The Position component provides placement strings like "top center" or "bottom center".
|
|
120
|
+
const placement = (position === null || position === void 0 ? void 0 : position.placement) || '';
|
|
121
|
+
const isPositionedAbove = placement.startsWith('top');
|
|
122
|
+
let availableHeight;
|
|
123
|
+
if (isPositionedAbove) {
|
|
124
|
+
// When opening upward: available space is from viewport top to popover bottom.
|
|
125
|
+
// This is the space where the popover can expand within the viewport.
|
|
126
|
+
availableHeight = rect.top + rect.height - 16;
|
|
127
|
+
} else {
|
|
128
|
+
// When opening downward: available space is from popover top to viewport bottom.
|
|
129
|
+
// Subtract a small buffer (16px) for padding/margin.
|
|
130
|
+
availableHeight = viewportHeight - rect.top - 16;
|
|
131
|
+
}
|
|
132
|
+
const propMaxHeight = this.props.popoverMaxHeight;
|
|
133
|
+
let calculatedMaxHeight = `${Math.max(100, availableHeight)}px`;
|
|
134
|
+
|
|
135
|
+
// If prop specifies a maxHeight, respect it as an additional constraint
|
|
136
|
+
if (propMaxHeight && propMaxHeight !== '100vh') {
|
|
137
|
+
calculatedMaxHeight = propMaxHeight;
|
|
138
|
+
}
|
|
139
|
+
this.setState({
|
|
140
|
+
calculatedPopoverMaxHeight: calculatedMaxHeight,
|
|
141
|
+
isHeightCalculated: true
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
};
|
|
88
147
|
this.checkSettings = () => {
|
|
89
148
|
if (this.props.children && this.props.colorMixerSettings) {
|
|
90
149
|
(0, _console.warn)(false, 'You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.', '');
|
|
@@ -107,7 +166,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
107
166
|
onShowContent: () => {
|
|
108
167
|
this.setState({
|
|
109
168
|
openColorPicker: true,
|
|
110
|
-
mixedColor: this.state.hexCode
|
|
169
|
+
mixedColor: this.state.hexCode,
|
|
170
|
+
calculatedPopoverMaxHeight: void 0,
|
|
171
|
+
isHeightCalculated: false
|
|
111
172
|
});
|
|
112
173
|
},
|
|
113
174
|
onHideContent: () => {
|
|
@@ -121,8 +182,11 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
121
182
|
shouldReturnFocus: true,
|
|
122
183
|
shouldCloseOnDocumentClick: true,
|
|
123
184
|
offsetY: "10rem",
|
|
185
|
+
onPositioned: this.handlePopoverPositioned,
|
|
186
|
+
onPositionChanged: this.handlePopoverPositioned,
|
|
124
187
|
children: (0, _jsxRuntime.jsx)("div", {
|
|
125
188
|
css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.popoverContentContainer,
|
|
189
|
+
ref: this.handlePopoverContentRef,
|
|
126
190
|
children: this.isDefaultPopover ? this.renderDefaultPopoverContent() : this.renderCustomPopoverContent()
|
|
127
191
|
})
|
|
128
192
|
});
|
|
@@ -144,7 +208,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
144
208
|
onChange === null || onChange === void 0 ? void 0 : onChange(`#${this.mixedColorWithStrippedAlpha}`);
|
|
145
209
|
}, () => this.setState({
|
|
146
210
|
openColorPicker: false,
|
|
147
|
-
mixedColor: this.state.hexCode
|
|
211
|
+
mixedColor: this.state.hexCode,
|
|
212
|
+
calculatedPopoverMaxHeight: void 0,
|
|
213
|
+
isHeightCalculated: false
|
|
148
214
|
}))
|
|
149
215
|
});
|
|
150
216
|
};
|
|
@@ -197,7 +263,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
197
263
|
children: [(0, _jsxRuntime.jsx)(_Button.Button, {
|
|
198
264
|
onClick: () => this.setState({
|
|
199
265
|
openColorPicker: false,
|
|
200
|
-
mixedColor: this.state.hexCode
|
|
266
|
+
mixedColor: this.state.hexCode,
|
|
267
|
+
calculatedPopoverMaxHeight: void 0,
|
|
268
|
+
isHeightCalculated: false
|
|
201
269
|
}),
|
|
202
270
|
color: "secondary",
|
|
203
271
|
margin: "xx-small",
|
|
@@ -228,7 +296,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
228
296
|
showHelperErrorMessages: false,
|
|
229
297
|
openColorPicker: false,
|
|
230
298
|
mixedColor: '',
|
|
231
|
-
labelHeight: 0
|
|
299
|
+
labelHeight: 0,
|
|
300
|
+
calculatedPopoverMaxHeight: void 0,
|
|
301
|
+
isHeightCalculated: false
|
|
232
302
|
};
|
|
233
303
|
}
|
|
234
304
|
componentDidMount() {
|
|
@@ -478,6 +548,7 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
478
548
|
}), !this.isSimple && (0, _jsxRuntime.jsx)("div", {
|
|
479
549
|
css: (_this$props$styles7 = this.props.styles) === null || _this$props$styles7 === void 0 ? void 0 : _this$props$styles7.colorMixerButtonContainer,
|
|
480
550
|
style: {
|
|
551
|
+
alignSelf: this.state.labelHeight > 0 ? 'flex-start' : 'flex-end',
|
|
481
552
|
paddingTop: this.state.labelHeight
|
|
482
553
|
},
|
|
483
554
|
children: (0, _jsxRuntime.jsx)("div", {
|
|
@@ -48,7 +48,8 @@ const generateStyle = (componentTheme, props, state) => {
|
|
|
48
48
|
const checkContrast = props.checkContrast,
|
|
49
49
|
popoverMaxHeight = props.popoverMaxHeight,
|
|
50
50
|
margin = props.margin;
|
|
51
|
-
const isSimple = state.isSimple
|
|
51
|
+
const isSimple = state.isSimple,
|
|
52
|
+
calculatedPopoverMaxHeight = state.calculatedPopoverMaxHeight;
|
|
52
53
|
const cssMargin = (0, _emotion.mapSpacingToShorthand)(margin, spacing);
|
|
53
54
|
return {
|
|
54
55
|
colorPicker: {
|
|
@@ -118,8 +119,14 @@ const generateStyle = (componentTheme, props, state) => {
|
|
|
118
119
|
},
|
|
119
120
|
popoverContentContainer: {
|
|
120
121
|
label: 'colorPicker__popoverContentContainer',
|
|
121
|
-
maxHeight: popoverMaxHeight || '100vh',
|
|
122
|
-
|
|
122
|
+
maxHeight: calculatedPopoverMaxHeight || popoverMaxHeight || '100vh',
|
|
123
|
+
overflowY: 'auto',
|
|
124
|
+
overflowX: 'hidden',
|
|
125
|
+
scrollbarGutter: 'stable',
|
|
126
|
+
display: 'flex',
|
|
127
|
+
flexDirection: 'column',
|
|
128
|
+
opacity: state.isHeightCalculated ? 1 : 0,
|
|
129
|
+
transition: 'opacity 150ms ease-in'
|
|
123
130
|
},
|
|
124
131
|
colorMixerButtonWrapper: {
|
|
125
132
|
label: 'colorPicker__colorMixerButtonWrapper',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/ui-color-picker",
|
|
3
|
-
"version": "10.26.
|
|
3
|
+
"version": "10.26.3",
|
|
4
4
|
"description": "A UI component library made by Instructure Inc.",
|
|
5
5
|
"author": "Instructure, Inc. Engineering and Product Design",
|
|
6
6
|
"module": "./es/index.js",
|
|
@@ -24,32 +24,32 @@
|
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@babel/runtime": "^7.27.6",
|
|
27
|
-
"@instructure/console": "10.26.
|
|
28
|
-
"@instructure/emotion": "10.26.
|
|
29
|
-
"@instructure/shared-types": "10.26.
|
|
30
|
-
"@instructure/ui-a11y-content": "10.26.
|
|
31
|
-
"@instructure/ui-buttons": "10.26.
|
|
32
|
-
"@instructure/ui-color-utils": "10.26.
|
|
33
|
-
"@instructure/ui-dom-utils": "10.26.
|
|
34
|
-
"@instructure/ui-drilldown": "10.26.
|
|
35
|
-
"@instructure/ui-form-field": "10.26.
|
|
36
|
-
"@instructure/ui-icons": "10.26.
|
|
37
|
-
"@instructure/ui-pill": "10.26.
|
|
38
|
-
"@instructure/ui-popover": "10.26.
|
|
39
|
-
"@instructure/ui-react-utils": "10.26.
|
|
40
|
-
"@instructure/ui-testable": "10.26.
|
|
41
|
-
"@instructure/ui-text": "10.26.
|
|
42
|
-
"@instructure/ui-text-input": "10.26.
|
|
43
|
-
"@instructure/ui-themes": "10.26.
|
|
44
|
-
"@instructure/ui-tooltip": "10.26.
|
|
45
|
-
"@instructure/ui-utils": "10.26.
|
|
46
|
-
"@instructure/ui-view": "10.26.
|
|
27
|
+
"@instructure/console": "10.26.3",
|
|
28
|
+
"@instructure/emotion": "10.26.3",
|
|
29
|
+
"@instructure/shared-types": "10.26.3",
|
|
30
|
+
"@instructure/ui-a11y-content": "10.26.3",
|
|
31
|
+
"@instructure/ui-buttons": "10.26.3",
|
|
32
|
+
"@instructure/ui-color-utils": "10.26.3",
|
|
33
|
+
"@instructure/ui-dom-utils": "10.26.3",
|
|
34
|
+
"@instructure/ui-drilldown": "10.26.3",
|
|
35
|
+
"@instructure/ui-form-field": "10.26.3",
|
|
36
|
+
"@instructure/ui-icons": "10.26.3",
|
|
37
|
+
"@instructure/ui-pill": "10.26.3",
|
|
38
|
+
"@instructure/ui-popover": "10.26.3",
|
|
39
|
+
"@instructure/ui-react-utils": "10.26.3",
|
|
40
|
+
"@instructure/ui-testable": "10.26.3",
|
|
41
|
+
"@instructure/ui-text": "10.26.3",
|
|
42
|
+
"@instructure/ui-text-input": "10.26.3",
|
|
43
|
+
"@instructure/ui-themes": "10.26.3",
|
|
44
|
+
"@instructure/ui-tooltip": "10.26.3",
|
|
45
|
+
"@instructure/ui-utils": "10.26.3",
|
|
46
|
+
"@instructure/ui-view": "10.26.3",
|
|
47
47
|
"prop-types": "^15.8.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@instructure/ui-axe-check": "10.26.
|
|
51
|
-
"@instructure/ui-babel-preset": "10.26.
|
|
52
|
-
"@instructure/ui-scripts": "10.26.
|
|
50
|
+
"@instructure/ui-axe-check": "10.26.3",
|
|
51
|
+
"@instructure/ui-babel-preset": "10.26.3",
|
|
52
|
+
"@instructure/ui-scripts": "10.26.3",
|
|
53
53
|
"@testing-library/jest-dom": "^6.6.3",
|
|
54
54
|
"@testing-library/react": "^16.0.1",
|
|
55
55
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -110,7 +110,9 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
110
110
|
showHelperErrorMessages: false,
|
|
111
111
|
openColorPicker: false,
|
|
112
112
|
mixedColor: '',
|
|
113
|
-
labelHeight: 0
|
|
113
|
+
labelHeight: 0,
|
|
114
|
+
calculatedPopoverMaxHeight: undefined,
|
|
115
|
+
isHeightCalculated: false
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -130,7 +132,19 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
130
132
|
|
|
131
133
|
handleInputContainerRef = (el: Element | null) => {
|
|
132
134
|
this.inputContainerRef = el
|
|
133
|
-
|
|
135
|
+
|
|
136
|
+
if (el) {
|
|
137
|
+
// Defer measurement until after layout is complete and CSS-in-JS styles are applied
|
|
138
|
+
requestAnimationFrame(() => {
|
|
139
|
+
this.setLabelHeight()
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
popoverContentRef: HTMLDivElement | null = null
|
|
145
|
+
|
|
146
|
+
handlePopoverContentRef = (el: HTMLDivElement | null) => {
|
|
147
|
+
this.popoverContentRef = el
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
setLabelHeight = () => {
|
|
@@ -143,6 +157,62 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
|
|
160
|
+
// Calculate the maximum height the popover can have without extending beyond
|
|
161
|
+
// the viewport. This enables scrolling when the ColorPicker's content (all
|
|
162
|
+
// color mixing controls, presets, and contrast checker) would otherwise exceed
|
|
163
|
+
// the available viewport space. Without this calculation, the popover would
|
|
164
|
+
// render off-screen on smaller viewports.
|
|
165
|
+
handlePopoverPositioned = (position?: { placement?: string }) => {
|
|
166
|
+
if (this.popoverContentRef) {
|
|
167
|
+
// Double requestAnimationFrame ensures measurements happen after all child components
|
|
168
|
+
// (ColorMixer, ColorPreset, ColorContrast) complete their mount lifecycle and Emotion
|
|
169
|
+
// finishes injecting CSS-in-JS styles. A single rAF was insufficient as styles are
|
|
170
|
+
// injected dynamically in componentDidMount(). This timing issue only manifested when
|
|
171
|
+
// StrictMode was disabled, since StrictMode's double-rendering provided an accidental
|
|
172
|
+
// second measurement pass.
|
|
173
|
+
requestAnimationFrame(() => {
|
|
174
|
+
// First frame: DOM structure is laid out
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
// Second frame: styles injected, child components mounted, dimensions stable
|
|
177
|
+
if (!this.popoverContentRef) return
|
|
178
|
+
|
|
179
|
+
const rect = this.popoverContentRef.getBoundingClientRect()
|
|
180
|
+
const viewportHeight = window.innerHeight
|
|
181
|
+
|
|
182
|
+
// Detect if popover is positioned above (top) or below (bottom) the trigger.
|
|
183
|
+
// The Position component provides placement strings like "top center" or "bottom center".
|
|
184
|
+
const placement = position?.placement || ''
|
|
185
|
+
const isPositionedAbove = placement.startsWith('top')
|
|
186
|
+
|
|
187
|
+
let availableHeight: number
|
|
188
|
+
|
|
189
|
+
if (isPositionedAbove) {
|
|
190
|
+
// When opening upward: available space is from viewport top to popover bottom.
|
|
191
|
+
// This is the space where the popover can expand within the viewport.
|
|
192
|
+
availableHeight = rect.top + rect.height - 16
|
|
193
|
+
} else {
|
|
194
|
+
// When opening downward: available space is from popover top to viewport bottom.
|
|
195
|
+
// Subtract a small buffer (16px) for padding/margin.
|
|
196
|
+
availableHeight = viewportHeight - rect.top - 16
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const propMaxHeight = this.props.popoverMaxHeight
|
|
200
|
+
let calculatedMaxHeight = `${Math.max(100, availableHeight)}px`
|
|
201
|
+
|
|
202
|
+
// If prop specifies a maxHeight, respect it as an additional constraint
|
|
203
|
+
if (propMaxHeight && propMaxHeight !== '100vh') {
|
|
204
|
+
calculatedMaxHeight = propMaxHeight
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.setState({
|
|
208
|
+
calculatedPopoverMaxHeight: calculatedMaxHeight,
|
|
209
|
+
isHeightCalculated: true
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
146
216
|
componentDidMount() {
|
|
147
217
|
this.props.makeStyles?.({ ...this.state, isSimple: this.isSimple })
|
|
148
218
|
this.checkSettings()
|
|
@@ -427,7 +497,12 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
427
497
|
}
|
|
428
498
|
isShowingContent={this.state.openColorPicker}
|
|
429
499
|
onShowContent={() => {
|
|
430
|
-
this.setState({
|
|
500
|
+
this.setState({
|
|
501
|
+
openColorPicker: true,
|
|
502
|
+
mixedColor: this.state.hexCode,
|
|
503
|
+
calculatedPopoverMaxHeight: undefined,
|
|
504
|
+
isHeightCalculated: false
|
|
505
|
+
})
|
|
431
506
|
}}
|
|
432
507
|
onHideContent={() => {
|
|
433
508
|
this.setState({ openColorPicker: false })
|
|
@@ -438,8 +513,13 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
438
513
|
shouldReturnFocus
|
|
439
514
|
shouldCloseOnDocumentClick
|
|
440
515
|
offsetY="10rem"
|
|
516
|
+
onPositioned={this.handlePopoverPositioned}
|
|
517
|
+
onPositionChanged={this.handlePopoverPositioned}
|
|
441
518
|
>
|
|
442
|
-
<div
|
|
519
|
+
<div
|
|
520
|
+
css={this.props.styles?.popoverContentContainer}
|
|
521
|
+
ref={this.handlePopoverContentRef}
|
|
522
|
+
>
|
|
443
523
|
{this.isDefaultPopover
|
|
444
524
|
? this.renderDefaultPopoverContent()
|
|
445
525
|
: this.renderCustomPopoverContent()}
|
|
@@ -470,7 +550,9 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
470
550
|
() =>
|
|
471
551
|
this.setState({
|
|
472
552
|
openColorPicker: false,
|
|
473
|
-
mixedColor: this.state.hexCode
|
|
553
|
+
mixedColor: this.state.hexCode,
|
|
554
|
+
calculatedPopoverMaxHeight: undefined,
|
|
555
|
+
isHeightCalculated: false
|
|
474
556
|
})
|
|
475
557
|
)}
|
|
476
558
|
</div>
|
|
@@ -576,7 +658,9 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
576
658
|
onClick={() =>
|
|
577
659
|
this.setState({
|
|
578
660
|
openColorPicker: false,
|
|
579
|
-
mixedColor: this.state.hexCode
|
|
661
|
+
mixedColor: this.state.hexCode,
|
|
662
|
+
calculatedPopoverMaxHeight: undefined,
|
|
663
|
+
isHeightCalculated: false
|
|
580
664
|
})
|
|
581
665
|
}
|
|
582
666
|
color="secondary"
|
|
@@ -639,7 +723,10 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
639
723
|
{!this.isSimple && (
|
|
640
724
|
<div
|
|
641
725
|
css={this.props.styles?.colorMixerButtonContainer}
|
|
642
|
-
style={{
|
|
726
|
+
style={{
|
|
727
|
+
alignSelf: this.state.labelHeight > 0 ? 'flex-start' : 'flex-end',
|
|
728
|
+
paddingTop: this.state.labelHeight
|
|
729
|
+
}}
|
|
643
730
|
>
|
|
644
731
|
<div css={this.props.styles?.colorMixerButtonWrapper}>
|
|
645
732
|
{this.renderPopover()}
|
package/src/ColorPicker/props.ts
CHANGED
|
@@ -54,7 +54,7 @@ const generateStyle = (
|
|
|
54
54
|
spacing
|
|
55
55
|
} = componentTheme
|
|
56
56
|
const { checkContrast, popoverMaxHeight, margin } = props
|
|
57
|
-
const { isSimple } = state
|
|
57
|
+
const { isSimple, calculatedPopoverMaxHeight } = state
|
|
58
58
|
|
|
59
59
|
const cssMargin = mapSpacingToShorthand(margin, spacing)
|
|
60
60
|
return {
|
|
@@ -127,8 +127,14 @@ const generateStyle = (
|
|
|
127
127
|
},
|
|
128
128
|
popoverContentContainer: {
|
|
129
129
|
label: 'colorPicker__popoverContentContainer',
|
|
130
|
-
maxHeight: popoverMaxHeight || '100vh',
|
|
131
|
-
|
|
130
|
+
maxHeight: calculatedPopoverMaxHeight || popoverMaxHeight || '100vh',
|
|
131
|
+
overflowY: 'auto',
|
|
132
|
+
overflowX: 'hidden',
|
|
133
|
+
scrollbarGutter: 'stable',
|
|
134
|
+
display: 'flex',
|
|
135
|
+
flexDirection: 'column',
|
|
136
|
+
opacity: state.isHeightCalculated ? 1 : 0,
|
|
137
|
+
transition: 'opacity 150ms ease-in'
|
|
132
138
|
},
|
|
133
139
|
colorMixerButtonWrapper: {
|
|
134
140
|
label: 'colorPicker__colorMixerButtonWrapper',
|