From 1277e355eb6cb5e7e50344cf9c3cb891d7aefe26 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 11 Oct 2026 23:42:19 +0200 Subject: [PATCH] u --- src/panels/PanelTile.jsx | 3 +- src/panels/TabbedPanelTileGrid.jsx | 204 +++++++++++++++++++++++++++++ src/panels/index.js | 1 + 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/panels/TabbedPanelTileGrid.jsx diff --git a/src/panels/PanelTile.jsx b/src/panels/PanelTile.jsx index 8a031ef..deffbcd 100644 --- a/src/panels/PanelTile.jsx +++ b/src/panels/PanelTile.jsx @@ -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 ( {children} diff --git a/src/panels/TabbedPanelTileGrid.jsx b/src/panels/TabbedPanelTileGrid.jsx new file mode 100644 index 0000000..b72d218 --- /dev/null +++ b/src/panels/TabbedPanelTileGrid.jsx @@ -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 + * + * } text="Auswahl-Merkmale"> + * ... + * ... + * + * } text="Text-Merkmale"> + * ... + * + * + * ``` + * + * @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 ( + + + + {tabs.map((tab) => ( + + {tab.text} + + + ) : ( + tab.text + ) + } + /> + ))} + + + + {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' }), + }, + }); + })} + + ); + } +} + +export default TabbedPanelTileGrid; diff --git a/src/panels/index.js b/src/panels/index.js index f24644f..7fc1bcb 100644 --- a/src/panels/index.js +++ b/src/panels/index.js @@ -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';