mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 20:58:04 +00:00
fix: keep confirm modal actions visible for long titles (#1578)
This commit is contained in:
@@ -22,11 +22,18 @@ export function DeleteStackDialog({ open, onOpenChange, stackName, onConfirm }:
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
kicker={`${(stackName ?? 'STACK').toUpperCase()} · REMOVE · IRREVERSIBLE`}
|
kicker="REMOVE · IRREVERSIBLE"
|
||||||
title={
|
title={
|
||||||
stackName ? (
|
stackName ? (
|
||||||
<>
|
<>
|
||||||
Delete <em className="font-display italic text-destructive">{stackName}</em>?
|
Delete{' '}
|
||||||
|
<em
|
||||||
|
className="font-display italic text-destructive break-all"
|
||||||
|
title={stackName}
|
||||||
|
>
|
||||||
|
{stackName}
|
||||||
|
</em>
|
||||||
|
?
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Delete stack?'
|
'Delete stack?'
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { DeleteStackDialog } from '../DeleteStackDialog';
|
||||||
|
|
||||||
|
const LONG_STACK_NAME = 'this-is-a-very-long-stack-name-that-should-not-push-actions-off-screen';
|
||||||
|
|
||||||
|
function renderLongNameDialog() {
|
||||||
|
render(
|
||||||
|
<DeleteStackDialog
|
||||||
|
open
|
||||||
|
onOpenChange={vi.fn()}
|
||||||
|
stackName={LONG_STACK_NAME}
|
||||||
|
onConfirm={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DeleteStackDialog', () => {
|
||||||
|
it('keeps Delete and Cancel visible when the stack name is very long', () => {
|
||||||
|
renderLongNameDialog();
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'Delete' })).toBeVisible();
|
||||||
|
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes the full stack name on hover via title', () => {
|
||||||
|
renderLongNameDialog();
|
||||||
|
|
||||||
|
expect(screen.getByTitle(LONG_STACK_NAME)).toHaveTextContent(LONG_STACK_NAME);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -88,12 +88,15 @@ function HeaderShell({
|
|||||||
}: HeaderShellProps) {
|
}: HeaderShellProps) {
|
||||||
const v = HEADER_VARIANT[variant];
|
const v = HEADER_VARIANT[variant];
|
||||||
return (
|
return (
|
||||||
<div className="relative border-b border-card-border/60 px-6 pt-6 pb-4 pr-12">
|
<div className="relative shrink-0 border-b border-card-border/60 px-6 pt-6 pb-4 pr-12">
|
||||||
<span aria-hidden className={cn('absolute inset-y-0 left-0 w-[3px]', v.rail)} />
|
<span aria-hidden className={cn('absolute inset-y-0 left-0 w-[3px]', v.rail)} />
|
||||||
<div className={cn(KICKER_CLASS, 'whitespace-nowrap', v.kicker)}>
|
<div className={cn(KICKER_CLASS, 'truncate', v.kicker)} title={kicker}>
|
||||||
{kicker}
|
{kicker}
|
||||||
</div>
|
</div>
|
||||||
<TitleComponent className="mt-1 font-heading text-[1.75rem] leading-tight text-stat-value">
|
<TitleComponent
|
||||||
|
className="mt-1 font-heading text-[1.75rem] leading-tight text-stat-value break-all line-clamp-3"
|
||||||
|
title={typeof title === 'string' ? title : undefined}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</TitleComponent>
|
</TitleComponent>
|
||||||
<DescriptionComponent className="sr-only">
|
<DescriptionComponent className="sr-only">
|
||||||
@@ -147,8 +150,22 @@ function ConfirmDestructiveHeader(props: ModalHeaderBaseProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalBody({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
return <div className={cn('px-6 py-5 space-y-4 max-h-[calc(85vh-12rem)] overflow-y-auto', className)} {...props} />;
|
/** Grow within a flex-column confirm shell; body scrolls instead of stretching the dialog. */
|
||||||
|
fill?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalBody({ className, fill, ...props }: ModalBodyProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'px-6 py-5 space-y-4 overflow-y-auto',
|
||||||
|
fill ? 'min-h-0 flex-1' : 'max-h-[calc(85vh-12rem)]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModalFooterProps {
|
interface ModalFooterProps {
|
||||||
@@ -160,7 +177,7 @@ interface ModalFooterProps {
|
|||||||
|
|
||||||
export function ModalFooter({ primary, secondary, hint, hintAccent }: ModalFooterProps) {
|
export function ModalFooter({ primary, secondary, hint, hintAccent }: ModalFooterProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4 border-t border-card-border/60 px-6 py-4">
|
<div className="flex shrink-0 items-center justify-between gap-4 border-t border-card-border/60 px-6 py-4">
|
||||||
<div className={cn(KICKER_CLASS, 'text-stat-subtitle')}>
|
<div className={cn(KICKER_CLASS, 'text-stat-subtitle')}>
|
||||||
{hint}
|
{hint}
|
||||||
{hintAccent !== undefined && (
|
{hintAccent !== undefined && (
|
||||||
@@ -221,9 +238,9 @@ export function ConfirmModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<AlertDialogContent className={cn('p-0 gap-0 overflow-hidden border-card-border/60', SIZE_CLASS[size])}>
|
<AlertDialogContent className={cn('flex max-h-[85dvh] flex-col p-0 gap-0 overflow-hidden border-card-border/60', SIZE_CLASS[size])}>
|
||||||
<Header kicker={kicker} title={title} description={description} />
|
<Header kicker={kicker} title={title} description={description} />
|
||||||
{children !== undefined && <ModalBody>{children}</ModalBody>}
|
{children !== undefined && <ModalBody fill>{children}</ModalBody>}
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
hint={hint}
|
hint={hint}
|
||||||
secondary={
|
secondary={
|
||||||
|
|||||||
Reference in New Issue
Block a user