u
This commit is contained in:
@@ -134,7 +134,7 @@ export const PanelTile = injectMuiTheme(PanelTileBase);
|
|||||||
* @behaviour Uses a CSS grid with `gap: 2` (theme spacing).
|
* @behaviour Uses a CSS grid with `gap: 2` (theme spacing).
|
||||||
* @behaviour Children position themselves via the `wide` prop of each tile.
|
* @behaviour Children position themselves via the `wide` prop of each tile.
|
||||||
*/
|
*/
|
||||||
export function PanelTileGrid({ children, sx }) {
|
export function PanelTileGrid({ children, sx, icon, text, label, badge, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -147,6 +147,7 @@ export function PanelTileGrid({ children, sx }) {
|
|||||||
gap: 2,
|
gap: 2,
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
204
src/panels/TabbedPanelTileGrid.jsx
Normal file
204
src/panels/TabbedPanelTileGrid.jsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import Tabs from '@mui/material/Tabs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tabbed wrapper for multiple `PanelTileGrid` layouts.
|
||||||
|
*
|
||||||
|
* Each child `PanelTileGrid` defines its tab metadata via props:
|
||||||
|
* - `text` (or `label`): Tab label string
|
||||||
|
* - `icon`: Optional icon ReactNode
|
||||||
|
* - `value`: Optional unique tab value (defaults to child index)
|
||||||
|
* - `disabled`: Optional boolean
|
||||||
|
* - `badge`: Optional string/number shown as a pill chip next to the label
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```jsx
|
||||||
|
* <TabbedPanelTileGrid>
|
||||||
|
* <PanelTileGrid icon={<StyleIcon />} text="Auswahl-Merkmale">
|
||||||
|
* <PanelTile wide={2}>...</PanelTile>
|
||||||
|
* <PanelTile wide={2}>...</PanelTile>
|
||||||
|
* </PanelTileGrid>
|
||||||
|
* <PanelTileGrid icon={<TuneIcon />} text="Text-Merkmale">
|
||||||
|
* <PanelTile wide={2}>...</PanelTile>
|
||||||
|
* </PanelTileGrid>
|
||||||
|
* </TabbedPanelTileGrid>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @component TabbedPanelTileGrid
|
||||||
|
* @import dashgui/panels
|
||||||
|
*
|
||||||
|
* @prop {ReactNode} children - One or more PanelTileGrid elements.
|
||||||
|
* @prop {any} [value] - Controlled active tab value/index.
|
||||||
|
* @prop {any} [defaultValue] - Initial active tab value/index for uncontrolled mode.
|
||||||
|
* @prop {any} [defaultTab] - Alias for defaultValue.
|
||||||
|
* @prop {(nextValue: any, event: React.SyntheticEvent) => void} [onChange] - Called on tab switch.
|
||||||
|
* @prop {boolean} [keepMounted=true] - When true, inactive grids remain mounted in DOM with `display: none` to preserve scroll, refs, and sortable states.
|
||||||
|
* @prop {object} [sx] - Extra styles merged into root container Box.
|
||||||
|
* @prop {object} [tabsWrapperSx] - Extra styles for the container around Tabs.
|
||||||
|
* @prop {object} [tabsSx] - Extra styles merged into MUI Tabs.
|
||||||
|
* @prop {object} [tabSx] - Extra styles merged into MUI Tab.
|
||||||
|
*/
|
||||||
|
export class TabbedPanelTileGrid extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
internalTab: props.defaultValue ?? props.defaultTab ?? 0,
|
||||||
|
};
|
||||||
|
this.handleTabChange = this.handleTabChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTabs() {
|
||||||
|
const children = React.Children.toArray(this.props.children).filter(Boolean);
|
||||||
|
return children.map((child, index) => {
|
||||||
|
const value = child.props.value !== undefined ? child.props.value : index;
|
||||||
|
const text = child.props.text ?? child.props.label ?? child.props.title ?? `Tab ${index + 1}`;
|
||||||
|
const icon = child.props.icon ?? null;
|
||||||
|
const disabled = Boolean(child.props.disabled);
|
||||||
|
const badge = child.props.badge;
|
||||||
|
const color = child.props.color;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
disabled,
|
||||||
|
badge,
|
||||||
|
color,
|
||||||
|
child,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveValue(tabs) {
|
||||||
|
if (this.props.value !== undefined) {
|
||||||
|
return this.props.value;
|
||||||
|
}
|
||||||
|
const { internalTab } = this.state;
|
||||||
|
if (tabs.some((t) => t.value === internalTab)) {
|
||||||
|
return internalTab;
|
||||||
|
}
|
||||||
|
const firstEnabled = tabs.find((t) => !t.disabled) ?? tabs[0];
|
||||||
|
return firstEnabled ? firstEnabled.value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabChange(event, nextValue) {
|
||||||
|
if (this.props.value === undefined) {
|
||||||
|
this.setState({ internalTab: nextValue }, () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}
|
||||||
|
this.props.onChange?.(nextValue, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
keepMounted = true,
|
||||||
|
sx,
|
||||||
|
tabsWrapperSx,
|
||||||
|
tabsSx,
|
||||||
|
tabSx,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const tabs = this.getTabs();
|
||||||
|
if (tabs.length === 0) return null;
|
||||||
|
|
||||||
|
const activeValue = this.getActiveValue(tabs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={sx}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
...tabsWrapperSx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
value={activeValue}
|
||||||
|
onChange={this.handleTabChange}
|
||||||
|
textColor="primary"
|
||||||
|
indicatorColor="primary"
|
||||||
|
sx={{
|
||||||
|
minHeight: 44,
|
||||||
|
'& .MuiTabs-flexContainer': {
|
||||||
|
gap: 1,
|
||||||
|
},
|
||||||
|
'& .MuiTab-root': {
|
||||||
|
minHeight: 44,
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '0.9375rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
gap: 1,
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
borderRadius: '8px 8px 0 0',
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
color: 'text.primary',
|
||||||
|
},
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'text.primary',
|
||||||
|
fontWeight: 700,
|
||||||
|
},
|
||||||
|
'& .MuiSvgIcon-root': {
|
||||||
|
fontSize: 20,
|
||||||
|
},
|
||||||
|
...tabSx,
|
||||||
|
},
|
||||||
|
...tabsSx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={String(tab.value)}
|
||||||
|
value={tab.value}
|
||||||
|
disabled={tab.disabled}
|
||||||
|
icon={tab.icon}
|
||||||
|
iconPosition="start"
|
||||||
|
label={
|
||||||
|
tab.badge != null ? (
|
||||||
|
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<span>{tab.text}</span>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={tab.badge}
|
||||||
|
sx={{ height: 20, fontSize: '0.75rem', fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
tab.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isSelected = tab.value === activeValue;
|
||||||
|
if (!isSelected && !keepMounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return React.cloneElement(tab.child, {
|
||||||
|
key: String(tab.value),
|
||||||
|
sx: {
|
||||||
|
...tab.child.props.sx,
|
||||||
|
...(isSelected ? {} : { display: 'none !important' }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabbedPanelTileGrid;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export { PanelTile, PanelTileGrid, GRID_COLUMNS } from './PanelTile';
|
export { PanelTile, PanelTileGrid, GRID_COLUMNS } from './PanelTile';
|
||||||
|
export { TabbedPanelTileGrid } from './TabbedPanelTileGrid';
|
||||||
export { EditablePanelTile } from './EditablePanelTile';
|
export { EditablePanelTile } from './EditablePanelTile';
|
||||||
export { injectMuiTheme } from './injectMuiTheme';
|
export { injectMuiTheme } from './injectMuiTheme';
|
||||||
export { PENDING_EDIT_BORDER } from './panelTileLayout';
|
export { PENDING_EDIT_BORDER } from './panelTileLayout';
|
||||||
|
|||||||
Reference in New Issue
Block a user