Skip to content

Commit 791b090

Browse files
committed
fix(pf4): added menu portal option for downshift select
1 parent f7f54cb commit 791b090

File tree

6 files changed

+133
-11
lines changed

6 files changed

+133
-11
lines changed

packages/pf4-component-mapper/demo/demo-schemas/select-schema.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ const loadOptions = (inputValue = '') => {
3737

3838
const selectSchema = {
3939
fields: [
40+
{
41+
component: componentTypes.SELECT,
42+
name: 'simple-portal-select',
43+
label: 'Simple portal select',
44+
options,
45+
menuIsPortal: true
46+
},
4047
{
4148
component: componentTypes.SELECT,
4249
name: 'simple-async-select',

packages/pf4-component-mapper/demo/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import FormRenderer from '@data-driven-forms/react-form-renderer';
55
import miqSchema from './demo-schemas/miq-schema';
66
import { uiArraySchema, arraySchema, array1Schema, schema, uiSchema, conditionalSchema, arraySchemaDDF } from './demo-schemas/widget-schema';
77
import { componentMapper, FormTemplate } from '../src';
8-
import { Title, Button, Toolbar, ToolbarGroup, ToolbarItem } from '@patternfly/react-core';
8+
import { Title, Button, Toolbar, ToolbarGroup, ToolbarItem, Modal } from '@patternfly/react-core';
99
import { wizardSchema, wizardSchemaWithFunction, wizardSchemaSimple, wizardSchemaSubsteps, wizardSchemaMoreSubsteps } from './demo-schemas/wizard-schema';
1010
import sandboxSchema from './demo-schemas/sandbox';
1111
import dualSchema from './demo-schemas/dual-list-schema';

packages/pf4-component-mapper/src/common/select/menu.js

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,72 @@
1-
import React from 'react';
1+
import React, { useEffect, useState, Fragment } from 'react';
2+
import { createPortal } from 'react-dom';
23
import Option from './option';
34
import Input from './input';
45
import EmptyOption from './empty-options';
56

7+
const getScrollParent = (element) => {
8+
let style = getComputedStyle(element);
9+
const excludeStaticParent = style.position === 'absolute';
10+
const overflowRx = /(auto|scroll)/;
11+
const docEl = document.documentElement;
12+
13+
if (style.position === 'fixed') {
14+
return docEl;
15+
}
16+
17+
for (let parent = element; (parent = parent.parentElement);) {
18+
style = getComputedStyle(parent);
19+
if (excludeStaticParent && style.position === 'static') {
20+
continue;
21+
}
22+
23+
if (overflowRx.test(style.overflow + style.overflowY + style.overflowX)) {
24+
return parent;
25+
}
26+
}
27+
28+
return docEl;
29+
};
30+
31+
const getMenuPosition = (selectBase) => {
32+
if (!selectBase) {
33+
return {};
34+
}
35+
36+
return selectBase.getBoundingClientRect();
37+
};
38+
39+
const MenuPortal = ({ selectToggleRef, menuPortalTarget, children, isSearchable }) => {
40+
const [position, setPosition] = useState(getMenuPosition(selectToggleRef.current));
41+
useEffect(() => {
42+
const scrollParentElement = getScrollParent(selectToggleRef.current);
43+
const scrollListener = scrollParentElement.addEventListener('scroll', () => {
44+
setPosition(getMenuPosition(selectToggleRef.current));
45+
});
46+
const resizeListener = window.addEventListener('resize', () => {
47+
setPosition(getMenuPosition(selectToggleRef.current));
48+
});
49+
return () => {
50+
window.removeEventListener('resize', resizeListener);
51+
scrollParentElement.removeEventListener('scroll', scrollListener);
52+
};
53+
}, [selectToggleRef]);
54+
55+
const top = isSearchable ? position.top + position.height + 64 : position.top + position.height;
56+
const portalDiv = (
57+
<div
58+
className={`pf-c-select ddorg_pf4-component-mapper__select-portal-menu${
59+
isSearchable ? ' ddorg_pf4-component-mapper__select-portal-menu-searchable' : ''
60+
}`}
61+
style={{ borderTop: '4px solid white', zIndex: 401, position: 'absolute', top, left: position.left, width: position.width }}
62+
>
63+
{children}
64+
</div>
65+
);
66+
67+
return createPortal(portalDiv, menuPortalTarget);
68+
};
69+
670
const Menu = ({
771
noResultsMessage,
872
noOptionsMessage,
@@ -16,12 +80,15 @@ const Menu = ({
1680
highlightedIndex,
1781
selectedItem,
1882
isMulti,
19-
isFetching
83+
isFetching,
84+
menuPortalTarget,
85+
menuIsPortal,
86+
selectToggleRef
2087
}) => {
2188
const filteredOptions = isSearchable ? filterOptions(options, filterValue) : options;
22-
return (
89+
const menuItems = (
2390
<ul className="pf-c-select__menu">
24-
{isSearchable && <Input inputRef={inputRef} getInputProps={getInputProps} />}
91+
{!menuIsPortal && isSearchable && <Input inputRef={inputRef} getInputProps={getInputProps} />}
2592
{filteredOptions.length === 0 && (
2693
<EmptyOption
2794
isSearchable={isSearchable}
@@ -36,12 +103,29 @@ const Menu = ({
36103
item,
37104
index,
38105
isActive: highlightedIndex === index,
39-
isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value
106+
isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value,
107+
onMouseUp: (e) => e.stopPropagation() // we need this to prevent issues with portal menu not selecting a option
40108
});
41109
return <Option key={item.value} item={item} {...itemProps} />;
42110
})}
43111
</ul>
44112
);
113+
if (menuIsPortal) {
114+
return (
115+
<Fragment>
116+
{isSearchable && (
117+
<ul className="pf-c-select__menu">
118+
<Input inputRef={inputRef} getInputProps={getInputProps} />
119+
</ul>
120+
)}
121+
<MenuPortal isSearchable={isSearchable} menuPortalTarget={menuPortalTarget} selectToggleRef={selectToggleRef}>
122+
{menuItems}
123+
</MenuPortal>
124+
</Fragment>
125+
);
126+
}
127+
128+
return menuItems;
45129
};
46130

47131
export default Menu;

packages/pf4-component-mapper/src/common/select/option.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Option.propTypes = {
2121
label: PropTypes.node
2222
}).isRequired,
2323
isActive: PropTypes.bool,
24-
isSelected: PropTypes.bool
24+
isSelected: PropTypes.bool,
25+
onClick: PropTypes.func.isRequired
2526
};
2627

2728
export default Option;

packages/pf4-component-mapper/src/common/select/select-styles.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@
77
animation: spin 2s linear infinite;
88
}
99

10+
.ddorg_pf4-component-mapper__select-portal-menu.ddorg_pf4-component-mapper__select-portal-menu-searchable {
11+
&::before {
12+
position: absolute;
13+
bottom: -4px;
14+
height: 4px;
15+
left: 0;
16+
right: 0;
17+
background: white;
18+
border-bottom-width: var(--pf-global--BorderWidth--sm);
19+
border-bottom-color: var(--pf-global--BorderColor--dark-100);
20+
border-bottom-style: solid;
21+
border-bottom-width: 1px;
22+
content: "";
23+
}
24+
}
25+
1026
.ddorg__pf4-component-mapper__select {
1127
&.single-select {
1228
.ddorg__pf4-component-mapper__select__placeholder {

packages/pf4-component-mapper/src/common/select/select.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CaretDownIcon, CloseIcon, CircleNotchIcon } from '@patternfly/react-ico
88
import '@patternfly/react-styles/css/components/Select/select.css';
99
import '@patternfly/react-styles/css/components/Chip/chip.css';
1010
import '@patternfly/react-styles/css/components/ChipGroup/chip-group.css';
11+
import '@patternfly/react-styles/css/components/Divider/divider.css';
1112

1213
import './select-styles.scss';
1314
import Menu from './menu';
@@ -90,7 +91,7 @@ const stateReducer = (state, changes, keepMenuOpen) => {
9091
case Downshift.stateChangeTypes.controlledPropUpdatedSelectedItem:
9192
return {
9293
...changes,
93-
inputValue: typeof changes.inputValue === 'string' ? changes.inputValue : state.inputValue
94+
inputValue: state.inputValue
9495
};
9596
default:
9697
return changes;
@@ -112,10 +113,13 @@ const InternalSelect = ({
112113
isFetching,
113114
onInputChange,
114115
loadingMessage,
116+
menuPortalTarget,
117+
menuIsPortal,
115118
...props
116119
}) => {
117120
const [showMore, setShowMore] = useState(false);
118121
const inputRef = useRef();
122+
const selectToggleRef = useRef();
119123
const parsedValue = parseInternalValue(value);
120124
const handleShowMore = () => setShowMore((prev) => !prev);
121125
const handleChange = (option) => onChange(getValue(isMulti, option, value));
@@ -136,7 +140,12 @@ const InternalSelect = ({
136140
const toggleButtonProps = getToggleButtonProps();
137141
return (
138142
<div className="pf-c-select">
139-
<div disabled={isDisabled} className={`pf-c-select__toggle${isDisabled ? ' pf-m-disabled' : ''}`} {...toggleButtonProps}>
143+
<div
144+
ref={selectToggleRef}
145+
disabled={isDisabled}
146+
className={`pf-c-select__toggle${isDisabled ? ' pf-m-disabled' : ''}`}
147+
{...toggleButtonProps}
148+
>
140149
<div className="pf-c-select_toggle-wrapper ddorg__pf4-component-mapper__select-toggle-wrapper">
141150
<ValueContainer placeholder={placeholder} value={itemToString(selectedItem, isMulti, showMore, handleShowMore, handleChange)} />
142151
</div>
@@ -162,6 +171,9 @@ const InternalSelect = ({
162171
highlightedIndex={highlightedIndex}
163172
selectedItem={isMulti ? value : parsedValue}
164173
isMulti={isMulti}
174+
menuPortalTarget={menuPortalTarget}
175+
menuIsPortal={menuIsPortal}
176+
selectToggleRef={selectToggleRef}
165177
/>
166178
)}
167179
</div>
@@ -192,7 +204,9 @@ InternalSelect.propTypes = {
192204
isMulti: PropTypes.bool,
193205
isFetching: PropTypes.bool,
194206
onInputChange: PropTypes.func,
195-
loadingMessage: PropTypes.node
207+
loadingMessage: PropTypes.node,
208+
menuPortalTarget: PropTypes.any,
209+
menuIsPortal: PropTypes.bool,
196210
};
197211

198212
const Select = ({ selectVariant, menuIsPortal, ...props }) => {
@@ -201,7 +215,7 @@ const Select = ({ selectVariant, menuIsPortal, ...props }) => {
201215

202216
const menuPortalTarget = menuIsPortal ? document.body : undefined;
203217

204-
return <DataDrivenSelect SelectComponent={InternalSelect} {...props} />;
218+
return <DataDrivenSelect SelectComponent={InternalSelect} menuPortalTarget={menuPortalTarget} menuIsPortal={menuIsPortal} {...props} />;
205219
};
206220

207221
Select.propTypes = {

0 commit comments

Comments
 (0)