react-cm-ui
Version:
React UI for Healthy Church
555 lines (497 loc) • 29.2 kB
JSX
import {
forEach,
isArray,
isEmpty,
map,
} from 'lodash';
import ClassNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { GRID_SIZES } from '../../layout/grid/gridConstants';
import {
UI_CLASS_NAME,
} from '../../global/constants';
import ActionBarActionsButton from './actionBarActionsButton'; // eslint-disable-line import/no-cycle
import ActionBarSearch from './actionBarSearch';
import Button from '../../inputs/button';
import DropdownButton from '../../inputs/dropdownButton';
import Grid from '../../layout/grid';
import Icon from '../../dataDisplay/icon';
import List from '../../dataDisplay/list';
import Utils from '../../utils/utils';
const searchPropTypes = PropTypes.shape({
autoFocus: PropTypes.bool,
onFocus: PropTypes.func,
});
const propTypes = {
children: PropTypes.element,
className: PropTypes.string,
columns: PropTypes.arrayOf(PropTypes.shape({
button: PropTypes.shape({}),
dropdownButton: PropTypes.shape({
className: PropTypes.string,
color: PropTypes.oneOf(Utils.colorEnums()),
disable: PropTypes.bool,
iconType: PropTypes.oneOf(['chevron-down', 'plus']),
id: PropTypes.string,
onClick: PropTypes.func,
style: PropTypes.shape({}),
title: PropTypes.string,
}),
jsx: PropTypes.node,
lg: PropTypes.oneOf(GRID_SIZES),
list: PropTypes.arrayOf(PropTypes.shape({
actionsButton: PropTypes.shape({}),
divide: PropTypes.bool,
jsx: PropTypes.node,
iconBack: PropTypes.shape({}),
iconFilter: PropTypes.shape({}),
iconGrid: PropTypes.shape({}),
iconSearch: searchPropTypes,
iconTable: PropTypes.shape({}),
})),
md: PropTypes.oneOf(GRID_SIZES),
search: searchPropTypes,
sm: PropTypes.oneOf(GRID_SIZES),
xl: PropTypes.oneOf(GRID_SIZES),
})),
/**
* Used for DOM testing. https://testing-library.com/docs/queries/bytestid/
*/
dataTestId: PropTypes.string,
id: PropTypes.string,
/**
* Defines the `justify-content` style property.
* It is applied for all screen sizes.
*/
justifyContent: PropTypes.oneOf([
'center',
'flex-end',
'flex-start',
'space-around',
'space-between',
'space-evenly',
]),
moduleType: PropTypes.oneOf(['drawer', 'page']),
style: PropTypes.shape({}), // eslint-disable-line react/forbid-prop-types
};
const defaultProps = {
children: undefined,
className: undefined,
columns: [],
dataTestId: `${UI_CLASS_NAME}-action_bar`,
id: undefined,
justifyContent: 'flex-start',
moduleType: undefined,
style: {},
};
const HAS_DRAWER_ACTION_BAR_CLASS_NAME = 'drawer-has_action_bar';
const HAS_PAGE_ACTION_BAR_CLASS_NAME = 'page-has_action_bar';
const HAS_DRAWER_CONTENT_ACTION_BAR_MOBILE_SEARCH_VISIBLE_CLASS_NAME = 'drawer--content-has_action_bar_mobile_search_visible';
const HAS_PAGE_CONTAINER_ACTION_BAR_MOBILE_SEARCH_VISIBLE_CLASS_NAME = 'page--container-has_action_bar_mobile_search_visible';
class ActionBar extends React.PureComponent {
constructor() {
super();
this.state = {
isMobileSearchVisible: false,
};
this.onMobileSearchIconToggle = this.onMobileSearchIconToggle.bind(this);
}
componentDidMount() {
const {
moduleType,
} = this.props;
if (this.actionBarRef) {
const closestDrawer = this.actionBarRef.closest('.ui.drawer');
if (moduleType === 'drawer' && closestDrawer) {
closestDrawer.classList.add(HAS_DRAWER_ACTION_BAR_CLASS_NAME);
}
const closestPage = this.actionBarRef.closest('.ui.page');
if (moduleType === 'page' && closestPage) {
closestPage.classList.add(HAS_PAGE_ACTION_BAR_CLASS_NAME);
}
this.setMobileSearchAutoFocus();
}
}
componentWillUnmount() {
const { moduleType } = this.props;
if (this.actionBarRef) {
const closestDrawer = this.actionBarRef.closest('.ui.drawer');
if (moduleType === 'drawer' && closestDrawer) {
closestDrawer.classList.remove(HAS_DRAWER_ACTION_BAR_CLASS_NAME);
}
}
}
onMobileSearchIconToggle() {
this.setState((prevState) => ({
isMobileSearchVisible: !prevState.isMobileSearchVisible,
}), () => {
const { moduleType } = this.props;
const { isMobileSearchVisible } = this.state;
if (this.actionBarRef) {
const closestDrawer = this.actionBarRef.closest('.ui.drawer');
if (moduleType === 'drawer' && closestDrawer) {
if (isMobileSearchVisible) {
closestDrawer
.querySelector('.ui.drawer--content')
.classList
.add(
HAS_DRAWER_CONTENT_ACTION_BAR_MOBILE_SEARCH_VISIBLE_CLASS_NAME,
);
} else {
closestDrawer
.querySelector('.ui.drawer--content')
.classList
.remove(
HAS_DRAWER_CONTENT_ACTION_BAR_MOBILE_SEARCH_VISIBLE_CLASS_NAME,
);
}
}
const closestPage = this.actionBarRef.closest('.ui.page');
if (moduleType === 'page' && closestPage) {
if (isMobileSearchVisible) {
closestPage
.querySelector('.ui.page--container')
.classList
.add(
HAS_PAGE_CONTAINER_ACTION_BAR_MOBILE_SEARCH_VISIBLE_CLASS_NAME,
);
} else {
closestPage
.querySelector('.ui.page--container')
.classList
.remove(
HAS_PAGE_CONTAINER_ACTION_BAR_MOBILE_SEARCH_VISIBLE_CLASS_NAME,
);
}
}
}
});
}
setMobileSearchAutoFocus() {
const {
columns,
} = this.props;
let hasEventBeenCalled = false;
forEach(columns, (c) => {
if (c.list) {
forEach(c.list, (l) => {
if (!hasEventBeenCalled && l.iconSearch?.autoFocus) {
this.onMobileSearchIconToggle();
hasEventBeenCalled = true;
}
});
}
});
}
render() {
const {
children,
className,
columns,
dataTestId,
id,
justifyContent,
moduleType,
style,
} = this.props;
const { isMobileSearchVisible } = this.state;
const containerClasses = ClassNames(
'ui',
`${moduleType}--action_bar`,
'action_bar',
className,
);
let searchDataForMobile = null;
let gridColumnKeyNum = 1;
let gridColumnListItemKeyNum = 1;
return (
<header
className={containerClasses}
data-testid={dataTestId}
id={id}
ref={(ref) => { this.actionBarRef = ref; }}
style={style}
>
<div style={{ width: '100%' }}>
{!isEmpty(columns) && (
<React.Fragment>
<Grid
alignItems="center"
className="action_bar--grid"
flexWrap="nowrap"
justifyContent={justifyContent}
spacing={2}
>
{map(columns, (column) => {
const {
button,
dropdownButton,
columns, // eslint-disable-line no-shadow
jsx,
search,
} = column;
let { list } = column;
const gridColumnClasses = ClassNames('action_bar--grid_column', column.className);
const gridColumnKey = `action_bar--grid_column-${gridColumnKeyNum++}`; // eslint-disable-line no-plusplus
if (columns) {
console.warn('ActionsBar\'s columns.columns is deprecated. Please use columns.list instead.'); // eslint-disable-line no-console
list = [...columns];
}
if (!button && !dropdownButton && !jsx && !list && !search) {
console.warn( // eslint-disable-line no-console
`${'<Page.ActionsBar>\'s column object must have one of the ' +
'following properties: button, jsx, or list.' +
'Please check this column object '}${JSON.stringify(column)}`,
);
return false;
}
let dropdownButtonOptionKeyNum = 1;
return (
<Grid.Column
className={gridColumnClasses}
key={gridColumnKey}
lg={column.lg}
md={column.md}
sm={column.sm}
/**
* NOTE: Remove `flexBasis`, `flexGrow`, `flexShrink`,
* and `width` when we finally remove the deprecated
* style prop.
*/
style={({
flexBasis: column.flexBasis || null,
flexGrow: column.flexGrow || null,
flexShrink: column.flexShrink || null,
width: 'auto',
...column.style,
})}
xl={column.xl}
>
{button && (
<Button
color={button.color}
designVersion={1}
disable={!!button.disable || !!button.disabled}
icon={button.iconType && !button.label}
id={button.id}
onClick={button.onClick}
style={button.style}
>
{button.iconType &&
<Icon type={button.iconType} />}
{button.label &&
<span>{button.label}</span>}
</Button>
)}
{dropdownButton && (
<DropdownButton
className={dropdownButton.className}
color={dropdownButton.color}
designVersion={1}
disable={dropdownButton.disable}
iconType={dropdownButton.iconType}
id={dropdownButton.id}
label={dropdownButton.label}
onClick={dropdownButton.onClick}
style={dropdownButton.style}
title={dropdownButton.title}
>
{map(dropdownButton.options, (option) => {
dropdownButtonOptionKeyNum += 1;
return (
<DropdownButton.Option
className={option.className}
disable={!!option.disable || !!option.disabled}
id={option.id}
key={`action_bar_dropdown_button_option-${dropdownButtonOptionKeyNum}`}
label={option.label}
onClick={option.onClick}
onKeyDown={option.onKeyDown}
style={option.style}
tabIndex={option.tabIndex}
/>
);
})}
</DropdownButton>
)}
{isArray(list) && (
<List
className="action_bar--list"
horizontal
>
{map(list, (item) => {
const {
actionsButton,
divide,
jsx: listJsx,
iconBack,
iconFilter,
iconGrid,
iconSearch,
iconTable,
} = item;
const listItemClassName = ClassNames('action_bar--list_item', {
'action_bar--list_item-jsx': listJsx,
'action_bar--list_item-icon_filter': iconFilter,
'action_bar--list_item-icon_grid': iconGrid,
'action_bar--list_item-icon_table': iconTable,
});
const itemKey = `action_bar--list_item-${gridColumnListItemKeyNum++}`; // eslint-disable-line no-plusplus
if (!actionsButton &&
!listJsx &&
!iconBack &&
!iconFilter &&
!iconGrid &&
!iconSearch &&
!iconTable
) {
console.warn( // eslint-disable-line no-console, max-len
`${'<Page.ActionsBar>\'s column.list object must have one of the ' +
'following properties: actionsButton, iconBack, iconFilter, iconGrid, or iconTable.' +
'Please check this column.list object '}${JSON.stringify(item)}`,
);
return false;
}
if (iconSearch) {
/* eslint-disable max-len */
/* eslint-disable react/jsx-props-no-spreading */
searchDataForMobile = (
<ActionBarSearch
{...iconSearch}
isMobileSearch
isMobileSearchVisible={isMobileSearchVisible}
type="mobileSearch"
/>
);
/* eslint-enable react/jsx-props-no-spreading */
/* eslint-enable max-len */
}
return (
<List.Item
className={listItemClassName}
divide={!!divide}
key={itemKey}
style={({
alignItems: item.alignItems ||
'flex-start',
flexBasis: item.flexBasis ||
'auto',
flexGrow: item.flexGrow || 0,
flexShrink: item.flexShrink ||
0,
width: 'auto',
...list.style,
})}
>
{/* eslint-disable max-len */}
{actionsButton && (
<ActionBarActionsButton
className={actionsButton.className}
iconBackgroundColor={actionsButton.iconBackgroundColor}
iconBackgroundActiveColor={actionsButton.iconBackgroundActiveColor}
iconType={actionsButton.iconType}
id={actionsButton.id}
isMobileSearchVisible={isMobileSearchVisible}
header={actionsButton.header}
moduleType={moduleType}
options={actionsButton.options}
style={actionsButton.style}
/>
)}
{/* eslint-enable max-len */}
{iconBack && (
<Icon
color={iconBack.selected ?
'highlight' :
null}
id={iconBack.id}
onClick={iconBack.onClick}
title={iconBack.title || 'Back'}
type="chevron-left"
/>
)}
{/* eslint-disable max-len */}
{iconFilter && (
<Icon
color={iconFilter.selected || iconFilter.isFiltering ?
'highlight' :
null}
id={iconFilter.id}
onClick={iconFilter.onClick}
title={iconFilter.title || 'Filter'}
type="filter"
/>
)}
{/* eslint-enable max-len */}
{iconGrid && (
<Icon
color={iconGrid.selected ?
'highlight' :
null}
id={iconGrid.id}
onClick={iconGrid.onClick}
title={iconGrid.title || 'Grid View'}
type="grid"
/>
)}
{/* eslint-disable max-len */}
{iconSearch && (
<Icon
className="action_bar--search_icon"
color={isMobileSearchVisible ?
'highlight' :
null}
id={iconSearch.id}
onClick={this.onMobileSearchIconToggle}
title={iconSearch.title || 'Search'}
type="search"
/>
)}
{/* eslint-enable max-len */}
{iconTable && (
<Icon
color={iconTable.selected ?
'highlight' :
null}
id={iconTable.id}
onClick={iconTable.onClick}
title={iconTable.title || 'Table View'}
type="list"
/>
)}
{listJsx}
</List.Item>
);
})}
</List>
)}
{search && (
<ActionBarSearch
autoFocus={search.autoFocus}
id={search.id}
onChange={search.onChange}
onClearClick={search.onClearClick}
onFocus={search.onFocus}
onKeyDown={search.onKeyDown}
placeholder={search.placeholder}
value={search.value}
/>
)}
{jsx}
</Grid.Column>
);
})}
</Grid>
{searchDataForMobile}
</React.Fragment>
)}
{children}
</div>
</header>
);
}
}
ActionBar.propTypes = propTypes;
ActionBar.defaultProps = defaultProps;
export default ActionBar;