@dhis2-ui/select 10.16.5 → 10.18.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/build/cjs/multi-select/multi-select.js +8 -0
- package/build/cjs/multi-select/multi-select.prod.stories.js +40 -1
- package/build/cjs/multi-select-field/multi-select-field.js +8 -0
- package/build/cjs/multi-select-field/multi-select-field.prod.stories.js +42 -2
- package/build/cjs/select/menu-wrapper.js +17 -6
- package/build/cjs/select/select.js +18 -12
- package/build/cjs/simple-single-select/__stories__/WithMenuMaxWidth.js +34 -0
- package/build/cjs/simple-single-select/__stories__/WithMenuMinWidth.js +37 -0
- package/build/cjs/simple-single-select/menu/menu.js +17 -8
- package/build/cjs/simple-single-select/simple-single-select.js +19 -9
- package/build/cjs/simple-single-select/simple-single-select.prod.stories.js +14 -0
- package/build/cjs/simple-single-select/simple-single-select.test.js +62 -0
- package/build/cjs/simple-single-select-field/simple-single-select-field.js +4 -0
- package/build/cjs/simple-single-select-field/simple-single-select-field.prod.stories.js +53 -2
- package/build/cjs/simple-single-select-field/simple-single-select-field.test.js +4 -0
- package/build/cjs/single-select/single-select.js +8 -0
- package/build/cjs/single-select/single-select.prod.stories.js +41 -2
- package/build/cjs/single-select-field/single-select-field.js +8 -0
- package/build/cjs/single-select-field/single-select-field.prod.stories.js +42 -2
- package/build/cjs/single-select-option/single-select-option.js +4 -4
- package/build/es/multi-select/multi-select.js +8 -0
- package/build/es/multi-select/multi-select.prod.stories.js +37 -0
- package/build/es/multi-select-field/multi-select-field.js +8 -0
- package/build/es/multi-select-field/multi-select-field.prod.stories.js +41 -1
- package/build/es/select/menu-wrapper.js +17 -6
- package/build/es/select/select.js +18 -12
- package/build/es/simple-single-select/__stories__/WithMenuMaxWidth.js +26 -0
- package/build/es/simple-single-select/__stories__/WithMenuMinWidth.js +29 -0
- package/build/es/simple-single-select/menu/menu.js +17 -8
- package/build/es/simple-single-select/simple-single-select.js +19 -9
- package/build/es/simple-single-select/simple-single-select.prod.stories.js +2 -0
- package/build/es/simple-single-select/simple-single-select.test.js +62 -0
- package/build/es/simple-single-select-field/simple-single-select-field.js +4 -0
- package/build/es/simple-single-select-field/simple-single-select-field.prod.stories.js +49 -0
- package/build/es/simple-single-select-field/simple-single-select-field.test.js +4 -0
- package/build/es/single-select/single-select.js +8 -0
- package/build/es/single-select/single-select.prod.stories.js +38 -1
- package/build/es/single-select-field/single-select-field.js +8 -0
- package/build/es/single-select-field/single-select-field.prod.stories.js +41 -1
- package/build/es/single-select-option/single-select-option.js +4 -4
- package/package.json +15 -15
- package/src/multi-select/multi-select.js +8 -0
- package/src/multi-select/multi-select.prod.stories.js +26 -0
- package/src/multi-select-field/multi-select-field.js +8 -0
- package/src/multi-select-field/multi-select-field.prod.stories.js +31 -0
- package/src/select/menu-wrapper.js +19 -3
- package/src/select/select.js +18 -12
- package/src/simple-single-select/__stories__/WithMenuMaxWidth.js +24 -0
- package/src/simple-single-select/__stories__/WithMenuMinWidth.js +25 -0
- package/src/simple-single-select/menu/menu.js +26 -8
- package/src/simple-single-select/simple-single-select.js +19 -6
- package/src/simple-single-select/simple-single-select.prod.stories.js +2 -0
- package/src/simple-single-select/simple-single-select.test.js +68 -0
- package/src/simple-single-select-field/simple-single-select-field.js +6 -0
- package/src/simple-single-select-field/simple-single-select-field.prod.stories.js +49 -0
- package/src/simple-single-select-field/simple-single-select-field.test.js +4 -0
- package/src/single-select/single-select.js +8 -0
- package/src/single-select/single-select.prod.stories.js +26 -0
- package/src/single-select-field/single-select-field.js +8 -0
- package/src/single-select-field/single-select-field.prod.stories.js +31 -0
- package/src/single-select-option/single-select-option.js +5 -3
- package/types/index.d.ts +45 -1
|
@@ -7,11 +7,23 @@ import React from 'react'
|
|
|
7
7
|
const MenuWrapper = ({
|
|
8
8
|
children,
|
|
9
9
|
dataTest,
|
|
10
|
+
inputWidth,
|
|
10
11
|
maxHeight = '280px',
|
|
11
|
-
|
|
12
|
+
menuMaxWidth,
|
|
13
|
+
menuMinWidth,
|
|
12
14
|
onClick,
|
|
13
15
|
selectRef,
|
|
14
16
|
}) => {
|
|
17
|
+
// menuMinWidth or menuMaxWidth enables flexible sizing (fit-content), with
|
|
18
|
+
// min-width = max(input, menuMinWidth). Without them, width matches the input.
|
|
19
|
+
const flexible = menuMinWidth || menuMaxWidth
|
|
20
|
+
const width = flexible ? 'fit-content' : inputWidth
|
|
21
|
+
const flexibleMinWidth = menuMinWidth
|
|
22
|
+
? `max(${inputWidth}, ${menuMinWidth})`
|
|
23
|
+
: inputWidth
|
|
24
|
+
const minWidth = flexible ? flexibleMinWidth : 'auto'
|
|
25
|
+
const maxWidth = menuMaxWidth || 'none'
|
|
26
|
+
|
|
15
27
|
return (
|
|
16
28
|
<Layer onBackdropClick={onClick} transparent>
|
|
17
29
|
<Popper
|
|
@@ -24,7 +36,9 @@ const MenuWrapper = ({
|
|
|
24
36
|
|
|
25
37
|
<style jsx>{`
|
|
26
38
|
div {
|
|
27
|
-
width: ${
|
|
39
|
+
width: ${width};
|
|
40
|
+
min-width: ${minWidth};
|
|
41
|
+
max-width: ${maxWidth};
|
|
28
42
|
height: auto;
|
|
29
43
|
max-height: ${maxHeight};
|
|
30
44
|
overflow: auto;
|
|
@@ -42,10 +56,12 @@ const MenuWrapper = ({
|
|
|
42
56
|
|
|
43
57
|
MenuWrapper.propTypes = {
|
|
44
58
|
dataTest: PropTypes.string.isRequired,
|
|
45
|
-
|
|
59
|
+
inputWidth: PropTypes.string.isRequired,
|
|
46
60
|
selectRef: PropTypes.object.isRequired,
|
|
47
61
|
children: PropTypes.node,
|
|
48
62
|
maxHeight: PropTypes.string,
|
|
63
|
+
menuMaxWidth: PropTypes.string,
|
|
64
|
+
menuMinWidth: PropTypes.string,
|
|
49
65
|
onClick: PropTypes.func,
|
|
50
66
|
}
|
|
51
67
|
|
package/src/select/select.js
CHANGED
|
@@ -15,7 +15,7 @@ const DOWN_KEY = 40
|
|
|
15
15
|
export class Select extends Component {
|
|
16
16
|
state = {
|
|
17
17
|
open: false,
|
|
18
|
-
|
|
18
|
+
inputWidth: 'auto',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
static defaultProps = {
|
|
@@ -30,7 +30,7 @@ export class Select extends Component {
|
|
|
30
30
|
this.inputRef.current.focus()
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
this.setState({
|
|
33
|
+
this.setState({ inputWidth: this.getInputWidth() })
|
|
34
34
|
window.addEventListener('resize', this.onResize)
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -48,21 +48,21 @@ export class Select extends Component {
|
|
|
48
48
|
* See: https://nolanlawson.com/2018/09/25/accurately-measuring-layout-on-the-web
|
|
49
49
|
*/
|
|
50
50
|
onResize = debounce(() => {
|
|
51
|
-
const
|
|
51
|
+
const inputWidth = this.getInputWidth()
|
|
52
52
|
|
|
53
|
-
if (this.state.
|
|
54
|
-
this.setState({
|
|
53
|
+
if (this.state.inputWidth !== inputWidth) {
|
|
54
|
+
this.setState({ inputWidth })
|
|
55
55
|
}
|
|
56
56
|
}, 50)
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
getInputWidth() {
|
|
59
59
|
const { offsetWidth } = this.inputRef.current
|
|
60
|
-
const {
|
|
60
|
+
const { inputWidth } = this.state
|
|
61
61
|
|
|
62
|
-
if (offsetWidth && `${offsetWidth}px` !==
|
|
62
|
+
if (offsetWidth && `${offsetWidth}px` !== inputWidth) {
|
|
63
63
|
return `${offsetWidth}px`
|
|
64
64
|
}
|
|
65
|
-
return
|
|
65
|
+
return inputWidth
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
handleFocusInput = () => {
|
|
@@ -82,7 +82,7 @@ export class Select extends Component {
|
|
|
82
82
|
handleOpen = () => {
|
|
83
83
|
this.setState({
|
|
84
84
|
open: true,
|
|
85
|
-
|
|
85
|
+
inputWidth: this.getInputWidth(),
|
|
86
86
|
})
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -159,7 +159,7 @@ export class Select extends Component {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
render() {
|
|
162
|
-
const { open,
|
|
162
|
+
const { open, inputWidth } = this.state
|
|
163
163
|
const {
|
|
164
164
|
children,
|
|
165
165
|
className,
|
|
@@ -167,6 +167,8 @@ export class Select extends Component {
|
|
|
167
167
|
onChange,
|
|
168
168
|
tabIndex,
|
|
169
169
|
maxHeight,
|
|
170
|
+
menuMinWidth,
|
|
171
|
+
menuMaxWidth,
|
|
170
172
|
error,
|
|
171
173
|
warning,
|
|
172
174
|
valid,
|
|
@@ -220,7 +222,9 @@ export class Select extends Component {
|
|
|
220
222
|
onClick={this.onOutsideClick}
|
|
221
223
|
maxHeight={maxHeight}
|
|
222
224
|
selectRef={this.selectRef}
|
|
223
|
-
|
|
225
|
+
inputWidth={inputWidth}
|
|
226
|
+
menuMinWidth={menuMinWidth}
|
|
227
|
+
menuMaxWidth={menuMaxWidth}
|
|
224
228
|
dataTest={`${dataTest}-menu`}
|
|
225
229
|
>
|
|
226
230
|
{menu}
|
|
@@ -246,6 +250,8 @@ Select.propTypes = {
|
|
|
246
250
|
error: sharedPropTypes.statusPropType,
|
|
247
251
|
initialFocus: PropTypes.bool,
|
|
248
252
|
maxHeight: PropTypes.string,
|
|
253
|
+
menuMaxWidth: PropTypes.string,
|
|
254
|
+
menuMinWidth: PropTypes.string,
|
|
249
255
|
tabIndex: PropTypes.string,
|
|
250
256
|
valid: sharedPropTypes.statusPropType,
|
|
251
257
|
warning: sharedPropTypes.statusPropType,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { SimpleSingleSelect } from '../simple-single-select.js'
|
|
3
|
+
|
|
4
|
+
const options = [
|
|
5
|
+
{ value: '1', label: 'option one' },
|
|
6
|
+
{ value: '2', label: 'option two' },
|
|
7
|
+
{ value: '3', label: 'A much longer option label that gets clamped' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export const WithMenuMaxWidth = () => {
|
|
11
|
+
const [selected, setSelected] = useState(null)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div style={{ width: 120 }}>
|
|
15
|
+
<SimpleSingleSelect
|
|
16
|
+
name="simple"
|
|
17
|
+
selected={selected}
|
|
18
|
+
onChange={setSelected}
|
|
19
|
+
options={options}
|
|
20
|
+
menuMaxWidth="200px"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { SimpleSingleSelect } from '../simple-single-select.js'
|
|
3
|
+
|
|
4
|
+
const options = [
|
|
5
|
+
{ value: '1', label: 'option one' },
|
|
6
|
+
{ value: '2', label: 'option two' },
|
|
7
|
+
{ value: '3', label: 'option three' },
|
|
8
|
+
{ value: '4', label: 'A longer option that exceeds the minimum' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export const WithMenuMinWidth = () => {
|
|
12
|
+
const [selected, setSelected] = useState(null)
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div style={{ width: 120 }}>
|
|
16
|
+
<SimpleSingleSelect
|
|
17
|
+
name="simple"
|
|
18
|
+
selected={selected}
|
|
19
|
+
onChange={setSelected}
|
|
20
|
+
options={options}
|
|
21
|
+
menuMinWidth="240px"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -26,6 +26,8 @@ export function Menu({
|
|
|
26
26
|
loading,
|
|
27
27
|
loadingText,
|
|
28
28
|
maxHeight,
|
|
29
|
+
maxWidth,
|
|
30
|
+
minWidth,
|
|
29
31
|
noMatchText,
|
|
30
32
|
optionUpdateStrategy,
|
|
31
33
|
selectRef,
|
|
@@ -34,18 +36,16 @@ export function Menu({
|
|
|
34
36
|
onClose,
|
|
35
37
|
onEndReached,
|
|
36
38
|
}) {
|
|
37
|
-
const [
|
|
39
|
+
const [selectWidth, setSelectWidth] = useState()
|
|
38
40
|
const dataTestPrefix = `${dataTest}-menu`
|
|
39
41
|
|
|
42
|
+
// Re-measuring whenever `hidden` changes keeps the width current when the
|
|
43
|
+
// select's container has been resized since the menu was last opened
|
|
40
44
|
useEffect(() => {
|
|
41
45
|
if (selectRef) {
|
|
42
|
-
|
|
43
|
-
callback() // We want to know the width as soon as the
|
|
44
|
-
|
|
45
|
-
selectRef.addEventListener('resize', callback)
|
|
46
|
-
return () => selectRef.removeEventListener('resize', callback)
|
|
46
|
+
setSelectWidth(`${selectRef.offsetWidth}px`)
|
|
47
47
|
}
|
|
48
|
-
}, [selectRef])
|
|
48
|
+
}, [selectRef, hidden])
|
|
49
49
|
|
|
50
50
|
if (hidden) {
|
|
51
51
|
return null
|
|
@@ -60,6 +60,14 @@ export function Menu({
|
|
|
60
60
|
|
|
61
61
|
const isEmpty = !options.length && !filterValue
|
|
62
62
|
|
|
63
|
+
const flexible = Boolean(minWidth || maxWidth)
|
|
64
|
+
// We never want the menu narrower than the select, so a maxWidth below
|
|
65
|
+
// the select's width intentionally has no effect
|
|
66
|
+
const flexibleMinWidth =
|
|
67
|
+
minWidth && selectWidth
|
|
68
|
+
? `max(${selectWidth}, ${minWidth})`
|
|
69
|
+
: minWidth || selectWidth
|
|
70
|
+
|
|
63
71
|
return (
|
|
64
72
|
<Layer onBackdropClick={onClose} transparent>
|
|
65
73
|
<Popper
|
|
@@ -67,7 +75,15 @@ export function Menu({
|
|
|
67
75
|
placement="bottom-start"
|
|
68
76
|
observeReferenceResize
|
|
69
77
|
>
|
|
70
|
-
<div
|
|
78
|
+
<div
|
|
79
|
+
className="menu"
|
|
80
|
+
style={{
|
|
81
|
+
width: flexible ? 'fit-content' : selectWidth,
|
|
82
|
+
minWidth: flexible ? flexibleMinWidth : undefined,
|
|
83
|
+
maxWidth,
|
|
84
|
+
maxHeight,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
71
87
|
{isEmpty && <Empty>{empty}</Empty>}
|
|
72
88
|
|
|
73
89
|
{hasNoFilterMatch && <NoMatch>{noMatchText}</NoMatch>}
|
|
@@ -160,6 +176,8 @@ Menu.propTypes = {
|
|
|
160
176
|
loading: PropTypes.bool,
|
|
161
177
|
loadingText: PropTypes.string,
|
|
162
178
|
maxHeight: PropTypes.string,
|
|
179
|
+
maxWidth: PropTypes.string,
|
|
180
|
+
minWidth: PropTypes.string,
|
|
163
181
|
noMatchText: PropTypes.string,
|
|
164
182
|
optionComponent: PropTypes.elementType,
|
|
165
183
|
optionUpdateStrategy: PropTypes.oneOf(['off', 'polite', 'assertive']),
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
useHandleKeyPressOnFilterInput,
|
|
13
13
|
} from './use-handle-key-press/index.js'
|
|
14
14
|
|
|
15
|
+
const defaultSelected = { label: '', value: '' }
|
|
16
|
+
const noop = () => undefined
|
|
17
|
+
|
|
15
18
|
function useFocussedOptionIndex({ filterable, filterValue, options }) {
|
|
16
19
|
const [defaultFocussedOptionIndex, setDefaultFocussedOptionIndex] =
|
|
17
20
|
useState(0)
|
|
@@ -75,19 +78,21 @@ export function SimpleSingleSelect({
|
|
|
75
78
|
loading = false,
|
|
76
79
|
menuLoadingText: _menuLoadingText = '',
|
|
77
80
|
menuMaxHeight = '288px',
|
|
81
|
+
menuMaxWidth,
|
|
82
|
+
menuMinWidth,
|
|
78
83
|
noMatchText: _noMatchText = '',
|
|
79
84
|
optionUpdateStrategy = 'polite',
|
|
80
85
|
placeholder = '',
|
|
81
86
|
prefix = '',
|
|
82
|
-
selected =
|
|
87
|
+
selected = defaultSelected,
|
|
83
88
|
tabIndex = '0',
|
|
84
89
|
valid = false,
|
|
85
90
|
warning = false,
|
|
86
|
-
onBlur =
|
|
87
|
-
onClear =
|
|
88
|
-
onEndReached =
|
|
89
|
-
onFilterChange =
|
|
90
|
-
onFocus =
|
|
91
|
+
onBlur = noop,
|
|
92
|
+
onClear = noop,
|
|
93
|
+
onEndReached = noop,
|
|
94
|
+
onFilterChange = noop,
|
|
95
|
+
onFocus = noop,
|
|
91
96
|
}) {
|
|
92
97
|
// We can't translate with default props as the translation function would
|
|
93
98
|
// be called before we get any translations from the consuming app/lib
|
|
@@ -261,6 +266,8 @@ export function SimpleSingleSelect({
|
|
|
261
266
|
loading={loading}
|
|
262
267
|
loadingText={menuLoadingText}
|
|
263
268
|
maxHeight={menuMaxHeight}
|
|
269
|
+
maxWidth={menuMaxWidth}
|
|
270
|
+
minWidth={menuMinWidth}
|
|
264
271
|
noMatchText={noMatchText}
|
|
265
272
|
optionUpdateStrategy={optionUpdateStrategy}
|
|
266
273
|
options={options}
|
|
@@ -342,6 +349,12 @@ SimpleSingleSelect.propTypes = {
|
|
|
342
349
|
/** Allows to modify the max height of the menu **/
|
|
343
350
|
menuMaxHeight: PropTypes.string,
|
|
344
351
|
|
|
352
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
|
|
353
|
+
menuMaxWidth: PropTypes.string,
|
|
354
|
+
|
|
355
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
|
|
356
|
+
menuMinWidth: PropTypes.string,
|
|
357
|
+
|
|
345
358
|
/** String that will be displayed when the select is being filtered but the options array is empty **/
|
|
346
359
|
noMatchText: requiredIf((props) => props.filterable, PropTypes.string),
|
|
347
360
|
|
|
@@ -22,6 +22,8 @@ export { WithOptionsAndLoadingText } from './__stories__/WithOptionsAndLoadingTe
|
|
|
22
22
|
export { WithoutOptionsAndLoading } from './__stories__/WithoutOptionsAndLoading.js'
|
|
23
23
|
export { WithManyOptions } from './__stories__/WithManyOptions.js'
|
|
24
24
|
export { WithCustomLowMaxHeight } from './__stories__/WithCustomLowMaxHeight.js'
|
|
25
|
+
export { WithMenuMinWidth } from './__stories__/WithMenuMinWidth.js'
|
|
26
|
+
export { WithMenuMaxWidth } from './__stories__/WithMenuMaxWidth.js'
|
|
25
27
|
export { WithOptionsAndDisabled } from './__stories__/WithOptionsAndDisabled.js'
|
|
26
28
|
export { WithSelectionAndDisabled } from './__stories__/WithSelectionAndDisabled.js'
|
|
27
29
|
export { WithPrefix } from './__stories__/WithPrefix.js'
|
|
@@ -150,6 +150,74 @@ describe('<SimpleSingleSelect />', () => {
|
|
|
150
150
|
expect(menu.style.maxHeight).toBe('100px')
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
+
describe('dropdown menu width', () => {
|
|
154
|
+
let offsetWidth
|
|
155
|
+
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
// The menu's width is derived from the select's measured width,
|
|
158
|
+
// which is always 0 in jsdom
|
|
159
|
+
offsetWidth = jest
|
|
160
|
+
.spyOn(HTMLElement.prototype, 'offsetWidth', 'get')
|
|
161
|
+
.mockReturnValue(120)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
offsetWidth.mockRestore()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const renderAndOpen = (props) => {
|
|
169
|
+
render(
|
|
170
|
+
<SimpleSingleSelect
|
|
171
|
+
name="simple"
|
|
172
|
+
selected={{ value: 'foo', label: 'Foo' }}
|
|
173
|
+
onChange={() => null}
|
|
174
|
+
options={[{ value: 'foo', label: 'Foo' }]}
|
|
175
|
+
{...props}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
fireEvent.click(screen.getByRole('combobox'))
|
|
180
|
+
|
|
181
|
+
const listbox = screen.getByRole('listbox')
|
|
182
|
+
return listbox.parentNode.parentNode.parentNode
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
it('should match the width of the select by default', () => {
|
|
186
|
+
const menu = renderAndOpen()
|
|
187
|
+
|
|
188
|
+
expect(menu.style.width).toBe('120px')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should not be narrower than menuMinWidth or the select', () => {
|
|
192
|
+
const menu = renderAndOpen({ menuMinWidth: '240px' })
|
|
193
|
+
|
|
194
|
+
expect(menu.style.minWidth).toBe('max(120px, 240px)')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should not be wider than menuMaxWidth', () => {
|
|
198
|
+
const menu = renderAndOpen({ menuMaxWidth: '200px' })
|
|
199
|
+
|
|
200
|
+
expect(menu.style.maxWidth).toBe('200px')
|
|
201
|
+
expect(menu.style.minWidth).toBe('120px')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should re-measure the select when the menu is reopened', () => {
|
|
205
|
+
renderAndOpen({ menuMinWidth: '240px' })
|
|
206
|
+
const comboBox = screen.getByRole('combobox')
|
|
207
|
+
|
|
208
|
+
// close the menu
|
|
209
|
+
fireEvent.click(comboBox)
|
|
210
|
+
|
|
211
|
+
// widen the select while the menu is closed, then reopen it
|
|
212
|
+
offsetWidth.mockReturnValue(300)
|
|
213
|
+
fireEvent.click(comboBox)
|
|
214
|
+
|
|
215
|
+
const listbox = screen.getByRole('listbox')
|
|
216
|
+
const menu = listbox.parentNode.parentNode.parentNode
|
|
217
|
+
expect(menu.style.minWidth).toBe('max(300px, 240px)')
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
153
221
|
it('should accept a placeholder', () => {
|
|
154
222
|
render(
|
|
155
223
|
<SimpleSingleSelect
|
|
@@ -137,6 +137,12 @@ SimpleSingleSelectField.propTypes = {
|
|
|
137
137
|
/** Allows to modify the max height of the menu **/
|
|
138
138
|
menuMaxHeight: PropTypes.string,
|
|
139
139
|
|
|
140
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
|
|
141
|
+
menuMaxWidth: PropTypes.string,
|
|
142
|
+
|
|
143
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) **/
|
|
144
|
+
menuMinWidth: PropTypes.string,
|
|
145
|
+
|
|
140
146
|
/** String that will be displayed when the select is being filtered but the options array is empty **/
|
|
141
147
|
noMatchText: requiredIf((props) => props.filterable, PropTypes.string),
|
|
142
148
|
|
|
@@ -26,6 +26,13 @@ const options = [
|
|
|
26
26
|
{ value: '10', label: 'ten' },
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
const longOptions = [
|
|
30
|
+
{ value: '1', label: 'option one' },
|
|
31
|
+
{ value: '2', label: 'option two' },
|
|
32
|
+
{ value: '3', label: 'option three' },
|
|
33
|
+
{ value: '4', label: 'A longer option that exceeds the minimum' },
|
|
34
|
+
]
|
|
35
|
+
|
|
29
36
|
export default {
|
|
30
37
|
title: 'SimpleSingleSelectField',
|
|
31
38
|
component: SimpleSingleSelectField,
|
|
@@ -186,3 +193,45 @@ export const InputWidth = () => {
|
|
|
186
193
|
</div>
|
|
187
194
|
)
|
|
188
195
|
}
|
|
196
|
+
|
|
197
|
+
export const WithMenuMinWidth = () => {
|
|
198
|
+
const [value, setValue] = useState('')
|
|
199
|
+
const valueLabel = value
|
|
200
|
+
? longOptions.find((option) => option.value === value)?.label
|
|
201
|
+
: ''
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div style={{ width: 120 }}>
|
|
205
|
+
<SimpleSingleSelectField
|
|
206
|
+
name="simple"
|
|
207
|
+
label="This is the label"
|
|
208
|
+
value={value}
|
|
209
|
+
valueLabel={valueLabel}
|
|
210
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
211
|
+
options={longOptions}
|
|
212
|
+
menuMinWidth="240px"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const WithMenuMaxWidth = () => {
|
|
219
|
+
const [value, setValue] = useState('')
|
|
220
|
+
const valueLabel = value
|
|
221
|
+
? longOptions.find((option) => option.value === value)?.label
|
|
222
|
+
: ''
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<div style={{ width: 120 }}>
|
|
226
|
+
<SimpleSingleSelectField
|
|
227
|
+
name="simple"
|
|
228
|
+
label="This is the label"
|
|
229
|
+
value={value}
|
|
230
|
+
valueLabel={valueLabel}
|
|
231
|
+
onChange={(nextValue) => setValue(nextValue)}
|
|
232
|
+
options={longOptions}
|
|
233
|
+
menuMaxWidth="200px"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -52,6 +52,8 @@ describe('<SimpleSingleSelectField />', () => {
|
|
|
52
52
|
loading={false}
|
|
53
53
|
menuLoadingText=""
|
|
54
54
|
menuMaxHeight=""
|
|
55
|
+
menuMaxWidth="400px"
|
|
56
|
+
menuMinWidth="240px"
|
|
55
57
|
noMatchText=""
|
|
56
58
|
optionUpdateStrategy="off"
|
|
57
59
|
placeholder=""
|
|
@@ -91,6 +93,8 @@ describe('<SimpleSingleSelectField />', () => {
|
|
|
91
93
|
expect(SimpleSingleSelect.mock.calls[0][0].loading).toBe(false)
|
|
92
94
|
expect(SimpleSingleSelect.mock.calls[0][0].menuLoadingText).toBe('')
|
|
93
95
|
expect(SimpleSingleSelect.mock.calls[0][0].menuMaxHeight).toBe('')
|
|
96
|
+
expect(SimpleSingleSelect.mock.calls[0][0].menuMaxWidth).toBe('400px')
|
|
97
|
+
expect(SimpleSingleSelect.mock.calls[0][0].menuMinWidth).toBe('240px')
|
|
94
98
|
expect(SimpleSingleSelect.mock.calls[0][0].noMatchText).toBe('')
|
|
95
99
|
expect(SimpleSingleSelect.mock.calls[0][0].optionUpdateStrategy).toBe(
|
|
96
100
|
'off'
|
|
@@ -13,6 +13,8 @@ const SingleSelect = ({
|
|
|
13
13
|
selected = '',
|
|
14
14
|
tabIndex,
|
|
15
15
|
maxHeight,
|
|
16
|
+
menuMinWidth,
|
|
17
|
+
menuMaxWidth,
|
|
16
18
|
inputMaxHeight,
|
|
17
19
|
onChange,
|
|
18
20
|
onFocus,
|
|
@@ -68,6 +70,8 @@ const SingleSelect = ({
|
|
|
68
70
|
menu={menu}
|
|
69
71
|
tabIndex={tabIndex}
|
|
70
72
|
maxHeight={maxHeight}
|
|
73
|
+
menuMinWidth={menuMinWidth}
|
|
74
|
+
menuMaxWidth={menuMaxWidth}
|
|
71
75
|
onChange={onChange}
|
|
72
76
|
onFocus={onFocus}
|
|
73
77
|
onKeyDown={onKeyDown}
|
|
@@ -127,6 +131,10 @@ SingleSelect.propTypes = {
|
|
|
127
131
|
loading: PropTypes.bool,
|
|
128
132
|
loadingText: PropTypes.string,
|
|
129
133
|
maxHeight: PropTypes.string,
|
|
134
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) */
|
|
135
|
+
menuMaxWidth: PropTypes.string,
|
|
136
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) */
|
|
137
|
+
menuMinWidth: PropTypes.string,
|
|
130
138
|
/** Text to show when filter returns no results. Required if `filterable` prop is true */
|
|
131
139
|
noMatchText: requiredIf((props) => props.filterable, PropTypes.string),
|
|
132
140
|
placeholder: PropTypes.string,
|
|
@@ -356,3 +356,29 @@ export const ShiftedIntoView = (args) => (
|
|
|
356
356
|
`}</style>
|
|
357
357
|
</>
|
|
358
358
|
)
|
|
359
|
+
|
|
360
|
+
export const WithMenuMaxWidth = (args) => (
|
|
361
|
+
<div style={{ width: 120 }}>
|
|
362
|
+
<SingleSelect {...args}>
|
|
363
|
+
<SingleSelectOption value="1" label="option one" />
|
|
364
|
+
<SingleSelectOption value="2" label="option two" />
|
|
365
|
+
<SingleSelectOption value="3" label="A much longer option label" />
|
|
366
|
+
</SingleSelect>
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
WithMenuMaxWidth.args = { menuMaxWidth: '200px' }
|
|
370
|
+
|
|
371
|
+
export const WithMenuMinWidth = (args) => (
|
|
372
|
+
<div style={{ width: 120 }}>
|
|
373
|
+
<SingleSelect {...args}>
|
|
374
|
+
<SingleSelectOption value="1" label="option one" />
|
|
375
|
+
<SingleSelectOption value="2" label="option two" />
|
|
376
|
+
<SingleSelectOption value="3" label="option three" />
|
|
377
|
+
<SingleSelectOption
|
|
378
|
+
value="4"
|
|
379
|
+
label="A longer option that exceeds the minimum"
|
|
380
|
+
/>
|
|
381
|
+
</SingleSelect>
|
|
382
|
+
</div>
|
|
383
|
+
)
|
|
384
|
+
WithMenuMinWidth.args = { menuMinWidth: '240px' }
|
|
@@ -45,6 +45,8 @@ class SingleSelectField extends React.Component {
|
|
|
45
45
|
helpText,
|
|
46
46
|
validationText,
|
|
47
47
|
maxHeight,
|
|
48
|
+
menuMinWidth,
|
|
49
|
+
menuMaxWidth,
|
|
48
50
|
inputMaxHeight,
|
|
49
51
|
inputWidth,
|
|
50
52
|
children,
|
|
@@ -81,6 +83,8 @@ class SingleSelectField extends React.Component {
|
|
|
81
83
|
selected={selected}
|
|
82
84
|
tabIndex={tabIndex}
|
|
83
85
|
maxHeight={maxHeight}
|
|
86
|
+
menuMinWidth={menuMinWidth}
|
|
87
|
+
menuMaxWidth={menuMaxWidth}
|
|
84
88
|
inputMaxHeight={inputMaxHeight}
|
|
85
89
|
onChange={onChange}
|
|
86
90
|
onFocus={onFocus}
|
|
@@ -148,6 +152,10 @@ SingleSelectField.propTypes = {
|
|
|
148
152
|
loadingText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
149
153
|
/** Constrains height of the SingleSelect */
|
|
150
154
|
maxHeight: PropTypes.string,
|
|
155
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) */
|
|
156
|
+
menuMaxWidth: PropTypes.string,
|
|
157
|
+
/** See [dropdown menu width](https://developers.dhis2.org/docs/ui/components/select#dropdown-menu-width) */
|
|
158
|
+
menuMinWidth: PropTypes.string,
|
|
151
159
|
/** Text to display when there are no filter results */
|
|
152
160
|
noMatchText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
|
153
161
|
/** Placeholder text when the SingleSelect is empty */
|
|
@@ -144,3 +144,34 @@ DefaultFilterPlaceholderAndNoMatchText.storyName =
|
|
|
144
144
|
export const DefaultLoadingText = Template.bind({})
|
|
145
145
|
DefaultLoadingText.args = { loading: true, ...DefaultEmpty.args }
|
|
146
146
|
DefaultLoadingText.storyName = 'Default: loadingText'
|
|
147
|
+
|
|
148
|
+
export const WithMenuMaxWidth = Template.bind({})
|
|
149
|
+
WithMenuMaxWidth.args = {
|
|
150
|
+
inputWidth: '120px',
|
|
151
|
+
menuMaxWidth: '200px',
|
|
152
|
+
children: [
|
|
153
|
+
<SingleSelectOption key="1" value="1" label="option one" />,
|
|
154
|
+
<SingleSelectOption key="2" value="2" label="option two" />,
|
|
155
|
+
<SingleSelectOption
|
|
156
|
+
key="3"
|
|
157
|
+
value="3"
|
|
158
|
+
label="A much longer option label"
|
|
159
|
+
/>,
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const WithMenuMinWidth = Template.bind({})
|
|
164
|
+
WithMenuMinWidth.args = {
|
|
165
|
+
inputWidth: '120px',
|
|
166
|
+
menuMinWidth: '240px',
|
|
167
|
+
children: [
|
|
168
|
+
<SingleSelectOption key="1" value="1" label="option one" />,
|
|
169
|
+
<SingleSelectOption key="2" value="2" label="option two" />,
|
|
170
|
+
<SingleSelectOption key="3" value="3" label="option three" />,
|
|
171
|
+
<SingleSelectOption
|
|
172
|
+
key="4"
|
|
173
|
+
value="4"
|
|
174
|
+
label="A longer option that exceeds the minimum"
|
|
175
|
+
/>,
|
|
176
|
+
],
|
|
177
|
+
}
|
|
@@ -63,9 +63,11 @@ const SingleSelectOption = ({
|
|
|
63
63
|
flex-shrink: 0;
|
|
64
64
|
background-size: contain;
|
|
65
65
|
background-repeat: no-repeat;
|
|
66
|
-
background-image: ${
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
background-image: ${
|
|
67
|
+
active
|
|
68
|
+
? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')`
|
|
69
|
+
: 'none'
|
|
70
|
+
};
|
|
69
71
|
}
|
|
70
72
|
`}</style>
|
|
71
73
|
</div>
|