diff --git a/components/select/src/multi-select-field/multi-select-field.js b/components/select/src/multi-select-field/multi-select-field.js index 55754af71..864a95bb6 100644 --- a/components/select/src/multi-select-field/multi-select-field.js +++ b/components/select/src/multi-select-field/multi-select-field.js @@ -46,6 +46,8 @@ class MultiSelectField extends React.Component { helpText, validationText, maxHeight, + menuMinWidth, + menuMaxWidth, inputMaxHeight, inputWidth, children, @@ -81,6 +83,8 @@ class MultiSelectField extends React.Component { selected={selected} tabIndex={tabIndex} maxHeight={maxHeight} + menuMinWidth={menuMinWidth} + menuMaxWidth={menuMaxWidth} inputMaxHeight={inputMaxHeight} onChange={onChange} onFocus={onFocus} @@ -148,6 +152,10 @@ MultiSelectField.propTypes = { loadingText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** Constrains height of the MultiSelect */ maxHeight: PropTypes.string, + /** Sets a maximum width for the dropdown menu */ + menuMaxWidth: PropTypes.string, + /** Sets a minimum width for the dropdown menu */ + menuMinWidth: PropTypes.string, /** Text to display when there are no filter results */ noMatchText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** Placeholder text when the MultiSelect is empty */ diff --git a/components/select/src/multi-select-field/multi-select-field.prod.stories.js b/components/select/src/multi-select-field/multi-select-field.prod.stories.js index 69869c2d3..b834964bb 100644 --- a/components/select/src/multi-select-field/multi-select-field.prod.stories.js +++ b/components/select/src/multi-select-field/multi-select-field.prod.stories.js @@ -135,3 +135,34 @@ DefaultFilterPlaceholderAndNoMatchText.storyName = export const DefaultLoadingText = Template.bind({}) DefaultLoadingText.args = { loading: true, ...DefaultEmpty.args } DefaultLoadingText.storyName = 'Default: loadingText' + +export const WithMenuMaxWidth = Template.bind({}) +WithMenuMaxWidth.args = { + inputWidth: '120px', + menuMaxWidth: '200px', + children: [ + , + , + , + ], +} + +export const WithMenuMinWidth = Template.bind({}) +WithMenuMinWidth.args = { + inputWidth: '120px', + menuMinWidth: '240px', + children: [ + , + , + , + , + ], +} diff --git a/components/select/src/multi-select/multi-select.js b/components/select/src/multi-select/multi-select.js index d4aa9fc42..6a4e3d353 100644 --- a/components/select/src/multi-select/multi-select.js +++ b/components/select/src/multi-select/multi-select.js @@ -15,6 +15,8 @@ const MultiSelect = ({ selected = staticArr, tabIndex, maxHeight, + menuMinWidth, + menuMaxWidth, inputMaxHeight, onChange, onFocus, @@ -72,6 +74,8 @@ const MultiSelect = ({ menu={menu} tabIndex={tabIndex} maxHeight={maxHeight} + menuMinWidth={menuMinWidth} + menuMaxWidth={menuMaxWidth} onChange={onChange} onFocus={onFocus} onKeyDown={onKeyDown} @@ -131,6 +135,10 @@ MultiSelect.propTypes = { loading: PropTypes.bool, loadingText: PropTypes.string, maxHeight: PropTypes.string, + /** Sets a maximum width for the dropdown menu */ + menuMaxWidth: PropTypes.string, + /** Sets a minimum width for the dropdown menu */ + menuMinWidth: PropTypes.string, /** Required if `filterable` prop is `true` */ noMatchText: requiredIf((props) => props.filterable, PropTypes.string), placeholder: PropTypes.string, diff --git a/components/select/src/multi-select/multi-select.prod.stories.js b/components/select/src/multi-select/multi-select.prod.stories.js index 7dd7c440e..95749aea4 100644 --- a/components/select/src/multi-select/multi-select.prod.stories.js +++ b/components/select/src/multi-select/multi-select.prod.stories.js @@ -406,3 +406,29 @@ export const RTL = (args) => { ) } RTL.args = { selected: ['1', '2'], prefix: 'RTL text' } + +export const WithMenuMaxWidth = (args) => ( +
+ + + + + +
+) +WithMenuMaxWidth.args = { menuMaxWidth: '200px' } + +export const WithMenuMinWidth = (args) => ( +
+ + + + + + +
+) +WithMenuMinWidth.args = { menuMinWidth: '240px' } diff --git a/components/select/src/select/menu-wrapper.js b/components/select/src/select/menu-wrapper.js index 597b357a0..a5d41f18a 100644 --- a/components/select/src/select/menu-wrapper.js +++ b/components/select/src/select/menu-wrapper.js @@ -7,11 +7,23 @@ import React from 'react' const MenuWrapper = ({ children, dataTest, + inputWidth, maxHeight = '280px', - menuWidth, + menuMaxWidth, + menuMinWidth, onClick, selectRef, }) => { + // menuMinWidth or menuMaxWidth enables flexible sizing (fit-content), with + // min-width = max(input, menuMinWidth). Without them, width matches the input. + const flexible = menuMinWidth || menuMaxWidth + const width = flexible ? 'fit-content' : inputWidth + const flexibleMinWidth = menuMinWidth + ? `max(${inputWidth}, ${menuMinWidth})` + : inputWidth + const minWidth = flexible ? flexibleMinWidth : 'auto' + const maxWidth = menuMaxWidth || 'none' + return ( {` div { - width: ${menuWidth}; + width: ${width}; + min-width: ${minWidth}; + max-width: ${maxWidth}; height: auto; max-height: ${maxHeight}; overflow: auto; @@ -42,10 +56,12 @@ const MenuWrapper = ({ MenuWrapper.propTypes = { dataTest: PropTypes.string.isRequired, - menuWidth: PropTypes.string.isRequired, + inputWidth: PropTypes.string.isRequired, selectRef: PropTypes.object.isRequired, children: PropTypes.node, maxHeight: PropTypes.string, + menuMaxWidth: PropTypes.string, + menuMinWidth: PropTypes.string, onClick: PropTypes.func, } diff --git a/components/select/src/select/select.js b/components/select/src/select/select.js index 1579b24c4..ec94773b7 100644 --- a/components/select/src/select/select.js +++ b/components/select/src/select/select.js @@ -14,7 +14,7 @@ const DOWN_KEY = 40 export class Select extends Component { state = { open: false, - menuWidth: 'auto', + inputWidth: 'auto', } static defaultProps = { @@ -29,7 +29,7 @@ export class Select extends Component { this.inputRef.current.focus() } - this.setState({ menuWidth: this.getMenuWidth() }) + this.setState({ inputWidth: this.getInputWidth() }) window.addEventListener('resize', this.onResize) } @@ -47,21 +47,21 @@ export class Select extends Component { * See: https://nolanlawson.com/2018/09/25/accurately-measuring-layout-on-the-web */ onResize = debounce(() => { - const menuWidth = this.getMenuWidth() + const inputWidth = this.getInputWidth() - if (this.state.menuWidth !== menuWidth) { - this.setState({ menuWidth }) + if (this.state.inputWidth !== inputWidth) { + this.setState({ inputWidth }) } }, 50) - getMenuWidth() { + getInputWidth() { const { offsetWidth } = this.inputRef.current - const { menuWidth } = this.state + const { inputWidth } = this.state - if (offsetWidth && `${offsetWidth}px` !== menuWidth) { + if (offsetWidth && `${offsetWidth}px` !== inputWidth) { return `${offsetWidth}px` } - return menuWidth + return inputWidth } handleFocusInput = () => { @@ -81,7 +81,7 @@ export class Select extends Component { handleOpen = () => { this.setState({ open: true, - menuWidth: this.getMenuWidth(), + inputWidth: this.getInputWidth(), }) } @@ -154,7 +154,7 @@ export class Select extends Component { } render() { - const { open, menuWidth } = this.state + const { open, inputWidth } = this.state const { children, className, @@ -162,6 +162,8 @@ export class Select extends Component { onChange, tabIndex, maxHeight, + menuMinWidth, + menuMaxWidth, error, warning, valid, @@ -215,7 +217,9 @@ export class Select extends Component { onClick={this.onOutsideClick} maxHeight={maxHeight} selectRef={this.selectRef} - menuWidth={menuWidth} + inputWidth={inputWidth} + menuMinWidth={menuMinWidth} + menuMaxWidth={menuMaxWidth} dataTest={`${dataTest}-menu`} > {menu} @@ -241,6 +245,8 @@ Select.propTypes = { error: sharedPropTypes.statusPropType, initialFocus: PropTypes.bool, maxHeight: PropTypes.string, + menuMaxWidth: PropTypes.string, + menuMinWidth: PropTypes.string, tabIndex: PropTypes.string, valid: sharedPropTypes.statusPropType, warning: sharedPropTypes.statusPropType, diff --git a/components/select/src/single-select-field/single-select-field.js b/components/select/src/single-select-field/single-select-field.js index f3766e2a9..e77f7bc05 100644 --- a/components/select/src/single-select-field/single-select-field.js +++ b/components/select/src/single-select-field/single-select-field.js @@ -45,6 +45,8 @@ class SingleSelectField extends React.Component { helpText, validationText, maxHeight, + menuMinWidth, + menuMaxWidth, inputMaxHeight, inputWidth, children, @@ -81,6 +83,8 @@ class SingleSelectField extends React.Component { selected={selected} tabIndex={tabIndex} maxHeight={maxHeight} + menuMinWidth={menuMinWidth} + menuMaxWidth={menuMaxWidth} inputMaxHeight={inputMaxHeight} onChange={onChange} onFocus={onFocus} @@ -148,6 +152,10 @@ SingleSelectField.propTypes = { loadingText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** Constrains height of the SingleSelect */ maxHeight: PropTypes.string, + /** Sets a maximum width for the dropdown menu */ + menuMaxWidth: PropTypes.string, + /** Sets a minimum width for the dropdown menu */ + menuMinWidth: PropTypes.string, /** Text to display when there are no filter results */ noMatchText: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** Placeholder text when the SingleSelect is empty */ diff --git a/components/select/src/single-select-field/single-select-field.prod.stories.js b/components/select/src/single-select-field/single-select-field.prod.stories.js index cf20d6dbc..167934510 100644 --- a/components/select/src/single-select-field/single-select-field.prod.stories.js +++ b/components/select/src/single-select-field/single-select-field.prod.stories.js @@ -144,3 +144,34 @@ DefaultFilterPlaceholderAndNoMatchText.storyName = export const DefaultLoadingText = Template.bind({}) DefaultLoadingText.args = { loading: true, ...DefaultEmpty.args } DefaultLoadingText.storyName = 'Default: loadingText' + +export const WithMenuMaxWidth = Template.bind({}) +WithMenuMaxWidth.args = { + inputWidth: '120px', + menuMaxWidth: '200px', + children: [ + , + , + , + ], +} + +export const WithMenuMinWidth = Template.bind({}) +WithMenuMinWidth.args = { + inputWidth: '120px', + menuMinWidth: '240px', + children: [ + , + , + , + , + ], +} diff --git a/components/select/src/single-select/single-select.js b/components/select/src/single-select/single-select.js index 9f190e1ac..334acfa94 100644 --- a/components/select/src/single-select/single-select.js +++ b/components/select/src/single-select/single-select.js @@ -13,6 +13,8 @@ const SingleSelect = ({ selected = '', tabIndex, maxHeight, + menuMinWidth, + menuMaxWidth, inputMaxHeight, onChange, onFocus, @@ -68,6 +70,8 @@ const SingleSelect = ({ menu={menu} tabIndex={tabIndex} maxHeight={maxHeight} + menuMinWidth={menuMinWidth} + menuMaxWidth={menuMaxWidth} onChange={onChange} onFocus={onFocus} onKeyDown={onKeyDown} @@ -127,6 +131,10 @@ SingleSelect.propTypes = { loading: PropTypes.bool, loadingText: PropTypes.string, maxHeight: PropTypes.string, + /** Sets a maximum width for the dropdown menu */ + menuMaxWidth: PropTypes.string, + /** Sets a minimum width for the dropdown menu */ + menuMinWidth: PropTypes.string, /** Text to show when filter returns no results. Required if `filterable` prop is true */ noMatchText: requiredIf((props) => props.filterable, PropTypes.string), placeholder: PropTypes.string, diff --git a/components/select/src/single-select/single-select.prod.stories.js b/components/select/src/single-select/single-select.prod.stories.js index 6039292be..b7d570de6 100644 --- a/components/select/src/single-select/single-select.prod.stories.js +++ b/components/select/src/single-select/single-select.prod.stories.js @@ -356,3 +356,29 @@ export const ShiftedIntoView = (args) => ( `} ) + +export const WithMenuMaxWidth = (args) => ( +
+ + + + + +
+) +WithMenuMaxWidth.args = { menuMaxWidth: '200px' } + +export const WithMenuMinWidth = (args) => ( +
+ + + + + + +
+) +WithMenuMinWidth.args = { menuMinWidth: '240px' }