diff --git a/webui/package.json b/webui/package.json index 6a60fdc..28782ae 100644 --- a/webui/package.json +++ b/webui/package.json @@ -24,6 +24,7 @@ "test:file-drop-zone": "rm -rf .file-drop-test-build && mkdir -p .file-drop-test-build && printf '{\"type\":\"commonjs\"}\\n' > .file-drop-test-build/package.json && tsc -p tsconfig.file-drop-tests.json && node .file-drop-test-build/tests/file-drop-resolver.test.js && node scripts/test-file-drop-zone-structure.mjs", "test:data-grid-actions": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/data-grid-actions.test.js", "test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs", + "test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js", "test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js", "test:module-permutations": "node scripts/test-module-permutations.mjs", "test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js", diff --git a/webui/src/components/IconButton.tsx b/webui/src/components/IconButton.tsx new file mode 100644 index 0000000..2a345b6 --- /dev/null +++ b/webui/src/components/IconButton.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; +import { usePlatformLanguage } from "../i18n/LanguageContext"; +import Button, { type ButtonProps } from "./Button"; + +export type IconButtonProps = Omit & { + label: string; + icon: ReactNode; +}; + +export default function IconButton({ + label, + icon, + className = "", + type = "button", + ...buttonProps +}: IconButtonProps) { + const { translateText } = usePlatformLanguage(); + const translatedLabel = translateText(label); + + return ( + + ); +} diff --git a/webui/src/components/admin/AdminIconButton.tsx b/webui/src/components/admin/AdminIconButton.tsx index 17c1839..22b8234 100644 --- a/webui/src/components/admin/AdminIconButton.tsx +++ b/webui/src/components/admin/AdminIconButton.tsx @@ -1,29 +1,8 @@ -import type { ReactNode } from "react"; -import Button from "../Button"; -import { usePlatformLanguage } from "../../i18n/LanguageContext"; +import IconButton, { type IconButtonProps } from "../IconButton"; -type Props = { - label: string; - icon: ReactNode; - onClick?: () => void; - disabled?: boolean; - variant?: "primary" | "secondary" | "ghost" | "danger"; -}; +export type AdminIconButtonProps = IconButtonProps; -export default function AdminIconButton({ label, icon, onClick, disabled = false, variant = "secondary" }: Props) { - const { translateText } = usePlatformLanguage(); - const translatedLabel = translateText(label); - return ( - - ); +/** @deprecated Prefer the neutral IconButton export for new consumers. */ +export default function AdminIconButton({ className = "", ...props }: AdminIconButtonProps) { + return ; } diff --git a/webui/src/index.ts b/webui/src/index.ts index bd53a49..775e6bc 100644 --- a/webui/src/index.ts +++ b/webui/src/index.ts @@ -44,6 +44,7 @@ export { default as ActionBlockerHint } from "./components/ActionBlockerHint"; export type { ActionBlockerReason } from "./components/ActionBlockerHint"; export { default as AdvancedOptionsPanel } from "./components/AdvancedOptionsPanel"; export { default as AdminIconButton } from "./components/admin/AdminIconButton"; +export type { AdminIconButtonProps } from "./components/admin/AdminIconButton"; export { default as AdminPageLayout } from "./components/admin/AdminPageLayout"; export { default as AdminSelectionList } from "./components/admin/AdminSelectionList"; export { adminErrorMessage, formatAdminDateTime, joinLabels } from "./components/admin/adminUtils"; @@ -72,6 +73,8 @@ export { default as GuidedReviewList } from "./components/GuidedReviewList"; export type { GuidedReviewItem } from "./components/GuidedReviewList"; export { default as HoverTooltip } from "./components/HoverTooltip"; export type { HoverTooltipProps, HoverTooltipTone } from "./components/HoverTooltip"; +export { default as IconButton } from "./components/IconButton"; +export type { IconButtonProps } from "./components/IconButton"; export { default as LoadingFrame } from "./components/LoadingFrame"; export { default as LoadingIndicator } from "./components/LoadingIndicator"; export { default as ExplorerTree } from "./components/ExplorerTree"; diff --git a/webui/src/styles/components.css b/webui/src/styles/components.css index 0fac526..a782b2d 100644 --- a/webui/src/styles/components.css +++ b/webui/src/styles/components.css @@ -953,7 +953,7 @@ align-self: start; } -.admin-icon-button.btn { +.icon-button.btn { display: inline-grid; place-items: center; width: 36px; @@ -962,7 +962,7 @@ padding: 0; } -.admin-icon-button.btn svg { +.icon-button.btn svg { width: 17px; height: 17px; } diff --git a/webui/tests/icon-button.test.tsx b/webui/tests/icon-button.test.tsx new file mode 100644 index 0000000..7ecc0a3 --- /dev/null +++ b/webui/tests/icon-button.test.tsx @@ -0,0 +1,40 @@ +function assert(condition: unknown, message = "assertion failed"): void { + if (!condition) throw new Error(message); +} + +import { renderToStaticMarkup } from "react-dom/server"; +import AdminIconButton from "../src/components/admin/AdminIconButton"; +import IconButton from "../src/components/IconButton"; +import { PlatformLanguageProvider } from "../src/i18n/LanguageContext"; + +function noop() {} + +const iconButtonMarkup = renderToStaticMarkup( + + } + variant="danger" + className="context-action" + onClick={noop} + /> + +); +assert(iconButtonMarkup.includes('type="button"'), "icon buttons default to a non-submitting button"); +assert(iconButtonMarkup.includes('class="btn btn-danger icon-button context-action"'), "central variants and caller classes are preserved"); +assert(iconButtonMarkup.includes('aria-label="Remove"'), "the accessible label is translated"); +assert(iconButtonMarkup.includes('title="Remove"'), "the native title is translated"); +assert(iconButtonMarkup.includes('class="icon-button-icon" aria-hidden="true"'), "the visible icon is decorative"); + +const reasonedMarkup = renderToStaticMarkup( + X} disabledReason="Permission denied" /> +); +assert(reasonedMarkup.includes("disabled=\"\""), "a disabled reason disables the icon button"); +assert(reasonedMarkup.includes("disabled-action-tooltip"), "a disabled reason remains discoverable"); +assert(!reasonedMarkup.includes("disabledReason"), "central-only props are not passed to the DOM"); + +const legacyMarkup = renderToStaticMarkup( + +} disabled /> +); +assert(legacyMarkup.includes("icon-button admin-icon-button"), "the legacy wrapper delegates to the neutral component and preserves its class hook"); +assert(legacyMarkup.includes("disabled=\"\""), "the legacy disabled contract is preserved"); diff --git a/webui/tsconfig.component-tests.json b/webui/tsconfig.component-tests.json index 9c7edf2..78a51d5 100644 --- a/webui/tsconfig.component-tests.json +++ b/webui/tsconfig.component-tests.json @@ -20,6 +20,7 @@ "include": [ "tests/data-grid-actions.test.tsx", "tests/dialog-focus.test.tsx", + "tests/icon-button.test.tsx", "tests/mail-components.test.tsx", "tests/resource-access-explanation.test.tsx", "src/components/CredentialPanel.tsx", @@ -34,6 +35,8 @@ "src/components/dialogInteractions.ts", "src/components/dialogStack.ts", "src/components/FormField.tsx", + "src/components/IconButton.tsx", + "src/components/admin/AdminIconButton.tsx", "src/components/ToggleSwitch.tsx", "src/components/table/TableActionGroup.tsx" ]