Files
superset2/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx
Claude 6218dcbbb3 feat(dashboard): ComponentHeaderControls — Phase 2
Shared vertical-dots menu component for dashboard grid components.
Generic `items: ComponentMenuItem[]` API — each component (Chart,
Markdown, Row, Column, Tabs) plugs in its own list; the visual chrome
(dots icon trigger, dropdown surface, accessible label, divider
handling, danger/disabled styling) lives in this one component.

Built on `MenuDotsDropdown` from `@superset-ui/core/components` so the
trigger styling matches Chart's existing `SliceHeaderControls` — Phase
4's per-component PRs will converge `SliceHeaderControls` and the
other menu patterns (Markdown's `MarkdownModeDropdown`, Row/Col's
gear-icon + `WithPopoverMenu`) onto this same component.

Phase 2 lands the component + tests only. The actual per-component
menu conversions are user-visible UX changes (e.g. Markdown loses its
toggle-style Edit/Preview switcher and gains a dots menu) and ship in
Phase 4 alongside theme wiring per component, so each can be reviewed
in isolation rather than as a sweeping refactor.

4 passing tests: empty items renders nothing, trigger renders, onClick
fires from menu selection, disabled items don't fire onClick.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 10:58:17 -07:00

95 lines
3.2 KiB
TypeScript

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { ReactNode } from 'react';
import { MenuDotsDropdown, Menu } from '@superset-ui/core/components';
export interface ComponentMenuItem {
/** Stable key for React + telemetry. */
key: string;
/** Label rendered in the menu row. */
label: ReactNode;
/** Optional icon rendered to the left of the label. */
icon?: ReactNode;
/** Click handler. Provider closes the menu after firing. */
onClick?: () => void;
/** When true, dims and disables the row. */
disabled?: boolean;
/** Renders a horizontal rule above this item. */
divider?: boolean;
/** Marks the row as destructive (red tone). */
danger?: boolean;
}
interface ComponentHeaderControlsProps {
items: ComponentMenuItem[];
/** Data-test attribute hook for the trigger button. */
dataTest?: string;
/**
* Optional `aria-label` override for the trigger button. Default is the
* generic "Component options".
*/
ariaLabel?: string;
}
/**
* Shared vertical-dots menu for dashboard grid components. Each component
* (Chart, Markdown, Row, Column, Tabs) plugs in its own `items` and the
* visual chrome — the dots icon, dropdown surface, accessible labelling —
* lives here.
*
* Built on `MenuDotsDropdown` from `@superset-ui/core/components` so we get
* the same trigger styling as Chart's `SliceHeaderControls` does today;
* Phase 4 will converge `SliceHeaderControls` onto this same component.
*
* The component is intentionally render-only: it does not read Redux, does
* not gate on `editMode`, and does not know about theming. Callers decide
* when to render it. This keeps it reusable across edit vs view, hover
* menus, embedded contexts, etc.
*/
export default function ComponentHeaderControls({
items,
dataTest = 'component-header-controls',
ariaLabel,
}: ComponentHeaderControlsProps) {
if (items.length === 0) return null;
// antd Menu items: split divider markers into their own item entries.
const menuItems = items.flatMap(item => {
const row = {
key: item.key,
label: item.label,
icon: item.icon,
onClick: item.onClick,
disabled: item.disabled,
danger: item.danger,
};
return item.divider
? [{ type: 'divider' as const, key: `${item.key}-divider` }, row]
: [row];
});
return (
<MenuDotsDropdown
data-test={dataTest}
aria-label={ariaLabel}
overlay={<Menu items={menuItems} />}
/>
);
}