diff --git a/client/src/components/UIPatterns/UIPatterns.js b/client/src/components/UIPatterns/UIPatterns.js index 9d49aa1b2f..e7415b17a5 100644 --- a/client/src/components/UIPatterns/UIPatterns.js +++ b/client/src/components/UIPatterns/UIPatterns.js @@ -1,5 +1,6 @@ import React from 'react'; import UIPatternsMessage from './UIPatternsMessage'; +import UIPatternsDescriptionList from './UIPatternsDescriptionList'; import UIPatternsDropdownSelector from './UIPatternsDropdownSelector'; import './UIPatterns.module.scss'; @@ -11,18 +12,20 @@ function UIPatterns() {
-
-
Message
-
+
Message
-
-
DropdownSelector
-
+
DropdownSelector
+
+
+
DescriptionList
+ +
+
); } diff --git a/client/src/components/UIPatterns/UIPatterns.module.scss b/client/src/components/UIPatterns/UIPatterns.module.scss index 9fe88c0f04..d58894430e 100644 --- a/client/src/components/UIPatterns/UIPatterns.module.scss +++ b/client/src/components/UIPatterns/UIPatterns.module.scss @@ -1,31 +1,13 @@ -/* This is a CSS Modules, and minimal, version of the Dashboard stylesheet */ +/* This is a version of the Dashboard stylesheet with these changes: + - CSS Modules + - minimal + - no flexbox +*/ .container { - display: flex; - flex-direction: column; - // FAQ: Extra padding is added to bottom only for this section padding: 20px 40px 40px 25px; } -.items { - /* As a flex item */ - flex-grow: 1; - - /* As a flex container */ - display: flex; - flex-direction: column; -} -/* Make bottom-most item fill up remaining vertical space */ -.items > :last-child { - flex-grow: 1; -} - -.header, -.item-header { - display: flex; - justify-content: space-between; - align-items: baseline; -} .header { border-bottom: 1px solid #707070; @@ -35,7 +17,7 @@ font-weight: 400; } -/* FAQ: Indirect child element of `.dashboard-items` */ +/* FAQ: Ancestor of `.dashboard-items` */ /* RFC: Class name `.dashboard-item` (to coincide with `.dashboard-items`) */ .grid-item { margin-top: 20px; diff --git a/client/src/components/UIPatterns/UIPatternsDescriptionList/UIPatternsDescriptionList.js b/client/src/components/UIPatterns/UIPatternsDescriptionList/UIPatternsDescriptionList.js new file mode 100644 index 0000000000..70550fb1be --- /dev/null +++ b/client/src/components/UIPatterns/UIPatternsDescriptionList/UIPatternsDescriptionList.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { DescriptionList, Icon } from '_common'; + +import './UIPatternsDescriptionList.module.css'; + +const DATA = { + Username: 'bobward500', + Prefix: 'Mr.', + Name: 'Bob Ward', + Suffix: 'The 5th', + 'Favorite Numeric Value': 5, + Icon: +}; + +function UIPatternsDropdownSelector() { + return ( + <> +
+
+
Vertical Layout & Default Density
+
+ +
+
+
+
Vertical Layout & Compact Density
+
+ +
+
+
+
Vertical Layout & Compact Density - Narrow Container
+
+ +
+
+
+
+
+
Horizontal Layout & Default Density
+
+ +
+
Horizontal Layout & Compact Density
+
+ +
+
Horizontal Layout & Compact Density - Narrow Container
+
+ +
+
+
+ + ); +} + +export default UIPatternsDropdownSelector; diff --git a/client/src/components/UIPatterns/UIPatternsDescriptionList/UIPatternsDescriptionList.module.css b/client/src/components/UIPatterns/UIPatternsDescriptionList/UIPatternsDescriptionList.module.css new file mode 100644 index 0000000000..0bfe51c59c --- /dev/null +++ b/client/src/components/UIPatterns/UIPatternsDescriptionList/UIPatternsDescriptionList.module.css @@ -0,0 +1,10 @@ +/* Force narrow widths to trigger density features */ +.item-narrow { width: 500px; } +.item-x-narrow { width: 7ch; } + +/* Align vertical lists left-to-right, or top-to-bottom */ +.list-cols, +.list-rows { display: flex; } +.list-cols > * { flex-grow: 1; } +.list-cols { flex-direction: row; } +.list-rows { flex-direction: column; } diff --git a/client/src/components/UIPatterns/UIPatternsDescriptionList/index.js b/client/src/components/UIPatterns/UIPatternsDescriptionList/index.js new file mode 100644 index 0000000000..6522921857 --- /dev/null +++ b/client/src/components/UIPatterns/UIPatternsDescriptionList/index.js @@ -0,0 +1 @@ +export { default } from './UIPatternsDescriptionList'; diff --git a/client/src/components/_common/DescriptionList/DescriptionList.js b/client/src/components/_common/DescriptionList/DescriptionList.js new file mode 100644 index 0000000000..f5ddd057dd --- /dev/null +++ b/client/src/components/_common/DescriptionList/DescriptionList.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './DescriptionList.module.css'; + +export const DIRECTION_CLASS_MAP = { + vertical: 'is-vert', + horizontal: 'is-horz' +}; +export const DEFAULT_DIRECTION = 'vertical'; +export const DIRECTIONS = ['', ...Object.keys(DIRECTION_CLASS_MAP)]; + +export const DENSITY_CLASS_MAP = { + compact: 'is-narrow', + default: 'is-wide' +}; +export const DEFAULT_DENSITY = 'default'; +export const DENSITIES = ['', ...Object.keys(DENSITY_CLASS_MAP)]; + +const DescriptionList = ({ className, data, density, direction }) => { + const modifierClasses = []; + modifierClasses.push(DENSITY_CLASS_MAP[density || DEFAULT_DENSITY]); + modifierClasses.push(DIRECTION_CLASS_MAP[direction || DEFAULT_DIRECTION]); + const containerStyleNames = ['container', ...modifierClasses].join(' '); + + return ( +
+ {Object.keys(data).map(key => ( + +
+ {key} +
+
+ {data[key]} +
+
+ ))} +
+ ); +}; +DescriptionList.propTypes = { + /** Additional className for the root element */ + className: PropTypes.string, + /** Selector type */ + /* FAQ: We can support any values, even a component */ + // eslint-disable-next-line react/forbid-prop-types + data: PropTypes.object.isRequired, + /** Layout density */ + density: PropTypes.oneOf(DENSITIES), + /** Layout direction */ + direction: PropTypes.oneOf(DIRECTIONS) +}; +DescriptionList.defaultProps = { + className: '', + density: DEFAULT_DENSITY, + direction: DEFAULT_DIRECTION +}; + +export default DescriptionList; diff --git a/client/src/components/_common/DescriptionList/DescriptionList.module.css b/client/src/components/_common/DescriptionList/DescriptionList.module.css new file mode 100644 index 0000000000..628c79a5ee --- /dev/null +++ b/client/src/components/_common/DescriptionList/DescriptionList.module.css @@ -0,0 +1,55 @@ +.container { + /* … */ +} + +/* Children */ + +.key { + composes: u-ellipsis from '../../../styles/trumps/_u-ellipsis.scss'; + + font-weight: 500; + text-overflow: ':'; +} +.key::after { + content: ':'; + display: inline; + margin-right: 0.25em; +} +.is-horz .value { + white-space: nowrap; +} + +/* Types */ + +.is-vert { + /* … */ +} +.is-horz { + display: flex; + flex-direction: row; +} +.is-horz .key ~ .key::before { + content: '|'; + display: inline-block; +} +.is-horz.is-narrow .key ~ .key::before { + margin-left: 0.5em; + margin-right: 0.5em; +} +.is-horz.is-wide .key ~ .key::before { + margin-left: 1em; + margin-right: 1em; +} + +.is-vert.is-narrow .value { + /* TODO: Support mixins, but prevent SCSS pollution in `src/styles/` */ + /* HACK: CSS Modules will NOT compose with nested classes */ + /* composes: u-ellipsis from '../../../styles/trumps/_u-ellipsis.scss'; */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Overwrite Bootstrap `_reboot.scss` */ +.is-vert.is-narrow .value { margin-left: 0; } +.is-vert.is-wide .value { margin-left: 2.5rem; } /* 40px Firefox default */ diff --git a/client/src/components/_common/DescriptionList/DescriptionList.test.js b/client/src/components/_common/DescriptionList/DescriptionList.test.js new file mode 100644 index 0000000000..4c05cb713b --- /dev/null +++ b/client/src/components/_common/DescriptionList/DescriptionList.test.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import DescriptionList, * as DL from './DescriptionList'; + +const DATA = { + Username: 'bobward500', + Prefix: 'Mr.', + Name: 'Bob Ward', + Suffix: 'The 5th' +}; + +describe('Description List', () => { + it('has accurate tags', async () => { + const { getByTestId, findAllByTestId } = render(); + const list = getByTestId('list'); + const keys = await findAllByTestId('key'); + const values = await findAllByTestId('value'); + expect(list).toBeDefined(); + expect(list.tagName).toEqual('DL'); + keys.forEach( key => { + expect(key.tagName).toEqual('DT'); + }); + values.forEach( value => { + expect(value.tagName).toEqual('DD'); + }); + }); + it.each(DL.DIRECTIONS)('has accurate className when direction is "%s"', direction => { + const { getByTestId, findAllByTestId } = render(); + const list = getByTestId('list'); + const className = DL.DIRECTION_CLASS_MAP[direction || DL.DEFAULT_DIRECTION]; + expect(list).toBeDefined(); + expect(list.className).toMatch(className); + }); + it.each(DL.DENSITIES)('has accurate className when density is "%s"', density => { + const { getByTestId, findAllByTestId } = render(); + const list = getByTestId('list'); + const className = DL.DENSITY_CLASS_MAP[density || DL.DEFAULT_DENSITY]; + expect(list).toBeDefined(); + expect(list.className).toMatch(className); + }); +}); diff --git a/client/src/components/_common/DescriptionList/index.js b/client/src/components/_common/DescriptionList/index.js new file mode 100644 index 0000000000..24ce8f7a0e --- /dev/null +++ b/client/src/components/_common/DescriptionList/index.js @@ -0,0 +1,3 @@ +import DescriptionList from './DescriptionList'; + +export default DescriptionList; diff --git a/client/src/components/_common/index.js b/client/src/components/_common/index.js index 55e9a3cb0c..e97c19dd19 100644 --- a/client/src/components/_common/index.js +++ b/client/src/components/_common/index.js @@ -7,4 +7,5 @@ export { default as InfiniteScrollTable } from './InfiniteScrollTable'; export { default as AppIcon } from './AppIcon'; export { default as Icon } from './Icon'; export { default as Message } from './Message'; +export { default as DescriptionList } from './DescriptionList'; export { default as DropdownSelector } from './DropdownSelector';