-
-
Notifications
You must be signed in to change notification settings - Fork 84
Koenig: Improve Unsplash Modal Responsiveness #1576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1c3b2d7
52e94cc
26d1316
9154f31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,9 @@ interface UnsplashModalProps { | |||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onImageInsert, unsplashProviderConfig}) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const ONE_COLUMN_WIDTH = 540; | ||||||||||||||||||||||||||||||||||||||||||||
| const TWO_COLUMN_WIDTH = 1024; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const unsplashProvider = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (!unsplashProviderConfig) { | ||||||||||||||||||||||||||||||||||||||||||||
| return new InMemoryUnsplashProvider(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -73,6 +76,37 @@ export const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onI | |||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }, [galleryRef, zoomedImg]); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const calculateColumnCountForViewport = () => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (window.matchMedia(`(max-width: ${ONE_COLUMN_WIDTH}px)`).matches) { | ||||||||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (window.matchMedia(`(max-width: ${TWO_COLUMN_WIDTH}px)`).matches) { | ||||||||||||||||||||||||||||||||||||||||||||
| return 2; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return 3; | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| React.useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| const handleResize = () => { | ||||||||||||||||||||||||||||||||||||||||||||
| const newColumnCount = calculateColumnCountForViewport(); | ||||||||||||||||||||||||||||||||||||||||||||
| masonryService.changeColumnCount(newColumnCount); | ||||||||||||||||||||||||||||||||||||||||||||
| UnsplashLib.layoutPhotos(); | ||||||||||||||||||||||||||||||||||||||||||||
| const columns = UnsplashLib.getColumns(); | ||||||||||||||||||||||||||||||||||||||||||||
| setDataset(columns || []); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('resize', handleResize); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| // Call once to make sure state is in sync on mount | ||||||||||||||||||||||||||||||||||||||||||||
| handleResize(); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||
| window.removeEventListener('resize', handleResize); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| }, [UnsplashLib, masonryService]); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const loadInitPhotos = React.useCallback(async () => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (initLoadRef.current === false || searchTerm.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||
| setDataset([]); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -155,6 +189,15 @@ export const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onI | |||||||||||||||||||||||||||||||||||||||||||
| }, [galleryRef, loadMorePhotos, zoomedImg]); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const selectImg = (payload:Photo) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const isMobileViewport = window.matchMedia('(max-width: 540px)').matches; | ||||||||||||||||||||||||||||||||||||||||||||
| const isTouchDevice = window.matchMedia('(pointer: coarse)').matches; | ||||||||||||||||||||||||||||||||||||||||||||
| const shouldNotZoom = isMobileViewport || isTouchDevice; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| if (shouldNotZoom) { | ||||||||||||||||||||||||||||||||||||||||||||
| setZoomedImg(null); | ||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
191
to
+200
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type mismatch: allow This function is called with -const selectImg = (payload:Photo) => {
+const selectImg = (payload: Photo | null) => {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| if (payload) { | ||||||||||||||||||||||||||||||||||||||||||||
| setZoomedImg(payload); | ||||||||||||||||||||||||||||||||||||||||||||
| setLastScrollPos(scrollPos); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -176,10 +219,12 @@ export const UnsplashSearchModal : React.FC<UnsplashModalProps> = ({onClose, onI | |||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <UnsplashSelector | ||||||||||||||||||||||||||||||||||||||||||||
| closeModal={onClose} | ||||||||||||||||||||||||||||||||||||||||||||
| closeModal={onClose} | ||||||||||||||||||||||||||||||||||||||||||||
| columnCount={UnsplashLib.getColumnCount()} | ||||||||||||||||||||||||||||||||||||||||||||
| handleSearch={handleSearch} | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| <UnsplashGallery | ||||||||||||||||||||||||||||||||||||||||||||
| columnCount={UnsplashLib.getColumnCount()} | ||||||||||||||||||||||||||||||||||||||||||||
| dataset={dataset} | ||||||||||||||||||||||||||||||||||||||||||||
| error={null} | ||||||||||||||||||||||||||||||||||||||||||||
| galleryRef={galleryRef} | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,6 +45,10 @@ export class UnsplashService implements IUnsplashService { | |||||||||||||||||||||||||||||||
| return this.masonryService.getColumns(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| getColumnCount(): number { | ||||||||||||||||||||||||||||||||
| return this.masonryService.columnCount; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Expose column count in the public interface to keep API consistent
Apply this diff to the interface: export interface IUnsplashService {
loadNew(): Promise<void>;
layoutPhotos(): void;
getColumns(): Photo[][] | [] | null;
+ getColumnCount(): number;
updateSearch(term: string): Promise<void>;
loadNextPage(): Promise<void>;
clearPhotos(): void;
triggerDownload(photo: Photo): void;
photos: Photo[];
searchIsRunning(): boolean;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| async updateSearch(term: string) { | ||||||||||||||||||||||||||||||||
| let results = await this.photoUseCases.searchPhotos(term); | ||||||||||||||||||||||||||||||||
| this.photos = results; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,7 +32,7 @@ const UnsplashImage: FC<UnsplashImageProps> = ({payload, srcUrl, links, likes, u | |
|
|
||
| return ( | ||
| <div | ||
| className={`relative mb-6 block ${zoomed ? 'h-full w-[max-content] cursor-zoom-out' : 'w-full cursor-zoom-in'}`} | ||
| className={`relative mb-6 block ${zoomed ? 'h-full w-[max-content] cursor-zoom-out' : 'w-full [@media(min-width:540px)]:cursor-zoom-in'}`} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve touch UX: make overlay reliably reveal on tap (not just CSS :hover) Relying on Within the container div, add -<div
+<div
+ tabIndex={0}
className={`relative mb-6 block ${zoomed ? 'h-full w-[max-content] cursor-zoom-out' : 'w-full [@media(min-width:540px)]:cursor-zoom-in'}`}Then update the overlay to also show on focus/active (outside the selected range, shown for completeness): -<div className="absolute inset-0 flex flex-col justify-between bg-gradient-to-b from-black/5 via-black/5 to-black/30 p-5 opacity-0 transition-all ease-in-out hover:opacity-100">
+<div className="absolute inset-0 flex flex-col justify-between bg-gradient-to-b from-black/5 via-black/5 to-black/30 p-5 opacity-0 transition-all ease-in-out hover:opacity-100 focus-within:opacity-100 active:opacity-100">This preserves hover on desktop and enables single-tap reveal on touch. 🤖 Prompt for AI Agents |
||
| style={{backgroundColor: payload.color || 'transparent'}} | ||
| data-kg-unsplash-gallery-item | ||
| onClick={handleClick}> | ||
|
|
@@ -61,10 +61,10 @@ const UnsplashImage: FC<UnsplashImageProps> = ({payload, srcUrl, links, likes, u | |
| icon="download" | ||
| /> | ||
| </div> | ||
| <div className="flex items-center justify-between"> | ||
| <div className="flex items-center"> | ||
| <img alt="author" className="mr-2 size-8 rounded-full" src={user.profile_image.medium} /> | ||
| <div className="mr-2 truncate font-sans text-sm font-medium text-white">{user.name}</div> | ||
| <div className="flex items-center justify-between truncate"> | ||
| <div className="flex w-0 flex-1 items-center"> | ||
| <img alt="author" className="mr-2 size-8 shrink-0 rounded-full" src={user.profile_image.medium} /> | ||
| <div className="truncate font-sans text-sm font-medium text-white">{user.name}</div> | ||
| </div> | ||
| <UnsplashButton label="Insert image" data-kg-unsplash-insert-button onClick={(e) => { | ||
| e.stopPropagation(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Guard resize reflow to avoid unnecessary work
On every resize event you reflow masonry and reset dataset even if the column count didn’t change. Add a guard to skip heavy work when the computed count matches the current one.
const handleResize = () => { - const newColumnCount = calculateColumnCountForViewport(); - masonryService.changeColumnCount(newColumnCount); - UnsplashLib.layoutPhotos(); - const columns = UnsplashLib.getColumns(); - setDataset(columns || []); + const newColumnCount = calculateColumnCountForViewport(); + // Skip if nothing changes + if (newColumnCount === UnsplashLib.getColumnCount()) { + return; + } + masonryService.changeColumnCount(newColumnCount); + UnsplashLib.layoutPhotos(); + const columns = UnsplashLib.getColumns(); + setDataset(columns || []); };Optional: throttle via
requestAnimationFrameor a small debounce to smooth continuous resizing if needed.🤖 Prompt for AI Agents