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() {
-
-
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';