UNPKG

terra-clinical-item-view

Version:

The Item View component allows displays to be organized into rows and column and themed, while providing means to add accessory elements and a comment.

399 lines (345 loc) 12.5 kB
import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import classNamesBind from 'classnames/bind'; import ThemeContext from 'terra-theme-context'; import ItemDisplay from 'terra-clinical-item-display'; import ItemComment from 'terra-clinical-item-display/lib/ItemComment'; import styles from './ItemView.module.scss'; const cx = classNamesBind.bind(styles); const Layouts = { ONE_COLUMN: 'oneColumn', TWO_COLUMNS: 'twoColumns', }; const TextEmphasisTypes = { DEFAULT: 'default', START: 'start', }; const AccessoryAlignments = { ALIGN_TOP: 'alignTop', ALIGN_CENTER: 'alignCenter', }; const propTypes = { /** * The visual column layout in which to present the displays. One of `oneColumn`, `twoColumn`. * When using the `twoColumn` layout, the displays are split between the columns by placing every other display into the second column. */ layout: PropTypes.oneOf(['oneColumn', 'twoColumns']), /** * Determines whether the displays are programmatically separated by row or as true columns when layout is set to `twoColumns`. * Screenreaders will read `trueColumn` displays from top to bottom, one column at a time. * However, this prop defaults to `false` and therefore the screenreader will read the displays in both columns together, left to right, one row at a time. * For accessibility purposes it is recommended to set trueColumn to `true`. */ trueColumn: PropTypes.bool, /** * The text color emphasis when using two columns. One of `default`, `start`. */ textEmphasis: PropTypes.oneOf(['default', 'start']), /** * Option to override the item view default styling so the original styling for item display is shown. */ overrideDefaultStyling: PropTypes.bool, /** * Whether or not all text on the view should be truncated. */ isTruncated: PropTypes.bool, /** * The vertical alignment of the start and end accessories. One of `alignTop`, `alignCenter`. */ accessoryAlignment: PropTypes.oneOf(['alignTop', 'alignCenter']), /** * The react element to be placed in the start aligned accessory position. */ startAccessory: PropTypes.node, /** * Indicates whether or not space is allocated for the start accessory if none is given. */ reserveStartAccessorySpace: PropTypes.bool, /** * The react element to be placed in the end aligned accessory position. */ endAccessory: PropTypes.node, /** * An array of terra-clinical-item-displays to be presented. * The Item View can only have 8 displays at a time, any additional displays passed in will be ignored. */ displays: PropTypes.arrayOf(PropTypes.element), /** * The react element for the comment element. */ comment: PropTypes.node, /** * Function callback for the ref of the outer most div. */ refCallback: PropTypes.func, /** * A string for content's width. Any valid css string. */ contentWidth: PropTypes.string, }; const defaultProps = { layout: Layouts.ONE_COLUMN, trueColumn: false, textEmphasis: TextEmphasisTypes.DEFAULT, overrideDefaultStyling: false, isTruncated: false, accessoryAlignment: AccessoryAlignments.ALIGN_CENTER, startAccessory: undefined, reserveStartAccessorySpace: false, endAccessory: undefined, displays: [], comment: undefined, contentWidth: undefined, }; const renderAccessory = (accessory, reserveSpace, accessoryAlignment, type) => { let accessorySection; if (accessory || reserveSpace) { const accessoryClassNames = cx( 'accessory', `${type}-accessory`, { 'accessory-align-center': accessoryAlignment === AccessoryAlignments.ALIGN_CENTER }, { 'accessory-align-top': accessoryAlignment === AccessoryAlignments.ALIGN_TOP }, ); accessorySection = ( <div className={accessoryClassNames}> {accessory} </div> ); } return accessorySection; }; const defaultEmphasisContentClassesFromIndexes = (rowIndex, rowCount) => { let contentSize = 'content-primary-size'; let contentColor = 'content-primary-color'; if (rowIndex > 0) { contentSize = 'content-secondary-size'; } if (rowIndex >= 2 || (rowCount === 2 && rowIndex === 1)) { contentColor = 'content-secondary-color'; } return [contentSize, contentColor]; }; const startEmphasisContentClassesFromIndexes = (rowIndex, rowCount, contentIndex) => { if (contentIndex > 0 || rowIndex >= 2) { return ['content-secondary-size', 'content-secondary-color']; } return defaultEmphasisContentClassesFromIndexes(rowIndex, rowCount); }; const classesForContent = (rowIndex, rowCount, contentIndex, emphasis) => { let classes; if (emphasis === TextEmphasisTypes.START) { classes = startEmphasisContentClassesFromIndexes(rowIndex, rowCount, contentIndex); } else { classes = defaultEmphasisContentClassesFromIndexes(rowIndex, rowCount); } return ['content'].concat(classes); }; const renderRow = (row, rowIndex, rowCount, emphasis, overrideDefaultStyling, contentWidth) => { const rowKey = rowIndex; const style = contentWidth && { width: contentWidth }; return ( <li className={cx('row')} key={rowKey}> <ul className={cx('row-list')} key={rowKey}> {row.map((display, displayIndex) => { const displayKey = displayIndex; const contentClasses = overrideDefaultStyling ? 'content' : classesForContent(rowIndex, rowCount, displayIndex, emphasis); return ( // eslint-disable-next-line react/forbid-dom-props <li style={style} className={cx(contentClasses)} key={displayKey}> {display} </li> ); })} </ul> </li> ); }; const renderTwoColumns = (displayGroup, displayGroupIndex, emphasis, overrideDefaultStyling, contentWidth) => { const columnKey = displayGroupIndex; const displayCount = displayGroup.length; const containerStyling = displayGroupIndex === 0 ? 'primary-column' : 'secondary-column'; const style = contentWidth && { width: contentWidth }; return ( <li className={cx(containerStyling)} key={columnKey}> <ul className={cx('column-list')} key={columnKey}> {displayGroup.map((display, contentIndex) => { const contentKey = contentIndex; const contentClasses = overrideDefaultStyling ? 'content' : classesForContent(contentIndex, displayCount, displayGroupIndex, emphasis); return ( // eslint-disable-next-line react/forbid-dom-props <li style={style} className={cx(contentClasses)} key={contentKey}> {display} </li> ); })} </ul> </li> ); }; const renderColumn = (displays, emphasis, overrideDefaultStyling, contentWidth) => { const displayCount = displays.length; const style = contentWidth && { width: contentWidth }; return ( <div> <ul className={cx('column-list-container')}> {displays.map((display, displayIndex) => { const contentKey = displayIndex; /** * We are only ever rendering one column so zero is being passed into classesForContent for the contentIndex variable. * classesForContent would usually take in an index for that but in this case we don't have one explicitly. */ const contentClasses = overrideDefaultStyling ? 'content' : classesForContent(displayIndex, displayCount, 0, emphasis); return ( // eslint-disable-next-line react/forbid-dom-props <li style={style} className={cx(contentClasses)} key={contentKey}> {display} </li> ); })} </ul> </div> ); }; const renderByRowView = (displays, emphasis, overrideDefaultStyling, contentWidth) => { const displayGroups = []; while (displays.length) { displayGroups.push(displays.splice(0, 2)); } return ( <div> <ul className={cx('row-list-container')}> {displayGroups.map((displayRow, rowIndex) => { const row = renderRow(displayRow, rowIndex, displayGroups.length, emphasis, overrideDefaultStyling, contentWidth); return row; })} </ul> </div> ); }; const renderSingleDisplayView = (singleDisplay, overrideDefaultStyling, contentWidth) => { /** * Since this is always a singular display, the content styling will be the primary defaults if they are not overridden. * We don't have to call into the classesForContent method and instead can just set the primary size and color here. */ const contentClass = overrideDefaultStyling ? 'content' : ['content', 'content-primary-size', 'content-primary-color']; const style = contentWidth && { width: contentWidth }; return ( <div className={cx('single-result-column-container')}> {/* eslint-disable-next-line react/forbid-dom-props */} <div style={style} className={cx(contentClass)}> {singleDisplay} </div> </div> ); }; const twoColumnGrouping = (displays) => { let count = 0; const displayGroups = []; const primaryColumn = []; const secondaryColumn = []; while (displays.length) { count += 1; if (count % 2 === 0) { secondaryColumn.push(displays.splice(0, 1)); } else { primaryColumn.push(displays.splice(0, 1)); } } displayGroups.push(primaryColumn); displayGroups.push(secondaryColumn); return displayGroups; }; const renderView = (displays, layout, emphasis, overrideDefaultStyling, trueColumn, contentWidth) => { if (displays === null || displays === undefined || !displays.length) { return undefined; } const displaysSlice = displays.slice(0, 8); /** * If there is only one display we don't want to return it as an item in a list. * The method renderSingleDisplayView here takes in the single display and returns it within simple divs instead. */ if (displaysSlice.length === 1) { return renderSingleDisplayView(displaysSlice, overrideDefaultStyling, contentWidth); } if (layout === Layouts.TWO_COLUMNS) { if (!trueColumn) { return renderByRowView(displaysSlice, emphasis, overrideDefaultStyling, contentWidth); } const displayGroups = twoColumnGrouping(displaysSlice); return ( <div> <ul className={cx('column-list-container')}> {displayGroups.map((group, index) => { const column = renderTwoColumns(group, index, emphasis, overrideDefaultStyling, contentWidth); return column; })} </ul> </div> ); } return ( <div> {renderColumn(displaysSlice, emphasis, overrideDefaultStyling, contentWidth)} </div> ); }; const isDisplaysTruncated = (displays) => { if (displays) { const displaysSlice = displays.slice(0, 8); for (let i = 0; i < displaysSlice.length; i += 1) { if (displaysSlice[i]?.props.isTruncated === true) { return true; } } } return false; }; const ItemView = ({ layout, trueColumn, textEmphasis, overrideDefaultStyling, isTruncated, accessoryAlignment, startAccessory, reserveStartAccessorySpace, endAccessory, displays, comment, refCallback, contentWidth, ...customProps }) => { const theme = React.useContext(ThemeContext); const isTrueTwoColumnView = layout === Layouts.TWO_COLUMNS && trueColumn; const isTruncatedDisplay = isTruncated || isDisplaysTruncated(displays); const viewClassNames = classNames( cx( 'item-view', { 'is-truncated': isTruncated }, { 'one-column': layout === Layouts.ONE_COLUMN }, { 'two-columns': isTrueTwoColumnView && !isTruncatedDisplay }, { 'truncated-two-columns': isTrueTwoColumnView && isTruncatedDisplay }, { 'two-columns-by-row': layout === Layouts.TWO_COLUMNS && !trueColumn }, theme.className, ), customProps.className, ); return ( <div {...customProps} className={viewClassNames} ref={refCallback}> {renderAccessory(startAccessory, reserveStartAccessorySpace, accessoryAlignment, 'start')} <div className={cx('body')}> {renderView(displays, layout, textEmphasis, overrideDefaultStyling, trueColumn, contentWidth)} {comment} </div> {renderAccessory(endAccessory, false, accessoryAlignment, 'end')} </div> ); }; ItemView.propTypes = propTypes; ItemView.defaultProps = defaultProps; ItemView.Display = ItemDisplay; ItemView.Comment = ItemComment; export default ItemView; export { Layouts, TextEmphasisTypes, AccessoryAlignments, };