diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderAdd/ConditionBuilderAdd.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderAdd/ConditionBuilderAdd.js new file mode 100644 index 0000000000..bc1b6931ff --- /dev/null +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderAdd/ConditionBuilderAdd.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { AddAlt } from '@carbon/react/icons'; +import { ConditionBuilderButton } from '../ConditionBuilderButton/ConditionBuilderButton'; +import PropTypes from 'prop-types'; +import { + blockClass, + translateWithId, +} from '../ConditionBuilderContext/DataConfigs'; + +const ConditionBuilderAdd=({ className, onClick })=> { + return ( +
+ +
+ ); +} + +export default ConditionBuilderAdd; + +ConditionBuilderAdd.propTypes = { + /** + * Provide an optional class to be applied to the containing node. + */ + className: PropTypes.string, + + /** + * callback triggered on click of add button + */ + onClick: PropTypes.func, +}; diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderButton/ConditionBuilderButton.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderButton/ConditionBuilderButton.js new file mode 100644 index 0000000000..d7f4d8ba67 --- /dev/null +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderButton/ConditionBuilderButton.js @@ -0,0 +1,91 @@ +import React from 'react'; +import cx from 'classnames'; + +import PropTypes from 'prop-types'; +import { Tooltip } from '@carbon/react'; +import { blockClass } from '../ConditionBuilderContext/DataConfigs'; + +export const ConditionBuilderButton = ({ + className, + label, + hideLabel, + tooltipAlign, + renderIcon: Icon, + onClick, + showToolTip, + role, + ...rest +}) => { + const Button = () => { + return ( + + ); + }; + + return hideLabel || showToolTip ? ( + + {Button()} + + ) : ( + <>{Button()} + ); +}; + +ConditionBuilderButton.propTypes = { + /** + * Provide an optional class to be applied to the containing node. + */ + className: PropTypes.string, + + /** + * decides if label and tooltip to be hidden + */ + hideLabel: PropTypes.bool, + /** + * label of the button + */ + label: PropTypes.string, + /** + * callback triggered on click of add button + */ + onClick: PropTypes.func, + /** + * Optional prop to allow overriding the icon rendering. + */ + renderIcon: PropTypes.func, + /** + *optional string defines the role of the button + */ + role: PropTypes.string, + /** + *decides if tooltip to be shown + */ + showToolTip: PropTypes.bool, + /** + * tooltip position + */ + tooltipAlign: PropTypes.string, +}; diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js index 91f6bca2f9..9a1d72b8f8 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js @@ -1,38 +1,47 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { pkg } from '../../../settings'; +import React, { + useCallback, + useContext, + useEffect, + useState, +} from 'react'; import PropTypes from 'prop-types'; +import ConditionBuilderGroup from '../ConditionBuilderGroup/ConditionBuilderGroup'; import { Button } from '@carbon/react'; import { Add } from '@carbon/react/icons'; import { ConditionBuilderContext, emptyState, } from '../ConditionBuilderContext/DataTreeContext'; +import { blockClass } from '../ConditionBuilderContext/DataConfigs'; -const blockClass = `${pkg.prefix}--condition-builder`; - -function ConditionBuilderContent({ startConditionLabel }) { +const ConditionBuilderContent = ({ + startConditionLabel, + conditionBuilderRef, +}) => { const { rootState, setRootState } = useContext(ConditionBuilderContext); const [isConditionBuilderActive, setIsConditionBuilderActive] = useState(true); - const conditionBuilderRef = useRef(); - useEffect(() => { if (rootState?.groups?.length) { setIsConditionBuilderActive(false); - if ( - rootState.groups[0].conditions.length == 1 && - rootState.groups[0].conditions[0].property == undefined - ) { - // when the add condition clicked to start the condition building, we by default open the popover of the first property - setTimeout(() => { - conditionBuilderRef.current.querySelector('.propertyField').click(); - }, 0); - } } else { setIsConditionBuilderActive(true); } - }, [rootState]); + }, [rootState, conditionBuilderRef]); + useEffect(() => { + if (isConditionBuilderActive == false) { + if (conditionBuilderRef.current) { + const initial = conditionBuilderRef.current.querySelector( + '[role="gridcell"] button' + ); + + if (initial) { + initial.setAttribute('tabindex', '0'); + } + } + } + }, [isConditionBuilderActive, conditionBuilderRef]); const onStartConditionBuilder = () => { //when add condition button is clicked. setIsConditionBuilderActive(false); @@ -40,11 +49,19 @@ function ConditionBuilderContent({ startConditionLabel }) { //or we can even pre-populate some existing builder and continue editing }; + const onRemove = useCallback( + (groupIndex) => { + setRootState({ + ...rootState, + groups: rootState.groups.filter( + (group, gIndex) => groupIndex !== gIndex + ), + }); + }, + [setRootState, rootState] + ); return ( -
+
{isConditionBuilderActive && (
); -} +}; export default ConditionBuilderContent; ConditionBuilderContent.propTypes = { + /** + * ref of condition builder + */ + conditionBuilderRef: PropTypes.object, /** * Provide a label to the button that starts condition builder */ diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataConfigs.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataConfigs.js index ffa238fa85..d4edd1cf58 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataConfigs.js +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataConfigs.js @@ -1,4 +1,6 @@ -import { translationsObject } from './Translations'; +import { pkg } from '../../../settings'; +import { translationsObject } from './translationObject'; + export const statementConfig = [ { label: 'if', @@ -83,8 +85,10 @@ export const operatorConfig = [ type: 'date', }, ]; +// The block part of our conventional BEM class names (blockClass__E--M). +export const blockClass=`${pkg.prefix}--condition-builder`; -function formatDate(date) { +const formatDate=(date)=> { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataTreeContext.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataTreeContext.js index b4c6ab1680..e8ef4624cc 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataTreeContext.js +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/DataTreeContext.js @@ -7,7 +7,7 @@ export const emptyState = { groupSeparateOperator: null, groupOperator: 'and', statement: 'if', - conditions: [{ property: undefined, operator: '', value: '' }], + conditions: [{ property: undefined, operator: '', value: '',popoverToOpen:'propertyField' }], }, ], }; diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/Translations.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/Translations.js deleted file mode 100644 index 6d2f786f14..0000000000 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/Translations.js +++ /dev/null @@ -1,21 +0,0 @@ -export const translationsObject = { - en: { - if: 'if', - 'excl-if': 'excl.if', - and: 'and', - or: 'or', - is: 'is', - greater: 'is greater than', - 'greater-equal': 'is greater than or equal to', - lower: 'is lower than', - 'lower-equal': 'is lower than or equal to', - 'starts-with': 'starts with', - 'ends-with': 'ends with', - contains: 'contains', - 'one-of': 'is one of', - before: 'is before', - after: 'is after', - between: 'is between', - 'add-condition': 'Add Condition', - }, -}; diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/translationObject.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/translationObject.js new file mode 100644 index 0000000000..25195daf09 --- /dev/null +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContext/translationObject.js @@ -0,0 +1,28 @@ +export const translationsObject = { + en: { + if: 'if', + 'excl-if': 'excl.if', + and: 'and', + or: 'or', + is: 'is', + greater: 'is greater than', + 'greater-equal': 'is greater than or equal to', + lower: 'is lower than', + 'lower-equal': 'is lower than or equal to', + 'starts-with': 'starts with', + 'ends-with': 'ends with', + contains: 'contains', + 'one-of': 'is one of', + before: 'is before', + after: 'is after', + between: 'is between', + 'add-condition': 'Add Condition', + 'remove-condition':'Remove Condition', + condition:'Condition', + property:'Property', + operator:'Operator', + connector:'Connector' + + }, + }; + \ No newline at end of file diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderGroup/ConditionBuilderGroup.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderGroup/ConditionBuilderGroup.js new file mode 100644 index 0000000000..4256fbbb90 --- /dev/null +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderGroup/ConditionBuilderGroup.js @@ -0,0 +1,178 @@ +import React from 'react'; +import ConditionBlock from '../ConditionBlock/ConditionBlock'; +import ConditionBuilderAdd from '../ConditionBuilderAdd/ConditionBuilderAdd'; +import PropTypes from 'prop-types'; +import { blockClass } from '../ConditionBuilderContext/DataConfigs'; + +/** + * + * state - this is the current group that is being rendered . This can be a inner group or outer group + * All the inner components of group are called from here. + * @returns + */ +const ConditionBuilderGroup = ({ state, aria, onRemove, onChange,conditionBuilderRef }) => { + //This method identify whether the condition is the last one , so that add button can be shown + const isLastCondition = (conditionIndex, conditionArr) => { + return conditionIndex + 1 >= conditionArr.length; + }; + const onRemoveHandler = (conditionIndex, e) => { + if (state.conditions.length > 1) { + onChange({ + ...state, + conditions: state.conditions.filter( + (condition, cIndex) => conditionIndex !== cIndex + ), + }); + handleFocusOnClose(e); + } else { + onRemove(); + } + + + }; + + const onChangeHandler = (updatedConditions, conditionIndex) => { + onChange({ + ...state, + conditions: state.conditions.map((condition, cIndex) => + conditionIndex === cIndex ? updatedConditions : condition + ), + }); + }; + + const addConditionHandler = (conditionIndex) => { + onChange({ + ...state, + conditions: [ + ...state.conditions.slice(0, conditionIndex + 1), + { + property: undefined, + operator: '', + value: '', + popoverToOpen:'propertyField' + }, + ...state.conditions.slice(conditionIndex + 1), + ], + }); + }; + + const handleFocusOnClose = (e) => { + let previousClose = e.currentTarget + ?.closest('[role="row"]') + ?.previousSibling?.querySelector('[data-name="closeCondition"]'); + + if (previousClose) { + previousClose.focus(); + } + }; + return ( +
+
+ {/* condition loop starts here */} + + {state?.conditions?.map( + (eachCondition, conditionIndex, conditionArr) => ( + <> + {/* This condition is for tree variant where there will be subgroups inside each group */} + {eachCondition.conditions && ( + { + onChangeHandler(updatedConditions, conditionIndex); + }} + onRemove={(e) => { + onRemoveHandler(conditionIndex, e); + }} + conditionBuilderRef={conditionBuilderRef} + /> + )} + {/* rendering each condition block */} + {!eachCondition.conditions && ( + <> + 0 ? state.groupOperator : undefined + } + aria={{ + level: aria.level + 1, + posinset: conditionIndex + 1, + setsize: state.conditions.length, + }} + isStatement={conditionIndex == 0} + state={eachCondition} + group={state} + conditionIndex={conditionIndex} + className={`${blockClass}__gap ${blockClass}__gap-bottom`} + onChange={(updatedConditions) => { + onChangeHandler(updatedConditions, conditionIndex); + }} + onRemove={(e) => { + onRemoveHandler(conditionIndex, e); + }} + onConnectorOperatorChange={(op) => { + onChange({ + ...state, + groupOperator: op, + }); + }} + onStatementChange={(updatedStatement) => { + onChange({ + ...state, + statement: updatedStatement, + }); + }} + /> + {/* for last condition shows add button */} + {isLastCondition(conditionIndex, conditionArr) && ( + { + addConditionHandler(conditionIndex); + }} + className={ + !isLastCondition(conditionIndex, conditionArr) + ? `${blockClass}__gap ${blockClass}__gap-bottom` + : '' + } + /> + )} + + )} + + ) + )} +
+
+ ); +}; + +export default ConditionBuilderGroup; +ConditionBuilderGroup.propTypes = { + /** + * object contains the aria attributes + */ + aria: PropTypes.object, + + /** + * ref of condition builder + */ + conditionBuilderRef: PropTypes.object, + + /** + + * callback to update the current condition of the state tree + */ + onChange: PropTypes.func, + /** + * call back to remove the particular group from the state tree + */ + onRemove: PropTypes.func, + /** + * state defines the current group + */ + state: PropTypes.object, +}; diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderItem/ConditionBuilderItem.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderItem/ConditionBuilderItem.js new file mode 100644 index 0000000000..4a66e10051 --- /dev/null +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderItem/ConditionBuilderItem.js @@ -0,0 +1,156 @@ +import React, { useState, useRef, useEffect } from 'react'; + +import { Popover, PopoverContent, Layer } from '@carbon/react'; +import PropTypes from 'prop-types'; +import { Add } from '@carbon/react/icons'; + +import { ConditionBuilderButton } from '../ConditionBuilderButton/ConditionBuilderButton'; +import { + blockClass, + translateWithId, + valueRenderers, +} from '../ConditionBuilderContext/DataConfigs'; + +export const ConditionBuilderItem = ({ + children, + className, + label, + renderIcon, + title, + type, + showToolTip, + state, + ...rest +}) => { + const contentRef = useRef(null); + const [propertyLabel, setPropertyLabel] = useState(label); + const [open, setOpen] = useState(false); + + useEffect(() => { + const propertyId = + rest['data-name'] == 'valueField' && type + ? valueRenderers[type](label) + : label; + setPropertyLabel(translateWithId(propertyId)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [label]); + + useEffect(() => { + if (state) { + let currentField=rest['data-name']; + //if any condition is changed, state prop is triggered + if (state.popoverToOpen && currentField !== state.popoverToOpen) { + setOpen(false); + } else if ( + currentField == 'valueField' && + type == 'option' && + state.operator !== 'one-of' + ) { + setOpen(false); + } + if (state.popoverToOpen == currentField) { + setOpen(true); + } + } else { + setOpen(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state, label]); + + useEffect(() => { + if (open && contentRef.current) { + const firstFocusableElement = + contentRef.current.querySelector('input, button,li'); + if (firstFocusableElement) { + firstFocusableElement.focus(); + } + } + }, [contentRef, open]); + + return ( + { + setOpen(false); + }} + > + { + setOpen(!open); + }} + role="gridcell" + className={className} + aria-haspopup + aria-expanded={open} + renderIcon={renderIcon ? renderIcon : label == undefined ? Add : ''} + showToolTip={showToolTip} + {...rest} + /> + + + +

+ {title} +

+
{open && children}
+
+
+
+ ); +}; + +ConditionBuilderItem.propTypes = { + /** + * provide the contents of the popover + */ + children: PropTypes.node, + /** + * Provide an optional class to be applied to the containing node. + */ + className: PropTypes.string, + /** + * boolean to keep open/close popover + */ + + /** + * text to be displayed in the field + */ + label: PropTypes.string, + + /** + * popover default state + */ + + /** + popoverState: PropTypes.string, + * Optional prop to allow overriding the icon rendering. + */ + renderIcon: PropTypes.func, + + /** + * show tool tip + */ + showToolTip: PropTypes.bool, + + /** + * current condition state object + */ + state: PropTypes.object, + + /** + showToolTip: PropTypes.bool, + * title of the popover + */ + title: PropTypes.string, + /** + * input type + */ + type: PropTypes.string, +}; diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionConnector/ConditionConnector.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionConnector/ConditionConnector.js new file mode 100644 index 0000000000..5854dccb33 --- /dev/null +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionConnector/ConditionConnector.js @@ -0,0 +1,64 @@ +import React, { useCallback } from 'react'; +import { ConditionBuilderItem } from '../ConditionBuilderItem/ConditionBuilderItem'; +import PropTypes from 'prop-types'; +import { blockClass, translateWithId } from '../ConditionBuilderContext/DataConfigs'; + +const ConditionConnector=({ operator, className })=> { + const handleConnectorHover = useCallback((e, isHover) => { + let parentGroup = e.currentTarget.closest('.eachGroup'); + if (isHover) { + parentGroup.classList.add('hoveredConnector'); + } else { + parentGroup.classList.remove('hoveredConnector'); + } + }, []); + return ( +
+ { + handleConnectorHover(e, true); + }} + onMouseLeave={(e) => { + handleConnectorHover(e, false); + }} + onFocus={(e) => { + handleConnectorHover(e, true); + }} + onBlur={(e) => { + handleConnectorHover(e, false); + }} + className={`${blockClass}__connector-button `} + > + {/* onChange(op)} + /> */} + +
+ ); +} + +export default ConditionConnector; +ConditionConnector.propTypes = { + /** + * Provide an optional class to be applied to the containing node. + */ + className: PropTypes.string, + + /** + * callback to update the current condition of the state tree + */ + onChange: PropTypes.func, + + /** + * string that defines the connector operator (and/or) + */ + operator: PropTypes.string, +};