@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.
Files changed (62) hide show
  1. package/build/cjs/multi-select/multi-select.js +8 -0
  2. package/build/cjs/multi-select/multi-select.prod.stories.js +40 -1
  3. package/build/cjs/multi-select-field/multi-select-field.js +8 -0
  4. package/build/cjs/multi-select-field/multi-select-field.prod.stories.js +42 -2
  5. package/build/cjs/select/menu-wrapper.js +17 -6
  6. package/build/cjs/select/select.js +18 -12
  7. package/build/cjs/simple-single-select/__stories__/WithMenuMaxWidth.js +34 -0
  8. package/build/cjs/simple-single-select/__stories__/WithMenuMinWidth.js +37 -0
  9. package/build/cjs/simple-single-select/menu/menu.js +17 -8
  10. package/build/cjs/simple-single-select/simple-single-select.js +19 -9
  11. package/build/cjs/simple-single-select/simple-single-select.prod.stories.js +14 -0
  12. package/build/cjs/simple-single-select/simple-single-select.test.js +62 -0
  13. package/build/cjs/simple-single-select-field/simple-single-select-field.js +4 -0
  14. package/build/cjs/simple-single-select-field/simple-single-select-field.prod.stories.js +53 -2
  15. package/build/cjs/simple-single-select-field/simple-single-select-field.test.js +4 -0
  16. package/build/cjs/single-select/single-select.js +8 -0
  17. package/build/cjs/single-select/single-select.prod.stories.js +41 -2
  18. package/build/cjs/single-select-field/single-select-field.js +8 -0
  19. package/build/cjs/single-select-field/single-select-field.prod.stories.js +42 -2
  20. package/build/cjs/single-select-option/single-select-option.js +4 -4
  21. package/build/es/multi-select/multi-select.js +8 -0
  22. package/build/es/multi-select/multi-select.prod.stories.js +37 -0
  23. package/build/es/multi-select-field/multi-select-field.js +8 -0
  24. package/build/es/multi-select-field/multi-select-field.prod.stories.js +41 -1
  25. package/build/es/select/menu-wrapper.js +17 -6
  26. package/build/es/select/select.js +18 -12
  27. package/build/es/simple-single-select/__stories__/WithMenuMaxWidth.js +26 -0
  28. package/build/es/simple-single-select/__stories__/WithMenuMinWidth.js +29 -0
  29. package/build/es/simple-single-select/menu/menu.js +17 -8
  30. package/build/es/simple-single-select/simple-single-select.js +19 -9
  31. package/build/es/simple-single-select/simple-single-select.prod.stories.js +2 -0
  32. package/build/es/simple-single-select/simple-single-select.test.js +62 -0
  33. package/build/es/simple-single-select-field/simple-single-select-field.js +4 -0
  34. package/build/es/simple-single-select-field/simple-single-select-field.prod.stories.js +49 -0
  35. package/build/es/simple-single-select-field/simple-single-select-field.test.js +4 -0
  36. package/build/es/single-select/single-select.js +8 -0
  37. package/build/es/single-select/single-select.prod.stories.js +38 -1
  38. package/build/es/single-select-field/single-select-field.js +8 -0
  39. package/build/es/single-select-field/single-select-field.prod.stories.js +41 -1
  40. package/build/es/single-select-option/single-select-option.js +4 -4
  41. package/package.json +15 -15
  42. package/src/multi-select/multi-select.js +8 -0
  43. package/src/multi-select/multi-select.prod.stories.js +26 -0
  44. package/src/multi-select-field/multi-select-field.js +8 -0
  45. package/src/multi-select-field/multi-select-field.prod.stories.js +31 -0
  46. package/src/select/menu-wrapper.js +19 -3
  47. package/src/select/select.js +18 -12
  48. package/src/simple-single-select/__stories__/WithMenuMaxWidth.js +24 -0
  49. package/src/simple-single-select/__stories__/WithMenuMinWidth.js +25 -0
  50. package/src/simple-single-select/menu/menu.js +26 -8
  51. package/src/simple-single-select/simple-single-select.js +19 -6
  52. package/src/simple-single-select/simple-single-select.prod.stories.js +2 -0
  53. package/src/simple-single-select/simple-single-select.test.js +68 -0
  54. package/src/simple-single-select-field/simple-single-select-field.js +6 -0
  55. package/src/simple-single-select-field/simple-single-select-field.prod.stories.js +49 -0
  56. package/src/simple-single-select-field/simple-single-select-field.test.js +4 -0
  57. package/src/single-select/single-select.js +8 -0
  58. package/src/single-select/single-select.prod.stories.js +26 -0
  59. package/src/single-select-field/single-select-field.js +8 -0
  60. package/src/single-select-field/single-select-field.prod.stories.js +31 -0
  61. package/src/single-select-option/single-select-option.js +5 -3
  62. 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
