Skip to content

Add groups/dividers to PF4 select #1148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/common/src/select/flat-options.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from "react";

interface Option {
label: string | ReactNode;
value: any;
selectAll?: boolean;
selectNone?: boolean;
}

interface ResultedOption {
label?: string | ReactNode;
value?: any;
selectAll?: boolean;
selectNone?: boolean;
group?: string | ReactNode;
divider?: boolean;
}


interface Options {
label?: string | ReactNode;
value?: any;
divider?: boolean;
selectAll?: boolean;
selectNone?: boolean;
options?: Option[];
}

declare const flatOptions: (options: Options[]) => ResultedOption[];

export default flatOptions;
3 changes: 3 additions & 0 deletions packages/common/src/select/flat-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const flatOptions = (options) => options.flatMap((option) => (option.options ? [{ group: option.label }, ...option.options] : [option]));

export default flatOptions;
1 change: 1 addition & 0 deletions packages/common/src/select/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default } from './select';
export * from './select';
export { default as parseInternalValue } from './parse-internal-value';
export { default as flatOptions } from './flat-options';
1 change: 1 addition & 0 deletions packages/common/src/select/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default } from './select';
export { default as parseInternalValue } from './parse-internal-value';
export { default as flatOptions } from './flat-options';
23 changes: 19 additions & 4 deletions packages/common/src/select/reducer.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
const reducer = (state, { type, payload, options = [] }) => {
export const init = ({ propsOptions, optionsTransformer }) => ({
isLoading: false,
options: optionsTransformer ? optionsTransformer(propsOptions) : propsOptions,
promises: {},
isInitialLoaded: false,
...(optionsTransformer && { originalOptions: propsOptions }),
});

const reducer = (state, { type, payload, options = [], optionsTransformer }) => {
switch (type) {
case 'updateOptions':
return {
...state,
options: payload,
options: optionsTransformer ? optionsTransformer(payload) : payload,
isLoading: false,
promises: {},
...(optionsTransformer && { originalOptions: payload }),
};
case 'startLoading':
return {
Expand All @@ -15,7 +24,8 @@ const reducer = (state, { type, payload, options = [] }) => {
case 'setOptions':
return {
...state,
options: payload,
options: optionsTransformer ? optionsTransformer(payload) : payload,
...(optionsTransformer && { originalOptions: payload }),
};
case 'initialLoaded':
return {
Expand All @@ -29,7 +39,12 @@ const reducer = (state, { type, payload, options = [] }) => {
...state.promises,
...payload,
},
options: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))],
options: optionsTransformer
? optionsTransformer([...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))])
: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))],
...(optionsTransformer && {
originalOptions: [...state.options, ...options.filter(({ value }) => !state.options.find((option) => option.value === value))],
}),
};
default:
return state;
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/select/select.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface SelectProps {
isSearchable?: boolean;
SelectComponent?: React.ComponentType;
noValueUpdates?: boolean;
optionsTransformer?: (options: AnyObject[]) => option[];
}

declare const Select: React.ComponentType<SelectProps>;
Expand Down
20 changes: 11 additions & 9 deletions packages/common/src/select/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import clsx from 'clsx';
import isEqual from 'lodash/isEqual';
import fnToString from '../utils/fn-to-string';
import reducer from './reducer';
import reducer, { init } from './reducer';
import useIsMounted from '../hooks/use-is-mounted';

