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. *