Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions resources/js/components/commit-composer-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,21 @@ export function CommitComposerDialog({
onEscapeKeyDown={(event) => {
if (
document.querySelector(
'[data-mention-suggestions], [data-command-suggestions]',
'[data-mention-suggestions], [data-command-suggestions], [role="dialog"].yarl__root',
) ||
(event.target instanceof Element &&
(typeof Element !== 'undefined' &&
event.target instanceof Element &&
event.target.closest('[data-composer-preview]'))
) {
event.preventDefault();
}
}}
onInteractOutside={(event) => {
if (
typeof Element !== 'undefined' &&
event.target instanceof Element &&
event.target.closest(
'[data-mention-suggestions], [data-command-suggestions]',
'[data-mention-suggestions], [data-command-suggestions], [role="dialog"].yarl__root',
)
) {
event.preventDefault();
Expand Down
81 changes: 75 additions & 6 deletions resources/js/components/commit-image-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ImagePlus, X } from 'lucide-react';
import { useId, useRef, useState } from 'react';
import { ActionTooltip } from '@/components/action-tooltip';
import type { ShownImage } from '@/components/commit-images';
import { ImageViewer } from '@/components/image-viewer';
import { Button } from '@/components/ui/button';
import {
acceptedImageTypes,
Expand Down Expand Up @@ -164,6 +166,7 @@ export function ImagePickerPreviews({
}) {
const id = useId();
const [describedIndex, setDescribedIndex] = useState<number | null>(null);
const [viewedIndex, setViewedIndex] = useState<number | null>(null);
const urls = images.map(objectUrlFor);

if (images.length === 0) {
Expand All @@ -174,16 +177,56 @@ export function ImagePickerPreviews({
describedIndex !== null && describedIndex < images.length
? describedIndex
: null;
const activeViewedIndex =
viewedIndex !== null && viewedIndex < images.length
? viewedIndex
: null;
const altButtonId = (index: number) => `${id}-alt-${index}`;
const previewButtonId = (index: number) => `${id}-preview-${index}`;

const closeViewer = () => {
const indexToFocus = activeViewedIndex;
setViewedIndex(null);
if (indexToFocus !== null && typeof document !== 'undefined') {
requestAnimationFrame(() => {
document.getElementById(previewButtonId(indexToFocus))?.focus();
});
}
};

const finishDescribing = () => {
if (editing !== null) {
if (editing !== null && typeof document !== 'undefined') {
document.getElementById(altButtonId(editing))?.focus();
}

setDescribedIndex(null);
};

const downloadFilename = (_image: ShownImage, index: number): string => {
const file = images[index];
if (!file) {
return `image-${index + 1}.png`;
}

if (file.name && file.name.includes('.')) {
return file.name;
}

const subtype = file.type
? file.type.replace(/^image\//, '').toLowerCase()
: '';
const ext = subtype === 'jpeg' ? 'jpg' : subtype || 'png';
const baseName = file.name || `image-${index + 1}`;

return `${baseName}.${ext}`;
};

const shownImages: ShownImage[] = images.map((image, index) => ({
id: image.name ? image.name.replace(/\.[^/.]+$/, '') : `${index + 1}`,
url: urls[index],
description: descriptions[index]?.trim() || null,
}));

return (
<div className="mt-2">
<ul className="flex gap-2" aria-label="Attached images">
Expand All @@ -192,15 +235,32 @@ export function ImagePickerPreviews({

return (
<li key={url} className="relative">
<img
src={url}
alt={`Attached image ${index + 1}`}
className="size-20 rounded-md border bg-muted object-cover"
/>
<button
id={previewButtonId(index)}
type="button"
onClick={() => setViewedIndex(index)}
aria-label={`Preview attached image ${index + 1}`}
className="block cursor-pointer overflow-hidden rounded-md border bg-muted text-left transition hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
data-test="preview-image-button"
>
<img
src={url}
alt={`Attached image ${index + 1}`}
className="size-20 object-cover"
/>
</button>
<button
type="button"
onClick={() => {
setDescribedIndex(null);
if (viewedIndex === index) {
setViewedIndex(null);
} else if (
viewedIndex !== null &&
viewedIndex > index
) {
setViewedIndex(viewedIndex - 1);
}
onRemove(index);
}}
aria-label={`Remove image ${index + 1}`}
Expand Down Expand Up @@ -238,6 +298,15 @@ export function ImagePickerPreviews({
})}
</ul>

<ImageViewer
images={shownImages}
index={activeViewedIndex}
onView={setViewedIndex}
onClose={closeViewer}
downloadUrl={(image) => image.url}
downloadFilename={downloadFilename}
/>

{editing !== null && (
<div
id={`${id}-description`}
Expand Down
12 changes: 9 additions & 3 deletions resources/js/components/commit-quote-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,23 @@ function QuoteComposer({
<DialogContent
onEscapeKeyDown={(event) => {
if (
document.querySelector('[data-mention-suggestions]') ||
(event.target instanceof Element &&
document.querySelector(
'[data-mention-suggestions], [role="dialog"].yarl__root',
) ||
(typeof Element !== 'undefined' &&
event.target instanceof Element &&
event.target.closest('[data-composer-preview]'))
) {
event.preventDefault();
}
}}
onInteractOutside={(event) => {
if (
typeof Element !== 'undefined' &&
event.target instanceof Element &&
event.target.closest('[data-mention-suggestions]')
event.target.closest(
'[data-mention-suggestions], [role="dialog"].yarl__root',
)
) {
event.preventDefault();
}
Expand Down
34 changes: 24 additions & 10 deletions resources/js/components/image-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function ImageViewer({
onView,
onClose,
downloadUrl,
downloadFilename,
className,
styles,
toolbarButtons = [],
Expand All @@ -25,7 +26,8 @@ export function ImageViewer({
index: number | null;
onView: (index: number) => void;
onClose: () => void;
downloadUrl: (image: ShownImage) => string;
downloadUrl?: (image: ShownImage) => string;
downloadFilename?: (image: ShownImage, index: number) => string;
className?: string;
styles?: LightboxProps['styles'];
toolbarButtons?: ReactNode[];
Expand All @@ -38,7 +40,7 @@ export function ImageViewer({
const isOpen = index !== null;

useEffect(() => {
if (!isOpen) {
if (!isOpen || typeof document === 'undefined') {
return;
}

Expand Down Expand Up @@ -71,16 +73,30 @@ export function ImageViewer({
alt: commitImageAlt(image, imageIndex, all.length),
width: image.width,
height: image.height,
download: {
url: downloadUrl(image),
filename: `${image.id}.webp`,
},
...(downloadUrl && {
download: {
url: downloadUrl(image),
filename:
downloadFilename?.(image, imageIndex) ??
`${image.id}.webp`,
},
}),
})),
[images, downloadUrl],
[images, downloadUrl, downloadFilename],
);

const hasMultipleImages = slides.length > 1;

const plugins = useMemo(() => {
const list = hasMultipleImages ? [Counter, Zoom] : [Zoom];

if (downloadUrl) {
list.push(Download);
}

return list;
}, [hasMultipleImages, downloadUrl]);

return (
<Lightbox
open={isOpen}
Expand All @@ -94,9 +110,7 @@ export function ImageViewer({
}
},
}}
plugins={
hasMultipleImages ? [Counter, Zoom, Download] : [Zoom, Download]
}
plugins={plugins}
className={className}
styles={{
...styles,
Expand Down
Loading