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 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 (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -147,6 +147,7 @@ export function PanelTileGrid({ children, sx }) {
|
||||
gap: 2,
|
||||
...sx,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</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 { TabbedPanelTileGrid } from './TabbedPanelTileGrid';
|
||||
export { EditablePanelTile } from './EditablePanelTile';
|
||||
export { injectMuiTheme } from './injectMuiTheme';
|
||||
export { PENDING_EDIT_BORDER } from './panelTileLayout';
|
||||
|
||||
Reference in New Issue
Block a user