@instructure/ui-color-picker 11.2.0 → 11.2.1-snapshot-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 +12 -0
- package/es/ColorPicker/index.js +76 -5
- package/es/ColorPicker/styles.js +10 -4
- package/lib/ColorPicker/index.js +76 -5
- package/lib/ColorPicker/styles.js +10 -4
- package/package.json +23 -23
- package/src/ColorPicker/README.md +57 -0
- package/src/ColorPicker/index.tsx +94 -7
- package/src/ColorPicker/props.ts +2 -0
- package/src/ColorPicker/styles.ts +9 -4
- 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,18 @@
|
|
|
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.2.1-snapshot-3](https://github.com/instructure/instructure-ui/compare/v11.2.0...v11.2.1-snapshot-3) (2025-11-19)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **ui-color-picker:** fix mixer button alignment and visual jump ([68c3e60](https://github.com/instructure/instructure-ui/commit/68c3e60acc5e1f410ed79a623a35fa3eaf7107f5))
|
|
12
|
+
* **ui-color-picker:** fix popover scrolling when content exceeds viewport ([66f2b18](https://github.com/instructure/instructure-ui/commit/66f2b18af0dead1f62ee61629262c39c6273dad0))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
# [11.2.0](https://github.com/instructure/instructure-ui/compare/v11.0.1...v11.2.0) (2025-11-06)
|
|
7
19
|
|
|
8
20
|
|
package/es/ColorPicker/index.js
CHANGED
|
@@ -63,7 +63,16 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
63
63
|
this.inputContainerRef = null;
|
|
64
64
|
this.handleInputContainerRef = el => {
|
|
65
65
|
this.inputContainerRef = el;
|
|
66
|
-
|
|
66
|
+
if (el) {
|
|
67
|
+
// Defer measurement until after layout is complete and CSS-in-JS styles are applied
|
|
68
|
+
requestAnimationFrame(() => {
|
|
69
|
+
this.setLabelHeight();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
this.popoverContentRef = null;
|
|
74
|
+
this.handlePopoverContentRef = el => {
|
|
75
|
+
this.popoverContentRef = el;
|
|
67
76
|
};
|
|
68
77
|
this.setLabelHeight = () => {
|
|
69
78
|
if (this.inputContainerRef) {
|
|
@@ -73,6 +82,56 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
73
82
|
});
|
|
74
83
|
}
|
|
75
84
|
};
|
|
85
|
+
// Calculate the maximum height the popover can have without extending beyond
|
|
86
|
+
// the viewport. This enables scrolling when the ColorPicker's content (all
|
|
87
|
+
// color mixing controls, presets, and contrast checker) would otherwise exceed
|
|
88
|
+
// the available viewport space. Without this calculation, the popover would
|
|
89
|
+
// render off-screen on smaller viewports.
|
|
90
|
+
this.handlePopoverPositioned = position => {
|
|
91
|
+
if (this.popoverContentRef) {
|
|
92
|
+
// Double requestAnimationFrame ensures measurements happen after all child components
|
|
93
|
+
// (ColorMixer, ColorPreset, ColorContrast) complete their mount lifecycle and Emotion
|
|
94
|
+
// finishes injecting CSS-in-JS styles. A single rAF was insufficient as styles are
|
|
95
|
+
// injected dynamically in componentDidMount(). This timing issue only manifested when
|
|
96
|
+
// StrictMode was disabled, since StrictMode's double-rendering provided an accidental
|
|
97
|
+
// second measurement pass.
|
|
98
|
+
requestAnimationFrame(() => {
|
|
99
|
+
// First frame: DOM structure is laid out
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
// Second frame: styles injected, child components mounted, dimensions stable
|
|
102
|
+
if (!this.popoverContentRef) return;
|
|
103
|
+
const rect = this.popoverContentRef.getBoundingClientRect();
|
|
104
|
+
const viewportHeight = window.innerHeight;
|
|
105
|
+
|
|
106
|
+
// Detect if popover is positioned above (top) or below (bottom) the trigger.
|
|
107
|
+
// The Position component provides placement strings like "top center" or "bottom center".
|
|
108
|
+
const placement = (position === null || position === void 0 ? void 0 : position.placement) || '';
|
|
109
|
+
const isPositionedAbove = placement.startsWith('top');
|
|
110
|
+
let availableHeight;
|
|
111
|
+
if (isPositionedAbove) {
|
|
112
|
+
// When opening upward: available space is from viewport top to popover bottom.
|
|
113
|
+
// This is the space where the popover can expand within the viewport.
|
|
114
|
+
availableHeight = rect.top + rect.height - 16;
|
|
115
|
+
} else {
|
|
116
|
+
// When opening downward: available space is from popover top to viewport bottom.
|
|
117
|
+
// Subtract a small buffer (16px) for padding/margin.
|
|
118
|
+
availableHeight = viewportHeight - rect.top - 16;
|
|
119
|
+
}
|
|
120
|
+
const propMaxHeight = this.props.popoverMaxHeight;
|
|
121
|
+
let calculatedMaxHeight = `${Math.max(100, availableHeight)}px`;
|
|
122
|
+
|
|
123
|
+
// If prop specifies a maxHeight, respect it as an additional constraint
|
|
124
|
+
if (propMaxHeight && propMaxHeight !== '100vh') {
|
|
125
|
+
calculatedMaxHeight = propMaxHeight;
|
|
126
|
+
}
|
|
127
|
+
this.setState({
|
|
128
|
+
calculatedPopoverMaxHeight: calculatedMaxHeight,
|
|
129
|
+
isHeightCalculated: true
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
76
135
|
this.checkSettings = () => {
|
|
77
136
|
if (this.props.children && this.props.colorMixerSettings) {
|
|
78
137
|
warn(false, 'You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.', '');
|
|
@@ -95,7 +154,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
95
154
|
onShowContent: () => {
|
|
96
155
|
this.setState({
|
|
97
156
|
openColorPicker: true,
|
|
98
|
-
mixedColor: this.state.hexCode
|
|
157
|
+
mixedColor: this.state.hexCode,
|
|
158
|
+
calculatedPopoverMaxHeight: void 0,
|
|
159
|
+
isHeightCalculated: false
|
|
99
160
|
});
|
|
100
161
|
},
|
|
101
162
|
onHideContent: () => {
|
|
@@ -109,8 +170,11 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
109
170
|
shouldReturnFocus: true,
|
|
110
171
|
shouldCloseOnDocumentClick: true,
|
|
111
172
|
offsetY: "10rem",
|
|
173
|
+
onPositioned: this.handlePopoverPositioned,
|
|
174
|
+
onPositionChanged: this.handlePopoverPositioned,
|
|
112
175
|
children: _jsx("div", {
|
|
113
176
|
css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.popoverContentContainer,
|
|
177
|
+
ref: this.handlePopoverContentRef,
|
|
114
178
|
children: this.isDefaultPopover ? this.renderDefaultPopoverContent() : this.renderCustomPopoverContent()
|
|
115
179
|
})
|
|
116
180
|
});
|
|
@@ -132,7 +196,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
132
196
|
onChange === null || onChange === void 0 ? void 0 : onChange(`#${this.mixedColorWithStrippedAlpha}`);
|
|
133
197
|
}, () => this.setState({
|
|
134
198
|
openColorPicker: false,
|
|
135
|
-
mixedColor: this.state.hexCode
|
|
199
|
+
mixedColor: this.state.hexCode,
|
|
200
|
+
calculatedPopoverMaxHeight: void 0,
|
|
201
|
+
isHeightCalculated: false
|
|
136
202
|
}))
|
|
137
203
|
});
|
|
138
204
|
};
|
|
@@ -185,7 +251,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
185
251
|
children: [_jsx(Button, {
|
|
186
252
|
onClick: () => this.setState({
|
|
187
253
|
openColorPicker: false,
|
|
188
|
-
mixedColor: this.state.hexCode
|
|
254
|
+
mixedColor: this.state.hexCode,
|
|
255
|
+
calculatedPopoverMaxHeight: void 0,
|
|
256
|
+
isHeightCalculated: false
|
|
189
257
|
}),
|
|
190
258
|
color: "secondary",
|
|
191
259
|
margin: "xx-small",
|
|
@@ -216,7 +284,9 @@ let ColorPicker = (_dec = withStyle(generateStyle, generateComponentTheme), _dec
|
|
|
216
284
|
showHelperErrorMessages: false,
|
|
217
285
|
openColorPicker: false,
|
|
218
286
|
mixedColor: '',
|
|
219
|
-
labelHeight: 0
|
|
287
|
+
labelHeight: 0,
|
|
288
|
+
calculatedPopoverMaxHeight: void 0,
|
|
289
|
+
isHeightCalculated: false
|
|
220
290
|
};
|
|
221
291
|
}
|
|
222
292
|
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: {
|
|
@@ -107,13 +108,18 @@ const generateStyle = (componentTheme, props, state) => {
|
|
|
107
108
|
},
|
|
108
109
|
colorMixerButtonContainer: {
|
|
109
110
|
label: 'colorPicker__colorMixerButtonContainer',
|
|
110
|
-
alignSelf: 'flex-start',
|
|
111
111
|
marginInlineStart: componentTheme.colorMixerButtonContainerLeftMargin
|
|
112
112
|
},
|
|
113
113
|
popoverContentContainer: {
|
|
114
114
|
label: 'colorPicker__popoverContentContainer',
|
|
115
|
-
maxHeight: popoverMaxHeight || '100vh',
|
|
116
|
-
|
|
115
|
+
maxHeight: calculatedPopoverMaxHeight || popoverMaxHeight || '100vh',
|
|
116
|
+
overflowY: 'auto',
|
|
117
|
+
overflowX: 'hidden',
|
|
118
|
+
scrollbarGutter: 'stable',
|
|
119
|
+
display: 'flex',
|
|
120
|
+
flexDirection: 'column',
|
|
121
|
+
opacity: state.isHeightCalculated ? 1 : 0,
|
|
122
|
+
transition: 'opacity 150ms ease-in'
|
|
117
123
|
},
|
|
118
124
|
colorMixerButtonWrapper: {
|
|
119
125
|
label: 'colorPicker__colorMixerButtonWrapper',
|
package/lib/ColorPicker/index.js
CHANGED
|
@@ -74,7 +74,16 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
74
74
|
this.inputContainerRef = null;
|
|
75
75
|
this.handleInputContainerRef = el => {
|
|
76
76
|
this.inputContainerRef = el;
|
|
77
|
-
|
|
77
|
+
if (el) {
|
|
78
|
+
// Defer measurement until after layout is complete and CSS-in-JS styles are applied
|
|
79
|
+
requestAnimationFrame(() => {
|
|
80
|
+
this.setLabelHeight();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
this.popoverContentRef = null;
|
|
85
|
+
this.handlePopoverContentRef = el => {
|
|
86
|
+
this.popoverContentRef = el;
|
|
78
87
|
};
|
|
79
88
|
this.setLabelHeight = () => {
|
|
80
89
|
if (this.inputContainerRef) {
|
|
@@ -84,6 +93,56 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
84
93
|
});
|
|
85
94
|
}
|
|
86
95
|
};
|
|
96
|
+
// Calculate the maximum height the popover can have without extending beyond
|
|
97
|
+
// the viewport. This enables scrolling when the ColorPicker's content (all
|
|
98
|
+
// color mixing controls, presets, and contrast checker) would otherwise exceed
|
|
99
|
+
// the available viewport space. Without this calculation, the popover would
|
|
100
|
+
// render off-screen on smaller viewports.
|
|
101
|
+
this.handlePopoverPositioned = position => {
|
|
102
|
+
if (this.popoverContentRef) {
|
|
103
|
+
// Double requestAnimationFrame ensures measurements happen after all child components
|
|
104
|
+
// (ColorMixer, ColorPreset, ColorContrast) complete their mount lifecycle and Emotion
|
|
105
|
+
// finishes injecting CSS-in-JS styles. A single rAF was insufficient as styles are
|
|
106
|
+
// injected dynamically in componentDidMount(). This timing issue only manifested when
|
|
107
|
+
// StrictMode was disabled, since StrictMode's double-rendering provided an accidental
|
|
108
|
+
// second measurement pass.
|
|
109
|
+
requestAnimationFrame(() => {
|
|
110
|
+
// First frame: DOM structure is laid out
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
// Second frame: styles injected, child components mounted, dimensions stable
|
|
113
|
+
if (!this.popoverContentRef) return;
|
|
114
|
+
const rect = this.popoverContentRef.getBoundingClientRect();
|
|
115
|
+
const viewportHeight = window.innerHeight;
|
|
116
|
+
|
|
117
|
+
// Detect if popover is positioned above (top) or below (bottom) the trigger.
|
|
118
|
+
// The Position component provides placement strings like "top center" or "bottom center".
|
|
119
|
+
const placement = (position === null || position === void 0 ? void 0 : position.placement) || '';
|
|
120
|
+
const isPositionedAbove = placement.startsWith('top');
|
|
121
|
+
let availableHeight;
|
|
122
|
+
if (isPositionedAbove) {
|
|
123
|
+
// When opening upward: available space is from viewport top to popover bottom.
|
|
124
|
+
// This is the space where the popover can expand within the viewport.
|
|
125
|
+
availableHeight = rect.top + rect.height - 16;
|
|
126
|
+
} else {
|
|
127
|
+
// When opening downward: available space is from popover top to viewport bottom.
|
|
128
|
+
// Subtract a small buffer (16px) for padding/margin.
|
|
129
|
+
availableHeight = viewportHeight - rect.top - 16;
|
|
130
|
+
}
|
|
131
|
+
const propMaxHeight = this.props.popoverMaxHeight;
|
|
132
|
+
let calculatedMaxHeight = `${Math.max(100, availableHeight)}px`;
|
|
133
|
+
|
|
134
|
+
// If prop specifies a maxHeight, respect it as an additional constraint
|
|
135
|
+
if (propMaxHeight && propMaxHeight !== '100vh') {
|
|
136
|
+
calculatedMaxHeight = propMaxHeight;
|
|
137
|
+
}
|
|
138
|
+
this.setState({
|
|
139
|
+
calculatedPopoverMaxHeight: calculatedMaxHeight,
|
|
140
|
+
isHeightCalculated: true
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
87
146
|
this.checkSettings = () => {
|
|
88
147
|
if (this.props.children && this.props.colorMixerSettings) {
|
|
89
148
|
(0, _console.warn)(false, 'You should either use children, colorMixerSettings or neither, not both. In this case, the colorMixerSettings will be ignored.', '');
|
|
@@ -106,7 +165,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
106
165
|
onShowContent: () => {
|
|
107
166
|
this.setState({
|
|
108
167
|
openColorPicker: true,
|
|
109
|
-
mixedColor: this.state.hexCode
|
|
168
|
+
mixedColor: this.state.hexCode,
|
|
169
|
+
calculatedPopoverMaxHeight: void 0,
|
|
170
|
+
isHeightCalculated: false
|
|
110
171
|
});
|
|
111
172
|
},
|
|
112
173
|
onHideContent: () => {
|
|
@@ -120,8 +181,11 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
120
181
|
shouldReturnFocus: true,
|
|
121
182
|
shouldCloseOnDocumentClick: true,
|
|
122
183
|
offsetY: "10rem",
|
|
184
|
+
onPositioned: this.handlePopoverPositioned,
|
|
185
|
+
onPositionChanged: this.handlePopoverPositioned,
|
|
123
186
|
children: (0, _jsxRuntime.jsx)("div", {
|
|
124
187
|
css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.popoverContentContainer,
|
|
188
|
+
ref: this.handlePopoverContentRef,
|
|
125
189
|
children: this.isDefaultPopover ? this.renderDefaultPopoverContent() : this.renderCustomPopoverContent()
|
|
126
190
|
})
|
|
127
191
|
});
|
|
@@ -143,7 +207,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
143
207
|
onChange === null || onChange === void 0 ? void 0 : onChange(`#${this.mixedColorWithStrippedAlpha}`);
|
|
144
208
|
}, () => this.setState({
|
|
145
209
|
openColorPicker: false,
|
|
146
|
-
mixedColor: this.state.hexCode
|
|
210
|
+
mixedColor: this.state.hexCode,
|
|
211
|
+
calculatedPopoverMaxHeight: void 0,
|
|
212
|
+
isHeightCalculated: false
|
|
147
213
|
}))
|
|
148
214
|
});
|
|
149
215
|
};
|
|
@@ -196,7 +262,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
196
262
|
children: [(0, _jsxRuntime.jsx)(_Button.Button, {
|
|
197
263
|
onClick: () => this.setState({
|
|
198
264
|
openColorPicker: false,
|
|
199
|
-
mixedColor: this.state.hexCode
|
|
265
|
+
mixedColor: this.state.hexCode,
|
|
266
|
+
calculatedPopoverMaxHeight: void 0,
|
|
267
|
+
isHeightCalculated: false
|
|
200
268
|
}),
|
|
201
269
|
color: "secondary",
|
|
202
270
|
margin: "xx-small",
|
|
@@ -227,7 +295,9 @@ let ColorPicker = exports.ColorPicker = (_dec = (0, _emotion.withStyle)(_styles.
|
|
|
227
295
|
showHelperErrorMessages: false,
|
|
228
296
|
openColorPicker: false,
|
|
229
297
|
mixedColor: '',
|
|
230
|
-
labelHeight: 0
|
|
298
|
+
labelHeight: 0,
|
|
299
|
+
calculatedPopoverMaxHeight: void 0,
|
|
300
|
+
isHeightCalculated: false
|
|
231
301
|
};
|
|
232
302
|
}
|
|
233
303
|
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: {
|
|
@@ -113,13 +114,18 @@ const generateStyle = (componentTheme, props, state) => {
|
|
|
113
114
|
},
|
|
114
115
|
colorMixerButtonContainer: {
|
|
115
116
|
label: 'colorPicker__colorMixerButtonContainer',
|
|
116
|
-
alignSelf: 'flex-start',
|
|
117
117
|
marginInlineStart: componentTheme.colorMixerButtonContainerLeftMargin
|
|
118
118
|
},
|
|
119
119
|
popoverContentContainer: {
|
|
120
120
|
label: 'colorPicker__popoverContentContainer',
|
|
121
|
-
maxHeight: popoverMaxHeight || '100vh',
|
|
122
|
-
|
|
121
|
+
maxHeight: calculatedPopoverMaxHeight || popoverMaxHeight || '100vh',
|
|
122
|
+
overflowY: 'auto',
|
|
123
|
+
overflowX: 'hidden',
|
|
124
|
+
scrollbarGutter: 'stable',
|
|
125
|
+
display: 'flex',
|
|
126
|
+
flexDirection: 'column',
|
|
127
|
+
opacity: state.isHeightCalculated ? 1 : 0,
|
|
128
|
+
transition: 'opacity 150ms ease-in'
|
|
123
129
|
},
|
|
124
130
|
colorMixerButtonWrapper: {
|
|
125
131
|
label: 'colorPicker__colorMixerButtonWrapper',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/ui-color-picker",
|
|
3
|
-
"version": "11.2.
|
|
3
|
+
"version": "11.2.1-snapshot-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",
|
|
@@ -15,34 +15,34 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/runtime": "^7.27.6",
|
|
18
|
-
"@instructure/
|
|
19
|
-
"@instructure/
|
|
20
|
-
"@instructure/ui-a11y-content": "11.2.
|
|
21
|
-
"@instructure/ui-buttons": "11.2.
|
|
22
|
-
"@instructure/
|
|
23
|
-
"@instructure/ui-color-utils": "11.2.
|
|
24
|
-
"@instructure/ui-dom-utils": "11.2.
|
|
25
|
-
"@instructure/ui-drilldown": "11.2.
|
|
26
|
-
"@instructure/ui-
|
|
27
|
-
"@instructure/ui-
|
|
28
|
-
"@instructure/ui-
|
|
29
|
-
"@instructure/ui-
|
|
30
|
-
"@instructure/ui-
|
|
31
|
-
"@instructure/ui-text": "11.2.
|
|
32
|
-
"@instructure/ui-
|
|
33
|
-
"@instructure/ui-
|
|
34
|
-
"@instructure/ui-utils": "11.2.
|
|
35
|
-
"@instructure/ui-
|
|
36
|
-
"@instructure/ui-view": "11.2.
|
|
18
|
+
"@instructure/console": "11.2.1-snapshot-3",
|
|
19
|
+
"@instructure/shared-types": "11.2.1-snapshot-3",
|
|
20
|
+
"@instructure/ui-a11y-content": "11.2.1-snapshot-3",
|
|
21
|
+
"@instructure/ui-buttons": "11.2.1-snapshot-3",
|
|
22
|
+
"@instructure/emotion": "11.2.1-snapshot-3",
|
|
23
|
+
"@instructure/ui-color-utils": "11.2.1-snapshot-3",
|
|
24
|
+
"@instructure/ui-dom-utils": "11.2.1-snapshot-3",
|
|
25
|
+
"@instructure/ui-drilldown": "11.2.1-snapshot-3",
|
|
26
|
+
"@instructure/ui-icons": "11.2.1-snapshot-3",
|
|
27
|
+
"@instructure/ui-pill": "11.2.1-snapshot-3",
|
|
28
|
+
"@instructure/ui-form-field": "11.2.1-snapshot-3",
|
|
29
|
+
"@instructure/ui-react-utils": "11.2.1-snapshot-3",
|
|
30
|
+
"@instructure/ui-popover": "11.2.1-snapshot-3",
|
|
31
|
+
"@instructure/ui-text": "11.2.1-snapshot-3",
|
|
32
|
+
"@instructure/ui-tooltip": "11.2.1-snapshot-3",
|
|
33
|
+
"@instructure/ui-text-input": "11.2.1-snapshot-3",
|
|
34
|
+
"@instructure/ui-utils": "11.2.1-snapshot-3",
|
|
35
|
+
"@instructure/ui-themes": "11.2.1-snapshot-3",
|
|
36
|
+
"@instructure/ui-view": "11.2.1-snapshot-3"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/jest-dom": "^6.6.3",
|
|
40
40
|
"@testing-library/react": "15.0.7",
|
|
41
41
|
"@testing-library/user-event": "^14.6.1",
|
|
42
42
|
"vitest": "^3.2.2",
|
|
43
|
-
"@instructure/ui-
|
|
44
|
-
"@instructure/ui-babel-preset": "11.2.
|
|
45
|
-
"@instructure/ui-
|
|
43
|
+
"@instructure/ui-axe-check": "11.2.1-snapshot-3",
|
|
44
|
+
"@instructure/ui-babel-preset": "11.2.1-snapshot-3",
|
|
45
|
+
"@instructure/ui-scripts": "11.2.1-snapshot-3"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"react": ">=18 <=19"
|
|
@@ -351,3 +351,60 @@ type: example
|
|
|
351
351
|
</div>
|
|
352
352
|
|
|
353
353
|
```
|
|
354
|
+
|
|
355
|
+
### Scrollable Popover Content
|
|
356
|
+
|
|
357
|
+
When the ColorPicker popover contains tall content (e.g., ColorMixer + ColorPreset + ColorContrast), the component automatically calculates the available viewport space and makes the popover scrollable.
|
|
358
|
+
|
|
359
|
+
The `popoverMaxHeight` prop can be used to set a custom maximum height for the popover content. By default, it's set to `'100vh'`, but the component dynamically adjusts this based on the available space to ensure the popover fits within the viewport and remains scrollable.
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
---
|
|
363
|
+
type: example
|
|
364
|
+
---
|
|
365
|
+
<ColorPicker
|
|
366
|
+
label="Color"
|
|
367
|
+
placeholderText="Enter HEX"
|
|
368
|
+
popoverButtonScreenReaderLabel="Open color mixer popover"
|
|
369
|
+
popoverMaxHeight="500px"
|
|
370
|
+
colorMixerSettings={{
|
|
371
|
+
popoverAddButtonLabel: "Add",
|
|
372
|
+
popoverCloseButtonLabel: "Cancel",
|
|
373
|
+
colorMixer: {
|
|
374
|
+
withAlpha: true,
|
|
375
|
+
rgbRedInputScreenReaderLabel:'Input field for red',
|
|
376
|
+
rgbGreenInputScreenReaderLabel:'Input field for green',
|
|
377
|
+
rgbBlueInputScreenReaderLabel:'Input field for blue',
|
|
378
|
+
rgbAlphaInputScreenReaderLabel:'Input field for alpha',
|
|
379
|
+
colorSliderNavigationExplanationScreenReaderLabel:`You are on a color slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`,
|
|
380
|
+
alphaSliderNavigationExplanationScreenReaderLabel:`You are on an alpha slider. To navigate the slider left or right, use the 'A' and 'D' buttons respectively`,
|
|
381
|
+
colorPaletteNavigationExplanationScreenReaderLabel:`You are on a color palette. To navigate on the palette up, left, down or right, use the 'W', 'A', 'S' and 'D' buttons respectively`
|
|
382
|
+
},
|
|
383
|
+
colorPreset: {
|
|
384
|
+
label: "Preset Colors",
|
|
385
|
+
colors: [
|
|
386
|
+
"#FF0000",
|
|
387
|
+
"#00FF00",
|
|
388
|
+
"#0000FF",
|
|
389
|
+
"#FFFF00",
|
|
390
|
+
"#FF00FF",
|
|
391
|
+
"#00FFFF",
|
|
392
|
+
"#000000",
|
|
393
|
+
"#FFFFFF",
|
|
394
|
+
"#808080"
|
|
395
|
+
]
|
|
396
|
+
},
|
|
397
|
+
colorContrast: {
|
|
398
|
+
firstColor: "#FFFFFF",
|
|
399
|
+
label: "Color Contrast Ratio",
|
|
400
|
+
successLabel: "PASS",
|
|
401
|
+
failureLabel: "FAIL",
|
|
402
|
+
normalTextLabel: "Normal text",
|
|
403
|
+
largeTextLabel: "Large text",
|
|
404
|
+
graphicsTextLabel: "Graphics text",
|
|
405
|
+
firstColorLabel: "Background",
|
|
406
|
+
secondColorLabel: "Foreground"
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
/>
|
|
410
|
+
```
|
|
@@ -107,7 +107,9 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
107
107
|
showHelperErrorMessages: false,
|
|
108
108
|
openColorPicker: false,
|
|
109
109
|
mixedColor: '',
|
|
110
|
-
labelHeight: 0
|
|
110
|
+
labelHeight: 0,
|
|
111
|
+
calculatedPopoverMaxHeight: undefined,
|
|
112
|
+
isHeightCalculated: false
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
@@ -127,7 +129,19 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
127
129
|
|
|
128
130
|
handleInputContainerRef = (el: Element | null) => {
|
|
129
131
|
this.inputContainerRef = el
|
|
130
|
-
|
|
132
|
+
|
|
133
|
+
if (el) {
|
|
134
|
+
// Defer measurement until after layout is complete and CSS-in-JS styles are applied
|
|
135
|
+
requestAnimationFrame(() => {
|
|
136
|
+
this.setLabelHeight()
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
popoverContentRef: HTMLDivElement | null = null
|
|
142
|
+
|
|
143
|
+
handlePopoverContentRef = (el: HTMLDivElement | null) => {
|
|
144
|
+
this.popoverContentRef = el
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
setLabelHeight = () => {
|
|
@@ -140,6 +154,62 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
|
|
157
|
+
// Calculate the maximum height the popover can have without extending beyond
|
|
158
|
+
// the viewport. This enables scrolling when the ColorPicker's content (all
|
|
159
|
+
// color mixing controls, presets, and contrast checker) would otherwise exceed
|
|
160
|
+
// the available viewport space. Without this calculation, the popover would
|
|
161
|
+
// render off-screen on smaller viewports.
|
|
162
|
+
handlePopoverPositioned = (position?: { placement?: string }) => {
|
|
163
|
+
if (this.popoverContentRef) {
|
|
164
|
+
// Double requestAnimationFrame ensures measurements happen after all child components
|
|
165
|
+
// (ColorMixer, ColorPreset, ColorContrast) complete their mount lifecycle and Emotion
|
|
166
|
+
// finishes injecting CSS-in-JS styles. A single rAF was insufficient as styles are
|
|
167
|
+
// injected dynamically in componentDidMount(). This timing issue only manifested when
|
|
168
|
+
// StrictMode was disabled, since StrictMode's double-rendering provided an accidental
|
|
169
|
+
// second measurement pass.
|
|
170
|
+
requestAnimationFrame(() => {
|
|
171
|
+
// First frame: DOM structure is laid out
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
// Second frame: styles injected, child components mounted, dimensions stable
|
|
174
|
+
if (!this.popoverContentRef) return
|
|
175
|
+
|
|
176
|
+
const rect = this.popoverContentRef.getBoundingClientRect()
|
|
177
|
+
const viewportHeight = window.innerHeight
|
|
178
|
+
|
|
179
|
+
// Detect if popover is positioned above (top) or below (bottom) the trigger.
|
|
180
|
+
// The Position component provides placement strings like "top center" or "bottom center".
|
|
181
|
+
const placement = position?.placement || ''
|
|
182
|
+
const isPositionedAbove = placement.startsWith('top')
|
|
183
|
+
|
|
184
|
+
let availableHeight: number
|
|
185
|
+
|
|
186
|
+
if (isPositionedAbove) {
|
|
187
|
+
// When opening upward: available space is from viewport top to popover bottom.
|
|
188
|
+
// This is the space where the popover can expand within the viewport.
|
|
189
|
+
availableHeight = rect.top + rect.height - 16
|
|
190
|
+
} else {
|
|
191
|
+
// When opening downward: available space is from popover top to viewport bottom.
|
|
192
|
+
// Subtract a small buffer (16px) for padding/margin.
|
|
193
|
+
availableHeight = viewportHeight - rect.top - 16
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const propMaxHeight = this.props.popoverMaxHeight
|
|
197
|
+
let calculatedMaxHeight = `${Math.max(100, availableHeight)}px`
|
|
198
|
+
|
|
199
|
+
// If prop specifies a maxHeight, respect it as an additional constraint
|
|
200
|
+
if (propMaxHeight && propMaxHeight !== '100vh') {
|
|
201
|
+
calculatedMaxHeight = propMaxHeight
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.setState({
|
|
205
|
+
calculatedPopoverMaxHeight: calculatedMaxHeight,
|
|
206
|
+
isHeightCalculated: true
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
143
213
|
componentDidMount() {
|
|
144
214
|
this.props.makeStyles?.({ ...this.state, isSimple: this.isSimple })
|
|
145
215
|
this.checkSettings()
|
|
@@ -424,7 +494,12 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
424
494
|
}
|
|
425
495
|
isShowingContent={this.state.openColorPicker}
|
|
426
496
|
onShowContent={() => {
|
|
427
|
-
this.setState({
|
|
497
|
+
this.setState({
|
|
498
|
+
openColorPicker: true,
|
|
499
|
+
mixedColor: this.state.hexCode,
|
|
500
|
+
calculatedPopoverMaxHeight: undefined,
|
|
501
|
+
isHeightCalculated: false
|
|
502
|
+
})
|
|
428
503
|
}}
|
|
429
504
|
onHideContent={() => {
|
|
430
505
|
this.setState({ openColorPicker: false })
|
|
@@ -435,8 +510,13 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
435
510
|
shouldReturnFocus
|
|
436
511
|
shouldCloseOnDocumentClick
|
|
437
512
|
offsetY="10rem"
|
|
513
|
+
onPositioned={this.handlePopoverPositioned}
|
|
514
|
+
onPositionChanged={this.handlePopoverPositioned}
|
|
438
515
|
>
|
|
439
|
-
<div
|
|
516
|
+
<div
|
|
517
|
+
css={this.props.styles?.popoverContentContainer}
|
|
518
|
+
ref={this.handlePopoverContentRef}
|
|
519
|
+
>
|
|
440
520
|
{this.isDefaultPopover
|
|
441
521
|
? this.renderDefaultPopoverContent()
|
|
442
522
|
: this.renderCustomPopoverContent()}
|
|
@@ -467,7 +547,9 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
467
547
|
() =>
|
|
468
548
|
this.setState({
|
|
469
549
|
openColorPicker: false,
|
|
470
|
-
mixedColor: this.state.hexCode
|
|
550
|
+
mixedColor: this.state.hexCode,
|
|
551
|
+
calculatedPopoverMaxHeight: undefined,
|
|
552
|
+
isHeightCalculated: false
|
|
471
553
|
})
|
|
472
554
|
)}
|
|
473
555
|
</div>
|
|
@@ -573,7 +655,9 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
573
655
|
onClick={() =>
|
|
574
656
|
this.setState({
|
|
575
657
|
openColorPicker: false,
|
|
576
|
-
mixedColor: this.state.hexCode
|
|
658
|
+
mixedColor: this.state.hexCode,
|
|
659
|
+
calculatedPopoverMaxHeight: undefined,
|
|
660
|
+
isHeightCalculated: false
|
|
577
661
|
})
|
|
578
662
|
}
|
|
579
663
|
color="secondary"
|
|
@@ -637,7 +721,10 @@ class ColorPicker extends Component<ColorPickerProps, ColorPickerState> {
|
|
|
637
721
|
{!this.isSimple && (
|
|
638
722
|
<div
|
|
639
723
|
css={this.props.styles?.colorMixerButtonContainer}
|
|
640
|
-
style={{
|
|
724
|
+
style={{
|
|
725
|
+
alignSelf: this.state.labelHeight > 0 ? 'flex-start' : 'flex-end',
|
|
726
|
+
paddingTop: this.state.labelHeight
|
|
727
|
+
}}
|
|
641
728
|
>
|
|
642
729
|
<div css={this.props.styles?.colorMixerButtonWrapper}>
|
|
643
730
|
{this.renderPopover()}
|
package/src/ColorPicker/props.ts
CHANGED