- menuWidth,
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: ${menuWidth};
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
- menuWidth: PropTypes.string.isRequired,
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
 
@@ -15,7 +15,7 @@ const DOWN_KEY = 40
15
15
  export class Select extends Component {
16
16
  state = {
17
17
  open: false,
18
- menuWidth: 'auto',
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({ menuWidth: this.getMenuWidth() })
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 menuWidth = this.getMenuWidth()
51
+ const inputWidth = this.getInputWidth()
52
52
 
53
- if (this.state.menuWidth !== menuWidth) {
54
- this.setState({ menuWidth })
53
+ if (this.state.inputWidth !== inputWidth) {
54
+ this.setState({ inputWidth })
55
55
  }
56
56
  }, 50)
57
57
 
58
- getMenuWidth() {
58
+ getInputWidth() {
59
59
  const { offsetWidth } = this.inputRef.current
60
- const { menuWidth } = this.state
60
+ const { inputWidth } = this.state
61
61
 
62
- if (offsetWidth && `${offsetWidth}px` !== menuWidth) {
62
+ if (offsetWidth && `${offsetWidth}px` !== inputWidth) {
63
63
  return `${offsetWidth}px`
64
64
  }
65
- return menuWidth
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
- menuWidth: this.getMenuWidth(),
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, menuWidth } = this.state
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
- menuWidth={menuWidth}
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 [menuWidth, setWidth] = useState('auto')
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
- const callback = () => setWidth(`${selectRef.offsetWidth}px`)
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 className="menu" style={{ width: menuWidth, maxHeight }}>
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 = { label: '', value: '' },
87
+ selected = defaultSelected,
83
88
  tabIndex = '0',
84
89
  valid = false,
85
90
  warning = false,
86
- onBlur = () => undefined,
87
- onClear = () => undefined,
88
- onEndReached = () => undefined,
89
- onFilterChange = () => undefined,
90
- onFocus = () => undefined,
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: ${active
67
- ? `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNjMyMDkgNy44MzIxNEMzLjI3MjA5IDcuNDcyMTQgMi43MjAwOSA3LjQ3MjE0IDIuMzYwMDkgNy44MzIxNEMyLjAwMDA5IDguMTkyMTQgMi4wMDAwOSA4Ljc0NDE0IDIuMzYwMDkgOS4xMDQxNEw1LjYyNDA5IDEyLjM2ODFDNS45ODQwOSAxMi43MjgxIDYuNTM2MDkgMTIuNzI4MSA2Ljg5NjA5IDEyLjM2ODFMMTQuNjQ4MSA0LjY0MDE0QzE1LjAwODEgNC4yODAxNCAxNS4wMDgxIDMuNzI4MTQgMTQuNjQ4MSAzLjM2ODE0QzE0LjI4ODEgMy4wMDgxNCAxMy43MzYxIDMuMDA4MTQgMTMuMzUyMSAzLjM2ODE0TDYuMjcyMDkgMTAuNDcyMUwzLjYzMjA5IDcuODMyMTRaIiBmaWxsPSIjMDA2OTVDIi8+Cjwvc3ZnPgo=')`
68
- : 'none'};
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>