Files
Work/Frontend/src/components/shell/WorkspaceMobileActionSheet/WorkspaceMobileActionSheet.tsx
2026-06-18 16:58:31 +01:00

135 lines
4.6 KiB
TypeScript

import { For, Show, createMemo, type JSX } from "solid-js";
import { Portal } from "solid-js/web";
import { X } from "../../../lib/icons";
import {
getWorkspaceContextMenuEyebrow,
getWorkspaceContextMenuSections,
type WorkspaceContextMenuAction,
type WorkspaceContextMenuSection,
type WorkspaceContextMenuTarget,
} from "../data/shell.data";
import styles from "./WorkspaceMobileActionSheet.module.scss";
type WorkspaceMobileActionSheetProps = {
target: WorkspaceContextMenuTarget | null;
onClose: VoidFunction;
onSelect: (action: WorkspaceContextMenuAction, target: WorkspaceContextMenuTarget) => void;
};
type FlattenedActionSection = {
id: string;
label?: string;
items: readonly WorkspaceContextMenuAction[];
};
const flattenMobileSections = (
sections: readonly WorkspaceContextMenuSection[],
): readonly FlattenedActionSection[] => {
// Mobile uses a flat action-sheet model, so desktop flyout groups become
// standalone labeled sections instead of nested menus.
return sections.flatMap((section) => {
const directActions = section.items.filter((action) => !action.children?.length);
const nestedSections = section.items
.filter((action) => action.children?.length)
.map((action) => ({
id: `${section.id}-${action.id}`,
label: action.label,
items: action.children ?? [],
}));
const flattenedSections: FlattenedActionSection[] = [];
if (directActions.length > 0) {
flattenedSections.push({
id: section.id,
label: section.label,
items: directActions,
});
}
return [...flattenedSections, ...nestedSections];
});
};
export const WorkspaceMobileActionSheet = (props: WorkspaceMobileActionSheetProps): JSX.Element => {
const sheetState = createMemo(() => {
if (!props.target) {
return null;
}
return {
target: props.target,
sections: flattenMobileSections(getWorkspaceContextMenuSections(props.target)),
};
});
const handleActionSelect = (action: WorkspaceContextMenuAction, target: WorkspaceContextMenuTarget): void => {
props.onSelect(action, target);
props.onClose();
};
return (
<Show when={sheetState()}>
{(sheetState): JSX.Element => {
const target = sheetState().target;
const sections = sheetState().sections;
return (
<Portal>
<div class={styles.layer} data-ui="workspace-mobile-action-sheet" data-target-kind={target.kind} data-item-type={target.kind === "item" ? target.itemType : undefined}>
<button class={styles.backdrop} type="button" aria-label="Close action sheet" data-slot="mobile-action-sheet-backdrop" onClick={props.onClose} />
<section class={styles.sheet} aria-label={`${target.label} actions`} data-slot="mobile-action-sheet-panel">
<div class={styles.handle} data-slot="mobile-action-sheet-handle" aria-hidden="true" />
<header class={styles.header} data-slot="mobile-action-sheet-header">
<div class={styles.headerCopy} data-slot="mobile-action-sheet-header-copy">
<span class={styles.eyebrow}>{getWorkspaceContextMenuEyebrow(target)}</span>
<strong class={styles.title}>{target.label}</strong>
</div>
<button class={styles.closeButton} type="button" aria-label="Close action sheet" data-slot="mobile-action-sheet-close" onClick={props.onClose}>
<X size={18} strokeWidth={2} />
</button>
</header>
<div class={styles.sectionList} data-slot="mobile-action-sheet-sections">
<For each={sections}>
{(section): JSX.Element => (
<section class={styles.section} data-slot="mobile-action-sheet-section" data-section-id={section.id}>
<Show when={section.label}>
<span class={styles.sectionLabel}>{section.label}</span>
</Show>
<div class={styles.actionList} data-slot="mobile-action-sheet-action-list">
<For each={section.items}>
{(action): JSX.Element => (
<button
type="button"
classList={{
[styles.action]: true,
[styles.actionDanger]: action.tone === "danger",
}}
data-slot="mobile-action-sheet-action"
data-action-id={action.id}
data-tone={action.tone ?? "default"}
onClick={() => handleActionSelect(action, target)}
>
<span class={styles.actionLabel}>{action.label}</span>
</button>
)}
</For>
</div>
</section>
)}
</For>
</div>
</section>
</div>
</Portal>
);
}}
</Show>
);
};