135 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
};
|