diff --git a/packages/pf4-component-mapper/demo/demo-schemas/select-schema.js b/packages/pf4-component-mapper/demo/demo-schemas/select-schema.js new file mode 100644 index 000000000..f0b085823 --- /dev/null +++ b/packages/pf4-component-mapper/demo/demo-schemas/select-schema.js @@ -0,0 +1,136 @@ +import componentTypes from '@data-driven-forms/react-form-renderer/dist/cjs/component-types'; + +const options = [ + { + label: 'Morton', + value: 'Jenifer' + }, + { + label: 'Vega', + value: 'Cervantes' + }, + { + label: 'Gilbert', + value: 'Wallace' + }, + { + label: 'Jami', + value: 'Cecilia' + }, + { + label: 'Ebony', + value: 'Kay' + } +]; + +const loadOptions = (inputValue = '') => { + return new Promise((res) => + setTimeout(() => { + if (inputValue.length === 0) { + return res(options.slice(0, 3)); + } + + return res(options.filter(({ label }) => label.toLocaleLowerCase().includes(inputValue.toLocaleLowerCase()))); + }, 1500) + ); +}; + +const selectSchema = { + fields: [ + { + component: componentTypes.SELECT, + name: 'simple-portal-select', + label: 'Simple portal select', + options, + menuIsPortal: true + }, + { + component: componentTypes.SELECT, + name: 'simple-async-select', + label: 'Simple async select', + loadOptions + }, + { + component: componentTypes.SELECT, + name: 'simple-searchable-async-select', + label: 'Simple searchable async select', + loadOptions, + isSearchable: true + }, + { + component: componentTypes.SELECT, + name: 'multi-async-select', + label: 'multi async select', + loadOptions, + isMulti: true + }, + { + component: componentTypes.SELECT, + name: 'searchable-multi-async-select', + label: 'Multi searchable async select', + loadOptions, + isSearchable: true + }, + { + component: componentTypes.SELECT, + name: 'multi-simple-select', + label: 'Simple multi select', + options, + isMulti: true + }, + { + component: componentTypes.SELECT, + name: 'multi-searchable-select', + label: 'Searchable multi select', + options, + isMulti: true, + isSearchable: true + }, + { + component: componentTypes.SELECT, + name: 'multi-clearable-searchable-select', + label: 'Searchable clearable multi select', + options, + isMulti: true, + isSearchable: true, + isClearable: true + }, + { + component: componentTypes.SELECT, + name: 'simple-select', + label: 'Simple-select', + options + }, + { + component: componentTypes.SELECT, + name: 'disabled-select', + label: 'Disabled-select', + options, + isDisabled: true + }, + { + component: componentTypes.SELECT, + name: 'clearable-select', + label: 'Clearable-select', + options, + isClearable: true + }, + { + component: componentTypes.SELECT, + name: 'searchable-select', + label: 'Clearable-select', + options, + isSearchable: true + }, + { + component: componentTypes.SELECT, + name: 'dosbaled-option-select', + label: 'Disabled-option-select', + options: [...options, { label: 'Disabled option', value: 'disabled', isDisabled: true }] + } + ] +}; + +export default { + ...selectSchema +}; diff --git a/packages/pf4-component-mapper/demo/index.js b/packages/pf4-component-mapper/demo/index.js index ae9a954f6..dbd0b4526 100644 --- a/packages/pf4-component-mapper/demo/index.js +++ b/packages/pf4-component-mapper/demo/index.js @@ -5,11 +5,12 @@ import FormRenderer from '@data-driven-forms/react-form-renderer'; import miqSchema from './demo-schemas/miq-schema'; import { uiArraySchema, arraySchema, array1Schema, schema, uiSchema, conditionalSchema, arraySchemaDDF } from './demo-schemas/widget-schema'; import { componentMapper, FormTemplate } from '../src'; -import { Title, Button, Toolbar, ToolbarGroup } from '@patternfly/react-core'; +import { Title, Button, Toolbar, ToolbarGroup, ToolbarItem, Modal } from '@patternfly/react-core'; import { wizardSchema, wizardSchemaWithFunction, wizardSchemaSimple, wizardSchemaSubsteps, wizardSchemaMoreSubsteps } from './demo-schemas/wizard-schema'; import sandboxSchema from './demo-schemas/sandbox'; import dualSchema from './demo-schemas/dual-list-schema'; import demoSchema from '@data-driven-forms/common/src/demoschema'; +import selectSchema from './demo-schemas/select-schema'; const Summary = props =>
Custom summary component.
; @@ -23,7 +24,7 @@ const fieldArrayState = { schema: arraySchemaDDF, additionalOptions: { class App extends React.Component { constructor(props) { super(props); - this.state = fieldArrayState + this.state = {schema: selectSchema, additionalOptions: {}} } render() { @@ -32,25 +33,30 @@ class App extends React.Component { Pf4 component mapper - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ( - + ); ClearIndicator.propTypes = { - innerProps: PropTypes.object.isRequired, - clearValue: PropTypes.func -}; - -ClearIndicator.defaultProps = { - clearValue: () => undefined + clearSelection: PropTypes.func.isRequired }; export default ClearIndicator; diff --git a/packages/pf4-component-mapper/src/common/select/clear-indicator.scss b/packages/pf4-component-mapper/src/common/select/clear-indicator.scss new file mode 100644 index 000000000..fe064681a --- /dev/null +++ b/packages/pf4-component-mapper/src/common/select/clear-indicator.scss @@ -0,0 +1,17 @@ +.ddorg__pf4-component-mapper__select-clear-indicator { + position: relative; + display: inline-block; + > svg { + fill: var(--pf-global--palette--black-600) + } + &:hover > svg { + fill: inherit + } + &::before { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/packages/pf4-component-mapper/src/common/select/dropdown-indicator.js b/packages/pf4-component-mapper/src/common/select/dropdown-indicator.js deleted file mode 100644 index 1f41a67c2..000000000 --- a/packages/pf4-component-mapper/src/common/select/dropdown-indicator.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { CircleNotchIcon, CaretDownIcon } from '@patternfly/react-icons'; - -const DropdownIndicator = ({ selectProps: { isFetching } }) => (isFetching ? : ); - -DropdownIndicator.propTypes = { - selectProps: PropTypes.shape({ - isFetching: PropTypes.bool - }).isRequired -}; - -export default DropdownIndicator; diff --git a/packages/pf4-component-mapper/src/common/select/empty-options.js b/packages/pf4-component-mapper/src/common/select/empty-options.js new file mode 100644 index 000000000..9dc17885e --- /dev/null +++ b/packages/pf4-component-mapper/src/common/select/empty-options.js @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const EmptyOptions = ({ noOptionsMessage, noResultsMessage, getInputProps, isSearchable, isFetching }) => { + const { value } = getInputProps(); + const message = isFetching ? noOptionsMessage() : isSearchable && value ? noResultsMessage : noOptionsMessage(); + return
{message}
; +}; + +EmptyOptions.propTypes = { + noOptionsMessage: PropTypes.func.isRequired, + noResultsMessage: PropTypes.node.isRequired, + getInputProps: PropTypes.func.isRequired, + isSearchable: PropTypes.bool, + isFetching: PropTypes.bool +}; + +export default EmptyOptions; diff --git a/packages/pf4-component-mapper/src/common/select/input.js b/packages/pf4-component-mapper/src/common/select/input.js index 2204a2a97..964f4caee 100644 --- a/packages/pf4-component-mapper/src/common/select/input.js +++ b/packages/pf4-component-mapper/src/common/select/input.js @@ -1,13 +1,39 @@ -import React from 'react'; -import { components } from 'react-select'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; +import { Divider } from '@patternfly/react-core'; -const Input = (props) => ; +import './input.scss'; + +const Input = ({ inputRef, isSearchable, isDisabled, getInputProps, value, ...props }) => { + const inputProps = getInputProps({ disabled: isDisabled }); + return ( + +
+ +
+ +
+ ); +}; Input.propTypes = { - selectProps: PropTypes.shape({ - isMulti: PropTypes.bool - }).isRequired + inputRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + isSearchable: PropTypes.bool, + isDisabled: PropTypes.bool, + getInputProps: PropTypes.func.isRequired, + value: PropTypes.string }; export default Input; diff --git a/packages/pf4-component-mapper/src/common/select/input.scss b/packages/pf4-component-mapper/src/common/select/input.scss new file mode 100644 index 000000000..447275cdc --- /dev/null +++ b/packages/pf4-component-mapper/src/common/select/input.scss @@ -0,0 +1,4 @@ +.ddorg__pf4-component-mapper__select-input { + border: none; + flex: 1; +} diff --git a/packages/pf4-component-mapper/src/common/select/menu.js b/packages/pf4-component-mapper/src/common/select/menu.js new file mode 100644 index 000000000..c8c6ba08b --- /dev/null +++ b/packages/pf4-component-mapper/src/common/select/menu.js @@ -0,0 +1,131 @@ +import React, { useEffect, useState, Fragment } from 'react'; +import { createPortal } from 'react-dom'; +import Option from './option'; +import Input from './input'; +import EmptyOption from './empty-options'; + +const getScrollParent = (element) => { + let style = getComputedStyle(element); + const excludeStaticParent = style.position === 'absolute'; + const overflowRx = /(auto|scroll)/; + const docEl = document.documentElement; + + if (style.position === 'fixed') { + return docEl; + } + + for (let parent = element; (parent = parent.parentElement);) { // eslint-disable-line + style = getComputedStyle(parent); + if (excludeStaticParent && style.position === 'static') { + continue; + } + + if (overflowRx.test(style.overflow + style.overflowY + style.overflowX)) { + return parent; + } + } + + return docEl; +}; + +const getMenuPosition = (selectBase) => { + if (!selectBase) { + return {}; + } + + return selectBase.getBoundingClientRect(); +}; + +const MenuPortal = ({ selectToggleRef, menuPortalTarget, children, isSearchable }) => { + const [position, setPosition] = useState(getMenuPosition(selectToggleRef.current)); + useEffect(() => { + const scrollParentElement = getScrollParent(selectToggleRef.current); + const scrollListener = scrollParentElement.addEventListener('scroll', () => { + setPosition(getMenuPosition(selectToggleRef.current)); + }); + const resizeListener = window.addEventListener('resize', () => { + setPosition(getMenuPosition(selectToggleRef.current)); + }); + return () => { + window.removeEventListener('resize', resizeListener); + scrollParentElement.removeEventListener('scroll', scrollListener); + }; + }, [selectToggleRef]); + + const top = isSearchable ? position.top + position.height + 64 : position.top + position.height; + const portalDiv = ( +
+ {children} +
+ ); + + return createPortal(portalDiv, menuPortalTarget); +}; + +const Menu = ({ + noResultsMessage, + noOptionsMessage, + filterOptions, + inputRef, + isSearchable, + filterValue, + options, + getItemProps, + getInputProps, + highlightedIndex, + selectedItem, + isMulti, + isFetching, + menuPortalTarget, + menuIsPortal, + selectToggleRef +}) => { + const filteredOptions = isSearchable ? filterOptions(options, filterValue) : options; + const menuItems = ( +
    + {!menuIsPortal && isSearchable && } + {filteredOptions.length === 0 && ( + + )} + {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
+ ); + if (menuIsPortal) { + return ( + + {isSearchable && ( +
    + +
+ )} + + {menuItems} + +
+ ); + } + + return menuItems; +}; + +export default Menu; diff --git a/packages/pf4-component-mapper/src/common/select/multi-value-container.js b/packages/pf4-component-mapper/src/common/select/multi-value-container.js deleted file mode 100644 index 53f70ae5b..000000000 --- a/packages/pf4-component-mapper/src/common/select/multi-value-container.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const MultiValueContainer = (props) => ( -
- {!Array.isArray(props.children) ? props.children : props.children[0]} - {props.children[1]} -
-); - -MultiValueContainer.propTypes = { - children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired, - data: PropTypes.shape({ label: PropTypes.node.isRequired }).isRequired, - className: PropTypes.string -}; - -MultiValueContainer.defaultProps = { - className: '' -}; - -export default MultiValueContainer; diff --git a/packages/pf4-component-mapper/src/common/select/multi-value-remove.js b/packages/pf4-component-mapper/src/common/select/multi-value-remove.js deleted file mode 100644 index 857c4481d..000000000 --- a/packages/pf4-component-mapper/src/common/select/multi-value-remove.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { components } from 'react-select'; - -import { TimesCircleIcon } from '@patternfly/react-icons'; - -const MultiValueRemove = (props) => ( - - - -); - -export default MultiValueRemove; diff --git a/packages/pf4-component-mapper/src/common/select/option.js b/packages/pf4-component-mapper/src/common/select/option.js index 262aa98e4..1062c4fc1 100644 --- a/packages/pf4-component-mapper/src/common/select/option.js +++ b/packages/pf4-component-mapper/src/common/select/option.js @@ -1,53 +1,37 @@ import React from 'react'; -import { components } from 'react-select'; import PropTypes from 'prop-types'; -import { Checkbox } from '@patternfly/react-core'; import { CheckIcon } from '@patternfly/react-icons'; -const Option = (props) => ( -
- {props.selectProps && props.selectProps && props.selectProps.isCheckbox && ( - props.selectOption(props.data)} - id={`${props.innerProps && props.innerProps.id}-checkbox`} - /> - )} - - {props.isSelected && props.selectProps && !props.selectProps.isCheckbox && } -
+const Option = ({ item, isActive, isSelected, ...props }) => ( +
  • + +
  • ); Option.propTypes = { - isFocused: PropTypes.bool, + item: PropTypes.shape({ + label: PropTypes.node, + isDisabled: PropTypes.bool, + disabled: PropTypes.bool + }).isRequired, + isActive: PropTypes.bool, isSelected: PropTypes.bool, - getStyles: PropTypes.func.isRequired, - selectOption: PropTypes.func, - cx: PropTypes.func.isRequired, - data: PropTypes.shape({ - selected: PropTypes.bool - }), - innerProps: PropTypes.shape({ - id: PropTypes.string - }), - selectProps: PropTypes.shape({ - isCheckbox: PropTypes.bool - }), - isDisabled: PropTypes.bool -}; - -Option.defaultProps = { - isFocused: false, - isSelected: false, - isDisabled: false, - selectOption: () => undefined, - selectProps: { - isCheckbox: false - }, - innerProps: { - id: 'some-classname' - } + onClick: PropTypes.func.isRequired }; export default Option; diff --git a/packages/pf4-component-mapper/src/common/select/select-styles.scss b/packages/pf4-component-mapper/src/common/select/select-styles.scss index 58266d05e..ae4461911 100644 --- a/packages/pf4-component-mapper/src/common/select/select-styles.scss +++ b/packages/pf4-component-mapper/src/common/select/select-styles.scss @@ -3,188 +3,22 @@ 100% { transform: rotate(360deg); } } -.ddorg__pf4-component-mapper__select { - &.single-select { - .ddorg__pf4-component-mapper__select__placeholder { - margin-left: 8px; - } - .ddorg__pf4-component-mapper__select__input { - margin-left: 6px; - } +.ddorg__pf4-component-mapper__select-loading-icon { + animation: spin 2s linear infinite; +} + +.ddorg_pf4-component-mapper__select-portal-menu.ddorg_pf4-component-mapper__select-portal-menu-searchable { + &::before { + position: absolute; + bottom: -4px; + height: 4px; + left: 0; + right: 0; + background: white; + border-bottom-width: var(--pf-global--BorderWidth--sm); + border-bottom-color: var(--pf-global--BorderColor--dark-100); + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; } - .spinning { - animation: spin 2s linear infinite; - } - .ddorg__pf4-component-mapper__select__control { - box-shadow: none; - cursor: pointer; - border-radius: 0; - border: 0; - &::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - content: ""; - border: var(--pf-global--BorderWidth--sm) solid; - border-color: var(--pf-global--Color--light-200); - border-bottom-color: var(--pf-global--Color--dark-100); - } - &:hover::before { - - border-bottom-color: var(--pf-global--active-color--100); - } - - &.ddorg__pf4-component-mapper__select__control--is-focused::before { - border-bottom-width: var(--pf-global--BorderWidth--md); - border-bottom-color: var(--pf-global--active-color--100); - } - .ddorg__pf4-component-mapper__select__indicators { - padding-right: 6px; - z-index: 1; - >button.pf-c-button.pf-m-plain { - display: flex; - justify-content: center; - } - .ddorg__pf4-component-mapper__select__indicator-separator { - display: none; - } - svg:first-child { - fill: var(--pf-global--Color--400); - - &:hover { - fill: var(--pf-global--Color--dark-100); - } - } - } - } -} - -/** -* Move menu styles out of select scope to enable using it while using portaling for context menu -* !important is used to override global styles comming from react-select -* z-index of menu has to be > 400 to show over pf4-modal -*/ - -.ddorg__pf4-component-mapper__select__menu { - cursor: pointer; - border-radius: 0 !important; - z-index: 1000 !important; -} - -.ddorg__pf4-component-mapper__select__menu--option { - display: flex; - align-items: center; - color: var(--pf-global--Color--dark-100); - - &.focused { - background-color: var(--pf-global--Color--light-200); - } - - svg { - width: 0.6em; - margin-right: 10px; - fill: var(--pf-global--active-color--100); - } - - div.pf-c-check { - padding-left: 1rem; - & + .ddorg__pf4-component-mapper__select__option { - padding-left: 0; - } - } - - &.disabled { - cursor: default; - } - - &.disabled div { - color: var(--pf-global--disabled-color--100); - pointer-events: none; - cursor: none; - } -} - -.ddorg__pf4-component-mapper__select__menu--option div { - background: transparent; - cursor: pointer; - color: var(--pf-global--Color--300); -} - -.ddorg__pf4-component-mapper__select__single-value { -padding-left: 8px; -} - -.ddorg__pf4-component-mapper__select__multivalue--container { -display: flex; -flex-direction: row; -flex-wrap: nowrap; -align-items: center; -box-sizing: border-box; -line-height: 24px; -position: relative; -margin-right: 4px; -font-size: 12px; -max-height: 26px; - -> .ddorg__pf4-component-mapper__select__multivalue--remove { - display: flex; - - svg { - fill: var(--pf-global--Color--400); - } - - > :hover { - background: transparent; - svg { - fill: var(--pf-global--Color--dark-100); - } - } -} - -&::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - content: ""; - border: var(--pf-global--BorderWidth--sm) solid var(--pf-global--Color--dark-200); - border-radius: 3px; - pointer-events: none; -} -} - -.ddorg__pf4-component-mapper__select__value--container { - display: flex; - padding-left: 8px; - align-items: center; - flex-wrap: wrap; - max-width: calc(100% - 70px); - - .ddorg__pf4-component-mapper__select__multivalue--container { - align-items: initial; - .ddorg__pf4-component-mapper__select__multi-value__label { - max-width: 240px; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - } - } - - .ddorg__pf4-component-mapper__select__value--container-chipgroup { - padding: 4px 6px; - font-size: 12px; - background-color: var(--pf-global--BorderColor--300); - border: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--300); - margin: 0; - max-height: 26px; - &:hover{ - border-color: var(--pf-global--Color--dark-100); - } - > span { - color: var(--pf-global--Color--dark-100); - } - } } diff --git a/packages/pf4-component-mapper/src/common/select/select.js b/packages/pf4-component-mapper/src/common/select/select.js index bb0a8dfd5..0539ecee6 100644 --- a/packages/pf4-component-mapper/src/common/select/select.js +++ b/packages/pf4-component-mapper/src/common/select/select.js @@ -1,60 +1,221 @@ -import React from 'react'; +import React, { useRef, useState } from 'react'; import PropTypes from 'prop-types'; import DataDrivenSelect from '@data-driven-forms/common/src/select'; -import ReactSelect from 'react-select'; -import CreatableSelect from 'react-select/creatable'; +import parseInternalValue from '@data-driven-forms/common/src/select/parse-internal-value'; +import Downshift from 'downshift'; +import { CaretDownIcon, CloseIcon, CircleNotchIcon } from '@patternfly/react-icons'; +import '@patternfly/react-styles/css/components/Select/select.css'; +import '@patternfly/react-styles/css/components/Chip/chip.css'; +import '@patternfly/react-styles/css/components/ChipGroup/chip-group.css'; +import '@patternfly/react-styles/css/components/Divider/divider.css'; -import MultiValueContainer from './multi-value-container'; -import ValueContainer from './value-container'; -import MultiValueRemove from './multi-value-remove'; -import DropdownIndicator from './dropdown-indicator'; +import './select-styles.scss'; +import Menu from './menu'; import ClearIndicator from './clear-indicator'; -import Option from './option'; +import ValueContainer from './value-container'; -import './select-styles.scss'; +const itemToString = (value, isMulti, showMore, handleShowMore, handleChange) => { + if (!value) { + return ''; + } -const Select = ({ selectVariant, menuIsPortal, ...props }) => { - const isSearchable = selectVariant === 'createable' || props.isSearchable; - const simpleValue = selectVariant === 'createable' ? false : props.simpleValue; + if (Array.isArray(value)) { + if (!value || value.length === 0) { + return; + } - const menuPortalTarget = menuIsPortal ? document.body : undefined; + if (isMulti) { + const visibleOptions = showMore ? value : value.slice(0, 3); + return ( +
    event.stopPropagation()}> +
      + {visibleOptions.map((item, index) => { + const label = typeof item === 'object' ? item.label : item; + return ( +
    • event.stopPropagation()} key={label}> +
      + + {label} + + +
      +
    • + ); + })} + {value.length > 3 && ( +
    • + +
    • + )} +
    +
    + ); + } + + return value.map((item) => (typeof item === 'object' ? item.label : item)).join(','); + } + + if (typeof value === 'object') { + return value.label; + } + + return value; +}; +const filterOptions = (options, filterValue = '') => options.filter(({ label }) => label.toLowerCase().includes(filterValue.toLowerCase())); + +const getValue = (isMulti, option, value) => { + if (!isMulti || !option) { + return option; + } + + const isSelected = value.find(({ value }) => value === option.value); + return isSelected ? value.filter(({ value }) => value !== option.value) : [...value, option]; +}; + +const stateReducer = (state, changes, keepMenuOpen) => { + switch (changes.type) { + case Downshift.stateChangeTypes.keyDownEnter: + case Downshift.stateChangeTypes.clickItem: + return { + ...changes, + isOpen: keepMenuOpen ? state.isOpen : !state.isOpen, + highlightedIndex: state.highlightedIndex, + inputValue: state.inputValue // prevent filter value change after option click + }; + case Downshift.stateChangeTypes.controlledPropUpdatedSelectedItem: + return { + ...changes, + inputValue: state.inputValue + }; + default: + return changes; + } +}; + +const InternalSelect = ({ + noResultsMessage, + noOptionsMessage, + onChange, + options, + value, + simpleValue, + placeholder, + isSearchable, + isDisabled, + isClearable, + isMulti, + isFetching, + onInputChange, + loadingMessage, + menuPortalTarget, + menuIsPortal, + ...props +}) => { + const [showMore, setShowMore] = useState(false); + const inputRef = useRef(); + const selectToggleRef = useRef(); + const parsedValue = parseInternalValue(value); + const handleShowMore = () => setShowMore((prev) => !prev); + const handleChange = (option) => onChange(getValue(isMulti, option, value)); return ( - itemToString(value, isMulti, showMore, handleShowMore, handleChange)} + selectedItem={value || ''} + stateReducer={(state, changes) => stateReducer(state, changes, isMulti)} + onInputValueChange={(inputValue) => { + if (onInputChange && typeof inputValue === 'string') { + onInputChange(inputValue); + } }} - menuPortalTarget={menuPortalTarget} - {...props} - className={`ddorg__pf4-component-mapper__select${props.isMulti ? ' multi-select' : ' single-select'}`} - classNamePrefix="ddorg__pf4-component-mapper__select" - styles={{ - menuPortal: (provided) => ({ - ...provided, - 'z-index': 'initial' - }) + > + {({ isOpen, inputValue, itemToString, selectedItem, clearSelection, getInputProps, getToggleButtonProps, getItemProps, highlightedIndex }) => { + const toggleButtonProps = getToggleButtonProps(); + return ( +
    +
    +
    + +
    + {isClearable && parsedValue && } + + {isFetching ? : } + +
    + {isOpen && ( + + )} +
    + ); }} - isSearchable={isSearchable} - simpleValue={simpleValue} - selectVariant={selectVariant} - /> + ); }; +InternalSelect.propTypes = { + onChange: PropTypes.func.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.any, + label: PropTypes.any + }) + ).isRequired, + value: PropTypes.any, + simpleValue: PropTypes.bool, + placeholder: PropTypes.string, + isSearchable: PropTypes.bool, + id: PropTypes.string, + name: PropTypes.string.isRequired, + isDisabled: PropTypes.bool, + isClearable: PropTypes.bool, + noResultsMessage: PropTypes.node, + noOptionsMessage: PropTypes.func, + isMulti: PropTypes.bool, + isFetching: PropTypes.bool, + onInputChange: PropTypes.func, + loadingMessage: PropTypes.node, + menuPortalTarget: PropTypes.any, + menuIsPortal: PropTypes.bool +}; + +const Select = ({ menuIsPortal, ...props }) => { + const menuPortalTarget = menuIsPortal ? document.body : undefined; + + return ; +}; + Select.propTypes = { - selectVariant: PropTypes.oneOf(['default', 'createable']), isSearchable: PropTypes.bool, showMoreLabel: PropTypes.node, showLessLabel: PropTypes.node, @@ -71,13 +232,13 @@ Select.propTypes = { loadOptions: PropTypes.func, loadingMessage: PropTypes.node, updatingMessage: PropTypes.node, - noOptionsMessage: PropTypes.func, menuIsPortal: PropTypes.bool, - placeholder: PropTypes.string + placeholder: PropTypes.string, + noResultsMessage: PropTypes.node, + noOptionsMessage: PropTypes.node }; Select.defaultProps = { - selectVariant: 'default', showMoreLabel: 'more', showLessLabel: 'Show less', simpleValue: true, @@ -87,7 +248,9 @@ Select.defaultProps = { menuIsPortal: false, placeholder: 'Choose...', isSearchable: false, - isClearable: false + isClearable: false, + noResultsMessage: 'No results found', + noOptionsMessage: 'No options' }; export default Select; diff --git a/packages/pf4-component-mapper/src/common/select/value-container.js b/packages/pf4-component-mapper/src/common/select/value-container.js index 6c9c188ac..3857bd47f 100644 --- a/packages/pf4-component-mapper/src/common/select/value-container.js +++ b/packages/pf4-component-mapper/src/common/select/value-container.js @@ -1,53 +1,13 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { Button } from '@patternfly/react-core'; - -class ValueContainer extends Component { - state = { - showAll: false - }; - render() { - const { isMulti, ...props } = this.props; - const { showAll } = this.state; - if (isMulti && props.children) { - return ( -
    - {showAll ? props.children[0] : props.children[0] && props.children[0][0] ? props.children[0][0] : props.children[0]} - {props.children[0] && props.children[0].length > 1 && ( - - )} - {Array.isArray(props.children) ? props.children[1] && props.children[1] : props.children} -
    - ); - } - - return props.children; - } -} - -ValueContainer.propTypes = { - isMulti: PropTypes.bool, - getStyles: PropTypes.func.isRequired, - children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired, - selectProps: PropTypes.shape({ - showLessLabel: PropTypes.node, - showMoreLabel: PropTypes.node - }) +const ValueContainer = ({ value, placeholder }) => { + return {value || placeholder}; }; -ValueContainer.defaultProps = { - isMulti: false, - selectProps: { - showLessLabel: 'Show less', - showMoreLabel: 'more' - } +ValueContainer.propTypes = { + value: PropTypes.node, + placeholder: PropTypes.node }; export default ValueContainer; diff --git a/packages/pf4-component-mapper/src/tests/form-fields.test.js b/packages/pf4-component-mapper/src/tests/form-fields.test.js index 557195152..e912a5a86 100644 --- a/packages/pf4-component-mapper/src/tests/form-fields.test.js +++ b/packages/pf4-component-mapper/src/tests/form-fields.test.js @@ -417,6 +417,8 @@ describe('FormFields', () => { .first() .props().disabled ).toEqual(true); + } else if (component === componentTypes.SELECT) { + expect(wrapper.find('div.pf-c-select__toggle').prop('disabled')).toEqual(true); } else { expect( wrapper @@ -434,6 +436,11 @@ describe('FormFields', () => { }; const wrapper = mount(); + if (component === componentTypes.SELECT) { + expect(true); + return; + } + if (component === componentTypes.TEXTAREA) { expect( wrapper diff --git a/packages/pf4-component-mapper/src/tests/select/select.test.js b/packages/pf4-component-mapper/src/tests/select/select.test.js index 8dbc0f98f..b75b66df5 100644 --- a/packages/pf4-component-mapper/src/tests/select/select.test.js +++ b/packages/pf4-component-mapper/src/tests/select/select.test.js @@ -1,7 +1,5 @@ import React from 'react'; import { mount } from 'enzyme'; -import { components } from 'react-select'; -import ReactSelect from 'react-select'; import isEqual from 'lodash/isEqual'; import Select from '../../common/select/select'; @@ -13,7 +11,7 @@ describe('', () => { it('should return single simple value', async () => { const wrapper = mount(', () => { it('should return single object value', async () => { const wrapper = mount(', () => { // simulate first return value in state const value = [1]; const wrapper = mount(', () => { /** * select second option */ - const option2 = wrapper - .find('.ddorg__pf4-component-mapper__select__menu--option') - .last() - .find('div') - .last(); + const option2 = wrapper.find('button.pf-c-select__menu-item').last(); await act(async () => { option2.simulate('click'); }); @@ -101,14 +86,11 @@ describe('); + wrapper.find('.pf-c-select__toggle').simulate('click'); /** * select first option */ - const option1 = wrapper - .find('.ddorg__pf4-component-mapper__select__menu--option') - .first() - .find('div') - .last(); + const option1 = wrapper.find('button.pf-c-select__menu-item').first(); await act(async () => { option1.simulate('click'); @@ -116,11 +98,7 @@ describe('', () => { }); it('should expand and close multi value chips', async () => { - const value = [1, 2]; - const wrapper = mount(); - expect(wrapper.find('.ddorg__pf4-component-mapper__select__multivalue--container')).toHaveLength(1); - const expandButton = wrapper.find('button.pf-c-button.pf-m-plain.ddorg__pf4-component-mapper__select__value--container-chipgroup'); + expect(wrapper.find('.pf-c-chip-group')).toHaveLength(1); + expect(wrapper.find('div.pf-c-chip')).toHaveLength(3); + const expandButton = wrapper.find('button.pf-c-chip.pf-m-overflow').last(); await act(async () => { expandButton.simulate('click'); }); wrapper.update(); - expect(wrapper.find('.ddorg__pf4-component-mapper__select__multivalue--container')).toHaveLength(2); + expect(wrapper.find('div.pf-c-chip')).toHaveLength(4); }); it('should call on change when removing chip', async () => { @@ -148,7 +138,7 @@ describe('', () => { { label: 'b', value: 2 } ], placeholder: 'Choose...', - selectVariant: 'default', showLessLabel: 'Show less', showMoreLabel: 'more', simpleValue: true, updatingMessage: 'Loading data...', menuIsPortal: false, value: [1, 2], - loadingMessage: 'Loading...' + loadingMessage: 'Loading...', + noOptionsMessage: 'No options', + noResultsMessage: 'No results found' }); }); it('should load single select Async options correctly', async () => { - const asyncLoading = jest.fn().mockReturnValue(Promise.resolve([{ label: 'label' }])); + const asyncLoading = jest.fn().mockReturnValue(Promise.resolve([{ label: 'label', value: '3' }])); let wrapper; @@ -200,13 +191,10 @@ describe('); }); wrapper.update(); + wrapper.find('.pf-c-select__toggle').simulate('click'); - expect( - wrapper - .find(ReactSelect) - .first() - .instance().props.options - ).toEqual([{ label: 'label' }]); + expect(wrapper.find('button.pf-c-select__menu-item')).toHaveLength(1); + expect(wrapper.find('button.pf-c-select__menu-item').text()).toEqual('label'); }); it('should load multi select Async options correctly and set initial value to undefined', async () => { @@ -229,12 +217,9 @@ describe('', () => { }); wrapper.update(); - expect( - wrapper - .find(ReactSelect) - .first() - .instance().props.options - ).toEqual([{ label: 'label', value: '123' }]); + wrapper.find('.pf-c-select__toggle').simulate('click'); + expect(wrapper.find('button.pf-c-select__menu-item')).toHaveLength(1); + expect(wrapper.find('button.pf-c-select__menu-item').text()).toEqual('label'); expect(onChange).toHaveBeenCalledWith(['123']); }); @@ -286,17 +268,14 @@ describe('); @@ -304,8 +283,9 @@ describe('', () => { wrapper = mount(); }); - let innerSelectProps = wrapper.find(ReactSelect).props().options; + let innerSelectProps = wrapper.find('InternalSelect').props().options; expect(isEqual(innerSelectProps, initialProps.options)).toEqual(true); @@ -347,7 +327,7 @@ describe('', () => { }); wrapper.update(); - let innerSelectProps = wrapper.find(ReactSelect).props().options; + let innerSelectProps = wrapper.find('InternalSelect').props().options; expect(isEqual(innerSelectProps, initialProps.options)).toEqual(true); @@ -368,7 +348,7 @@ describe('