const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) => {
Expand All @@ -16,7 +16,9 @@ const getSelectValue = (stateValue, simpleValue, isMulti, allOptions) => {

if (hasSelectAll || hasSelectNone) {
enhancedValue = enhancedValue || [];
const optionsLength = allOptions.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).length;
const optionsLength = allOptions.filter(
({ selectAll, selectNone, divider, options }) => !selectAll && !selectNone && !divider && !options
).length;

const selectedAll = optionsLength === enhancedValue.length;
const selectedNone = enhancedValue.length === 0;
Expand All @@ -43,7 +45,7 @@ const handleSelectChange = (option, simpleValue, isMulti, onChange, allOptions,
const sanitizedOption = !enhanceOption && isMulti ? [] : enhanceOption;

if (isMulti && sanitizedOption.find(({ selectAll }) => selectAll)) {
return onChange(allOptions.filter(({ selectAll, selectNone }) => !selectAll && !selectNone).map(({ value }) => value));
return onChange(allOptions.filter(({ selectAll, selectNone, value }) => !selectAll && !selectNone && value).map(({ value }) => value));
}

if (isMulti && sanitizedOption.find(({ selectNone }) => selectNone)) {
Expand Down Expand Up @@ -73,14 +75,11 @@ const Select = ({
loadOptionsChangeCounter,
SelectComponent,
noValueUpdates,
optionsTransformer,
...props
}) => {
const [state, dispatch] = useReducer(reducer, {
isLoading: false,
options: propsOptions,
promises: {},
isInitialLoaded: false,
});
const [state, originalDispatch] = useReducer(reducer, { optionsTransformer, propsOptions }, init);
const dispatch = (action) => originalDispatch({ ...action, optionsTransformer });

const isMounted = useIsMounted();

Expand Down Expand Up @@ -145,6 +144,7 @@ const Select = ({
onChange={() => {}}
{...loadingProps}
noOptionsMessage={renderNoOptionsMessage()}
{...(state.originalOptions && { originalOptions: state.originalOptions })}
/>
);
}
Expand Down Expand Up @@ -194,6 +194,7 @@ const Select = ({
noOptionsMessage={renderNoOptionsMessage()}
hideSelectedOptions={false}
closeMenuOnSelect={!isMulti}
{...(state.originalOptions && { originalOptions: state.originalOptions })}
/>
);
};
Expand All @@ -220,6 +221,7 @@ Select.propTypes = {
isSearchable: PropTypes.bool,
SelectComponent: PropTypes.elementType.isRequired,
noValueUpdates: PropTypes.bool,
optionsTransformer: PropTypes.func,
};

Select.defaultProps = {
Expand Down
8 changes: 4 additions & 4 deletions packages/pf4-component-mapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
"javascript"
],
"devDependencies": {
"@patternfly/react-core": "^4.101.3",
"@patternfly/react-icons": "^4.9.5"
"@patternfly/react-core": "^4.157.3",
"@patternfly/react-icons": "^4.11.7"
},
"peerDependencies": {
"@data-driven-forms/react-form-renderer": ">=3.2.1",
"@patternfly/react-core": "^4.101.3",
"@patternfly/react-icons": "^4.9.5"
"@patternfly/react-core": "^4.157.3",
"@patternfly/react-icons": "^4.11.7"
},
"dependencies": {
"@data-driven-forms/common": "*",
Expand Down
44 changes: 34 additions & 10 deletions packages/pf4-component-mapper/src/select/select/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,26 @@ const Menu = ({
menuPortalTarget,
menuIsPortal,
selectToggleRef,
originalOptions,
}) => {
const filteredOptions = isSearchable ? filterOptions(options, filterValue) : options;
const filteredOptions = isSearchable ? filterOptions(originalOptions, filterValue) : originalOptions;

let index = 0;

const createOption = (item) => {
index++;

const itemProps = getItemProps({
item,
index,
isActive: highlightedIndex === index,
isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value,
onMouseUp: (e) => e.stopPropagation(), // we need this to prevent issues with portal menu not selecting a option
});

return <Option key={item.key || item.value || (typeof item.label === 'string' && item.label) || item} item={item} {...itemProps} />;
};

const menuItems = (
<ul className={`pf-c-select__menu${menuIsPortal ? ' ddorg__pf4-component-mapper__select-menu-portal' : ''}`}>
{filteredOptions.length === 0 && (
Expand All @@ -130,15 +148,21 @@ const Menu = ({
isFetching={isFetching}
/>
)}
{filteredOptions.map((item, index) => {
const itemProps = getItemProps({
item,
index,
isActive: highlightedIndex === index,
isSelected: isMulti ? !!selectedItem.find(({ value }) => item.value === value) : selectedItem === item.value,
onMouseUp: (e) => e.stopPropagation(), // we need this to prevent issues with portal menu not selecting a option
});
return <Option key={item.key || item.value || (typeof item.label === 'string' && item.label) || item} item={item} {...itemProps} />;
{filteredOptions.map((item, arrayIndex) => {
if (item.options) {
return (
<div className="pf-c-select__menu-group" key={`group-${arrayIndex}`}>
<div className="pf-c-select__menu-group-title">{item.label}</div>
{item.options.map((nestedItem) => createOption(nestedItem))}
</div>
);
}

if (item.divider) {
return <hr className="pf-c-divider" key={`divider-${index}`} />;
}

return createOption(item);
})}
</ul>
);
Expand Down
51 changes: 44 additions & 7 deletions packages/pf4-component-mapper/src/select/select/select.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useRef, useState } from 'react';
import PropTypes from 'prop-types';

import DataDrivenSelect from '@data-driven-forms/common/select';
import DataDrivenSelect, { flatOptions } from '@data-driven-forms/common/select';
import parseInternalValue from '@data-driven-forms/common/select/parse-internal-value';
import Downshift from 'downshift';
import { CaretDownIcon, CloseIcon, CircleNotchIcon } from '@patternfly/react-icons';
Expand Down Expand Up @@ -69,11 +69,36 @@ const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) =>
};

// TODO fix the value of internal select not to be an array all the time. It forces the filter value to be an array and it crashes sometimes.
const filterOptions = (options, filterValue = '') =>
options.filter(({ label }) => {
const filter = Array.isArray(filterValue) && filterValue.length > 0 ? filterValue[0] : filterValue;
return label.toLowerCase().includes(filter.toLowerCase());
});
const filterOptions = (options, filterValue = '') => {
const filter = (Array.isArray(filterValue) && filterValue.length > 0 ? filterValue[0] : filterValue).toLowerCase();

if (!filter) {
return options;
}

return options
.map((option) => {
if (option.options) {
const filteredNested = option.options.map((option) => (option.label?.toLowerCase().includes(filter) ? option : null)).filter(Boolean);

if (filteredNested.length === 0) {
return null;
}

return {
...option,
options: filteredNested,
};
}

if (option.label?.toLowerCase().includes(filter)) {
return option;
}

return null;
})
.filter(Boolean);
};

const getValue = (isMulti, option, value) => {
if (!isMulti || !option) {
Expand Down Expand Up @@ -154,6 +179,7 @@ const InternalSelect = ({
loadingMessage,
menuPortalTarget,
menuIsPortal,
originalOptions,
...props
}) => {
const [showMore, setShowMore] = useState(false);
Expand Down Expand Up @@ -223,6 +249,7 @@ const InternalSelect = ({
menuPortalTarget={menuPortalTarget}
menuIsPortal={menuIsPortal}
selectToggleRef={selectToggleRef}
originalOptions={originalOptions}
/>
)}
</div>
Expand All @@ -238,6 +265,7 @@ InternalSelect.propTypes = {
PropTypes.shape({
value: PropTypes.any,
label: PropTypes.any,
divider: PropTypes.bool,
})
).isRequired,
value: PropTypes.any,
Expand All @@ -256,12 +284,21 @@ InternalSelect.propTypes = {
loadingMessage: PropTypes.node,
menuPortalTarget: PropTypes.any,
menuIsPortal: PropTypes.bool,
originalOptions: PropTypes.array,
};

const Select = ({ menuIsPortal, ...props }) => {
const menuPortalTarget = menuIsPortal ? document.body : undefined;

return <DataDrivenSelect SelectComponent={InternalSelect} menuPortalTarget={menuPortalTarget} menuIsPortal={menuIsPortal} {...props} />;
return (
<DataDrivenSelect
SelectComponent={InternalSelect}
menuPortalTarget={menuPortalTarget}
menuIsPortal={menuIsPortal}
{...props}
optionsTransformer={flatOptions}
/>
);
};

Select.propTypes = {
Expand Down
Loading