From 65084636874a79ef59b1145e686cb132a5760d76 Mon Sep 17 00:00:00 2001
From: Pablo Mayrgundter
Date: Thu, 17 Aug 2023 16:29:54 -0400
Subject: [PATCH] Dialog+tabs (#782)
* add tab component
* add tabs component to the library
* comments
* comments
* add callback
* fix the boxes
* chnages
* add tabs dialog
* add tabs to a dialog
* reworked the dialog
* organize component props
* edit component
* clean up
* clean up
* adjust the theme
* spacing clean up
* add array of call backs
* clean up
* adjustments
* add tab fixture and add scrollable prop the dialog
* correct a typo
* address the comments
* address comments
* Fixes and simplifications.
* Restore DialogActions to TabbedDialog. Enhance asserts
* Fixup title
* Add TabbedDialog.test.jsx
* tsc fixups.
---------
Co-authored-by: OlegMoshkovich
---
package.json | 2 +-
src/Components/Buttons.jsx | 12 +++--
src/Components/Dialog.jsx | 2 +-
src/Components/TabbedDialog.fixture.jsx | 31 ++++++++++++
src/Components/TabbedDialog.jsx | 66 +++++++++++++++++++++++++
src/Components/TabbedDialog.test.jsx | 43 ++++++++++++++++
src/Components/Tabs.fixture.jsx | 14 ++++++
src/Components/Tabs.jsx | 25 ++++++++++
src/Styles.jsx | 4 +-
src/theme/Components.js | 53 +++++++++++++++++++-
src/theme/Theme.jsx | 2 +-
src/theme/Typography.js | 1 +
src/utils/assert.js | 25 ++++++++++
src/utils/assert.test.js | 40 ++++++++++++++-
14 files changed, 309 insertions(+), 11 deletions(-)
create mode 100644 src/Components/TabbedDialog.fixture.jsx
create mode 100644 src/Components/TabbedDialog.jsx
create mode 100644 src/Components/TabbedDialog.test.jsx
create mode 100644 src/Components/Tabs.fixture.jsx
create mode 100644 src/Components/Tabs.jsx
diff --git a/package.json b/package.json
index 183002abd..03c8bba2e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bldrs",
- "version": "1.0.0-r689",
+ "version": "1.0.0-r705",
"main": "src/index.jsx",
"license": "MIT",
"homepage": "https://github.com/bldrs-ai/Share",
diff --git a/src/Components/Buttons.jsx b/src/Components/Buttons.jsx
index d4383347b..ef533c931 100644
--- a/src/Components/Buttons.jsx
+++ b/src/Components/Buttons.jsx
@@ -116,9 +116,9 @@ export function CloseButton({onClick}) {
*
* @property {string} title Text to show in button
* @property {Function} onClick callback
- * @property {object} icon Start icon to left of text
- * @property {boolean} border Default: false
- * @property {boolean} background Default: true
+ * @property {object} [icon] Start icon to left of text
+ * @property {boolean} [border] Default: false
+ * @property {boolean} [background] Default: true
* @return {object} React component
*/
export function RectangularButton({
@@ -129,7 +129,11 @@ export function RectangularButton({
background = true,
}) {
assertDefined(title, onClick)
- return
+ return (
+ icon ?
+ :
+
+ )
}
diff --git a/src/Components/Dialog.jsx b/src/Components/Dialog.jsx
index 66d9e3df9..c9320f62e 100644
--- a/src/Components/Dialog.jsx
+++ b/src/Components/Dialog.jsx
@@ -1,8 +1,8 @@
import React from 'react'
+import MuiDialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import DialogTitle from '@mui/material/DialogTitle'
-import MuiDialog from '@mui/material/Dialog'
import {RectangularButton, CloseButton} from '../Components/Buttons'
import {assertDefined} from '../utils/assert'
diff --git a/src/Components/TabbedDialog.fixture.jsx b/src/Components/TabbedDialog.fixture.jsx
new file mode 100644
index 000000000..a83ebc3f0
--- /dev/null
+++ b/src/Components/TabbedDialog.fixture.jsx
@@ -0,0 +1,31 @@
+/* eslint-disable no-magic-numbers */
+import React from 'react'
+import FixtureContext from '../FixtureContext'
+import debug from '../utils/debug'
+import TabbedDialog from './TabbedDialog'
+
+
+const loremIpsum = (size) => `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. `.repeat(size)
+
+
+export default (
+
+ {loremIpsum(3)}
),
+ ({loremIpsum(2)}
),
+ ({loremIpsum(4)}
),
+ ]}
+ actionCbs={[
+ () => debug().log('clicked 1'),
+ () => debug().log('clicked 2'),
+ () => debug().log('clicked 3'),
+ ]}
+ isDialogDisplayed={true}
+ setIsDialogDisplayed={() => debug().log('setIsDialogDisplayed')}
+ />
+
+)
diff --git a/src/Components/TabbedDialog.jsx b/src/Components/TabbedDialog.jsx
new file mode 100644
index 000000000..086bf2f42
--- /dev/null
+++ b/src/Components/TabbedDialog.jsx
@@ -0,0 +1,66 @@
+import React, {useState} from 'react'
+import MuiDialog from '@mui/material/Dialog'
+import DialogActions from '@mui/material/DialogActions'
+import DialogContent from '@mui/material/DialogContent'
+import DialogTitle from '@mui/material/DialogTitle'
+import {assertDefined, assertArraysEqualLength} from '../utils/assert'
+import {CloseButton, RectangularButton} from './Buttons'
+import Tabs from './Tabs'
+
+
+/**
+ * A Dialog with tabs to page between associated contents.
+ *
+ * @property {Array} tabLabels Tab names
+ * @property {Array} headerLabels Short messages describing the current operation
+ * @property {Array} contentComponents Components coresponding to the tabs
+ * @property {Array} actionCbs Callbacks for each component's ok button
+ * @property {boolean} isDialogDisplayed React var
+ * @property {Function} setIsDialogDisplayed React setter
+ * @property {boolean} [isTabsScrollable] Activate if the number of tabs is larger than 5
+ * @property {React.ReactElement} [icon] Leading icon above header description
+ * @property {string} [actionButtonLabels] Labels for action ok buttons
+ * @return {React.Component}
+ */
+export default function TabbedDialog({
+ tabLabels,
+ headerLabels,
+ contentComponents,
+ actionCbs,
+ isDialogDisplayed,
+ setIsDialogDisplayed,
+ isTabsScrollable = false,
+ icon,
+ actionButtonLabels,
+}) {
+ assertDefined(tabLabels, headerLabels, contentComponents, actionCbs, isDialogDisplayed, setIsDialogDisplayed)
+ assertArraysEqualLength(tabLabels, headerLabels, contentComponents, actionCbs)
+ const close = () => setIsDialogDisplayed(false)
+ const [currentTab, setCurrentTab] = useState(0)
+ return (
+
+
+
+ {icon && <>{icon}
>}
+ {headerLabels[currentTab]}
+
+
+
+
+
+ {contentComponents[currentTab]}
+
+
+
+
+
+ )
+}
diff --git a/src/Components/TabbedDialog.test.jsx b/src/Components/TabbedDialog.test.jsx
new file mode 100644
index 000000000..ad5968395
--- /dev/null
+++ b/src/Components/TabbedDialog.test.jsx
@@ -0,0 +1,43 @@
+import React from 'react'
+import {render, screen, fireEvent} from '@testing-library/react'
+import TabbedDialog from './TabbedDialog'
+
+
+describe('TabbedDialog', () => {
+ it('', () => {
+ const cb1 = jest.fn()
+ const cb2 = jest.fn()
+ const cb3 = jest.fn()
+ render(
+ {'A content'}),
+ ({'B content'}
),
+ ({'C content'}
),
+ ]}
+ actionCbs={[cb1, cb2, cb3]}
+ actionButtonLabels={['A OK', 'B OK', 'C OK']}
+ isDialogDisplayed={true}
+ setIsDialogDisplayed={jest.fn()}
+ />)
+
+ fireEvent.click(screen.getByText('A OK'))
+ expect(cb1.mock.calls.length).toBe(1)
+ expect(cb2.mock.calls.length).toBe(0)
+ expect(cb3.mock.calls.length).toBe(0)
+
+ fireEvent.click(screen.getByText('Open'))
+ fireEvent.click(screen.getByText('B OK'))
+ expect(cb1.mock.calls.length).toBe(1)
+ expect(cb2.mock.calls.length).toBe(1)
+ expect(cb3.mock.calls.length).toBe(0)
+
+ fireEvent.click(screen.getByText('Save'))
+ fireEvent.click(screen.getByText('C OK'))
+ expect(cb1.mock.calls.length).toBe(1)
+ expect(cb2.mock.calls.length).toBe(1)
+ expect(cb3.mock.calls.length).toBe(1)
+ })
+})
diff --git a/src/Components/Tabs.fixture.jsx b/src/Components/Tabs.fixture.jsx
new file mode 100644
index 000000000..1f0e6d2c4
--- /dev/null
+++ b/src/Components/Tabs.fixture.jsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import Tabs from './Tabs'
+import FixtureContext from '../FixtureContext'
+import debug from '../utils/debug'
+
+
+export default (
+
+ debug().log('Clicked')}
+ />
+
+)
diff --git a/src/Components/Tabs.jsx b/src/Components/Tabs.jsx
new file mode 100644
index 000000000..282b32de7
--- /dev/null
+++ b/src/Components/Tabs.jsx
@@ -0,0 +1,25 @@
+import React, {useState} from 'react'
+import MuiTabs from '@mui/material/Tabs'
+import Tab from '@mui/material/Tab'
+import {assertDefined} from '../utils/assert'
+
+
+/**
+ * @property {Array} tabLabels Names of each tab
+ * @property {Function} actionCb callBack fired when the tabs is selected, returns currect tab number
+ * @property {boolean} [isScrollable] Enable scrolling for many (> 5) tabs
+ * @return {React.Component}
+ */
+export default function Tabs({tabLabels, actionCb, isScrollable = false}) {
+ assertDefined(tabLabels, actionCb)
+ const [value, setValue] = useState(0)
+ const handleChange = (event, newValue) => {
+ setValue(newValue)
+ actionCb(newValue)
+ }
+ return (
+
+ {tabLabels.map((tab) => )}
+
+ )
+}
diff --git a/src/Styles.jsx b/src/Styles.jsx
index 642e2773e..39102c1ff 100644
--- a/src/Styles.jsx
+++ b/src/Styles.jsx
@@ -26,7 +26,7 @@ export default function Styles({theme}) {
},
'.MuiDialog-paper': {
textAlign: 'center',
- padding: '1em 0.5em',
+ padding: '0.5em',
},
'.MuiDialog-paper > .MuiButtonBase-root': {
position: 'absolute',
@@ -65,7 +65,7 @@ export default function Styles({theme}) {
height: '12px',
},
'*::-webkit-scrollbar': {
- width: '10px',
+ width: '2px',
background: theme.palette.secondary.background,
},
'*::-webkit-scrollbar-thumb': {
diff --git a/src/theme/Components.js b/src/theme/Components.js
index c9d2d6834..29d015b71 100644
--- a/src/theme/Components.js
+++ b/src/theme/Components.js
@@ -2,7 +2,7 @@
* @param {object} Mui color palette.
* @return {object} Mui component overrides.
*/
-export function getComponentOverrides(palette) {
+export function getComponentOverrides(palette, typography) {
return {
MuiTreeItem: {
styleOverrides: {
@@ -55,6 +55,19 @@ export function getComponentOverrides(palette) {
},
},
},
+ MuiDialog: {
+ styleOverrides: {
+ root: {
+ },
+ },
+ },
+ MuiDialogContent: {
+ styleOverrides: {
+ root: {
+ padding: '0px 10px',
+ },
+ },
+ },
MuiPaper: {
styleOverrides: {
root: {
@@ -75,6 +88,44 @@ export function getComponentOverrides(palette) {
},
],
},
+ MuiTab: {
+ styleOverrides: {
+ root: {
+ 'textTransform': 'none',
+ 'minWidth': 0,
+ 'fontSize': '.9em',
+ 'fontWeight': typography.fontWeight,
+ 'marginRight': 0,
+ 'color': palette.primary.contrastText,
+ 'fontFamily': typography.fontFamily,
+ '&:hover': {
+ color: palette.secondary.main,
+ },
+ '&.Mui-selected': {
+ color: palette.secondary.main,
+ fontWeight: typography.fontWeight,
+ },
+ '&.Mui-focusVisible': {
+ backgroundColor: 'green',
+ },
+ '@media (max-width: 700px)': {
+ fontSize: '.8em',
+ },
+ },
+ },
+ },
+ MuiTabs: {
+ styleOverrides: {
+ root: {
+ 'paddingBottom': '12px',
+ 'width': '100%',
+ '& .MuiTabs-indicator': {
+ backgroundColor: palette.secondary.main,
+ },
+ },
+ },
+ },
+
MuiCardActions: {
styleOverrides: {
root: {
diff --git a/src/theme/Theme.jsx b/src/theme/Theme.jsx
index 215dfa79e..4d3f98649 100644
--- a/src/theme/Theme.jsx
+++ b/src/theme/Theme.jsx
@@ -53,7 +53,7 @@ function loadTheme(mode, setMode, themeChangeListeners) {
// https://mui.com/customization/dark-mode/
const activePalette = mode === Themes.Day ? day : night
const theme = {
- components: getComponentOverrides(activePalette),
+ components: getComponentOverrides(activePalette, getTypography()),
typography: getTypography(),
shape: {borderRadius: 8},
palette: activePalette,
diff --git a/src/theme/Typography.js b/src/theme/Typography.js
index 93e3723f1..38443c642 100644
--- a/src/theme/Typography.js
+++ b/src/theme/Typography.js
@@ -13,6 +13,7 @@ export function getTypography() {
fontFamily: fontFamily,
fontSize: fontSize,
letterSpacing: letterSpacing,
+ fontWeight: fontWeight,
lineHeight: lineHeight,
h1: {fontSize: '1.3em', fontWeight},
h2: {fontSize: '1.2em', fontWeight},
diff --git a/src/utils/assert.js b/src/utils/assert.js
index c91dfd661..d54ea28d2 100644
--- a/src/utils/assert.js
+++ b/src/utils/assert.js
@@ -25,6 +25,11 @@ export function assertDefined(...args) {
if (Object.prototype.hasOwnProperty.call(args, ndx)) {
const arg = args[ndx]
assert(arg !== null && arg !== undefined, `Arg ${ndx} is not defined`)
+ if (Array.isArray(arg)) {
+ for (let i = 0; i < arg.length; i++) {
+ assertDefined(arg[i], `Arg ${ndx} is an array with undefined index ${i}`)
+ }
+ }
}
}
if (args.length === 1) {
@@ -44,3 +49,23 @@ export function assertDefinedBoolean(arg) {
}
return false
}
+
+
+/**
+ * @param {any} arrays Variable length arguments to assert are defined.
+ * @return {Array>} The arrays
+ */
+export function assertArraysEqualLength(...arrays) {
+ if (arrays.length <= 1) {
+ throw new Error('Expected multiple arrays')
+ }
+ const arrLength = arrays[0].length
+ for (const ndx in arrays) {
+ if (Object.prototype.hasOwnProperty.call(arrays, ndx)) {
+ const array = arrays[ndx]
+ assertDefined(array)
+ assert(arrLength === array.length, `Array ${ndx} has unexpected length != ${array.length}`)
+ }
+ }
+ return arrays
+}
diff --git a/src/utils/assert.test.js b/src/utils/assert.test.js
index e3aa6650c..b5295baec 100644
--- a/src/utils/assert.test.js
+++ b/src/utils/assert.test.js
@@ -1,6 +1,8 @@
+/* eslint-disable no-magic-numbers */
import {
assert,
assertDefined,
+ assertArraysEqualLength,
} from './assert'
@@ -15,8 +17,28 @@ test('assert', () => {
})
-test('assert', () => {
+test('assertDefined', () => {
assertDefined(1)
+ assertDefined(1, 2)
+ assertDefined(1, 2, 3)
+ assertDefined([])
+ assertDefined([], [])
+ assertDefined([], [], [])
+ expectFailure(() => {
+ assertDefined([undefined])
+ })
+ expectFailure(() => {
+ assertDefined([1], [undefined])
+ })
+ expectFailure(() => {
+ assertDefined([undefined], [1])
+ })
+ expectFailure(() => {
+ assertDefined([undefined], [1])
+ })
+ expectFailure(() => {
+ assertDefined([undefined], [undefined])
+ })
expectFailure(() => {
assertDefined(undefined)
})
@@ -37,6 +59,22 @@ test('assert', () => {
})
+test('assertArraysEqualLength', () => {
+ expectFailure(() => {
+ assertArraysEqualLength()
+ })
+ expectFailure(() => {
+ assertArraysEqualLength([])
+ })
+ assertArraysEqualLength([], [])
+ assertArraysEqualLength([], [], [])
+ assertArraysEqualLength([], [], [], [])
+
+ assertArraysEqualLength([1, 1, 1], [2, 2, 2])
+ assertArraysEqualLength([1, 1, 1], [2, 2, 2], [3, 3, 3])
+})
+
+
/**
* Catches expected failures or throws if no failure.
*