Skip to content

Commit

Permalink
Dialog+tabs (#782)
Browse files Browse the repository at this point in the history
* 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 <oleg.mosh@gmail.com>
  • Loading branch information
pablo-mayrgundter and OlegMoshkovich committed Aug 17, 2023
1 parent 363b80d commit 6508463
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
12 changes: 8 additions & 4 deletions src/Components/Buttons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -129,7 +129,11 @@ export function RectangularButton({
background = true,
}) {
assertDefined(title, onClick)
return <Button onClick={onClick} startIcon={icon} variant='rectangular'>{title}</Button>
return (
icon ?
<Button onClick={onClick} startIcon={icon} variant='rectangular'>{title}</Button> :
<Button onClick={onClick} variant='rectangular'>{title}</Button>
)
}


Expand Down
2 changes: 1 addition & 1 deletion src/Components/Dialog.jsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
31 changes: 31 additions & 0 deletions src/Components/TabbedDialog.fixture.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<FixtureContext>
<TabbedDialog
tabLabels={['Explore', 'Open', 'Save']}
headerLabels={['Explore Sample Projects', 'Open Project', 'Save Project']}
contentComponents={[
(<p key='1'>{loremIpsum(3)}</p>),
(<p key='2'>{loremIpsum(2)}</p>),
(<p key='3'>{loremIpsum(4)}</p>),
]}
actionCbs={[
() => debug().log('clicked 1'),
() => debug().log('clicked 2'),
() => debug().log('clicked 3'),
]}
isDialogDisplayed={true}
setIsDialogDisplayed={() => debug().log('setIsDialogDisplayed')}
/>
</FixtureContext>
)
66 changes: 66 additions & 0 deletions src/Components/TabbedDialog.jsx
Original file line number Diff line number Diff line change
@@ -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<string>} tabLabels Tab names
* @property {Array<string>} headerLabels Short messages describing the current operation
* @property {Array<React.Element>} contentComponents Components coresponding to the tabs
* @property {Array<React.Element>} 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 (
<MuiDialog
maxWidth={'xs'}
open={isDialogDisplayed}
onClose={close}
PaperProps={{variant: 'control'}}
>
<CloseButton onClick={close}/>
<DialogTitle>
{icon && <>{icon}<br/></>}
{headerLabels[currentTab]}
</DialogTitle>
<DialogContent>
<Tabs tabLabels={tabLabels} actionCb={setCurrentTab} isScrollable={isTabsScrollable}/>
</DialogContent>
<DialogContent>
{contentComponents[currentTab]}
</DialogContent>
<DialogActions>
<RectangularButton
title={actionButtonLabels ? actionButtonLabels[currentTab] : 'Ok'}
onClick={actionCbs[currentTab]}
/>
</DialogActions>
</MuiDialog>
)
}
43 changes: 43 additions & 0 deletions src/Components/TabbedDialog.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<TabbedDialog
tabLabels={['Explore', 'Open', 'Save']}
headerLabels={['Explore Sample Projects', 'Open Project', 'Save Project']}
contentComponents={[
(<p key='1'>{'A content'}</p>),
(<p key='2'>{'B content'}</p>),
(<p key='3'>{'C content'}</p>),
]}
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)
})
})
14 changes: 14 additions & 0 deletions src/Components/Tabs.fixture.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'
import Tabs from './Tabs'
import FixtureContext from '../FixtureContext'
import debug from '../utils/debug'


export default (
<FixtureContext>
<Tabs
tabLabels={['Explore', 'Open', 'Save']}
actionCb={() => debug().log('Clicked')}
/>
</FixtureContext>
)
25 changes: 25 additions & 0 deletions src/Components/Tabs.jsx
Original file line number Diff line number Diff line change
@@ -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<string>} 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 (
<MuiTabs value={value} onChange={handleChange} centered variant={isScrollable ? 'scrollable' : 'fullWidth'}>
{tabLabels.map((tab) => <Tab key={tab} label={tab}/>)}
</MuiTabs>
)
}
4 changes: 2 additions & 2 deletions src/Styles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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': {
Expand Down
53 changes: 52 additions & 1 deletion src/theme/Components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -55,6 +55,19 @@ export function getComponentOverrides(palette) {
},
},
},
MuiDialog: {
styleOverrides: {
root: {
},
},
},
MuiDialogContent: {
styleOverrides: {
root: {
padding: '0px 10px',
},
},
},
MuiPaper: {
styleOverrides: {
root: {
Expand All @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/theme/Theme.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/theme/Typography.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
25 changes: 25 additions & 0 deletions src/utils/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -44,3 +49,23 @@ export function assertDefinedBoolean(arg) {
}
return false
}


/**
* @param {any} arrays Variable length arguments to assert are defined.
* @return {Array<Array<any>>} 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
}
Loading

0 comments on commit 6508463

Please sign in to comment.