This commit is contained in:
seb
2026-10-11 23:42:19 +02:00
parent 37461e539b
commit 1277e355eb
3 changed files with 207 additions and 1 deletions

View File

@@ -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>

View 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;

View File

@@ -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';