mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
feat(planner): seek places sidebar on map selection
This commit is contained in:
@@ -124,6 +124,40 @@ describe('PlacesSidebar', () => {
|
||||
expect(screen.getByText('Central Park')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-PLACES-009a: selected visible place is scrolled into view', async () => {
|
||||
const scrollIntoView = Element.prototype.scrollIntoView as unknown as ReturnType<typeof vi.fn>;
|
||||
scrollIntoView.mockClear();
|
||||
const places = [
|
||||
buildPlace({ id: 10, name: 'First Place' }),
|
||||
buildPlace({ id: 42, name: 'Map Click Target' }),
|
||||
];
|
||||
|
||||
render(<PlacesSidebar {...defaultProps} places={places} selectedPlaceId={42} />);
|
||||
|
||||
const selectedRow = screen.getByText('Map Click Target').closest('[data-place-id="42"]');
|
||||
expect(selectedRow).toHaveAttribute('aria-selected', 'true');
|
||||
await waitFor(() => {
|
||||
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-COMP-PLACES-009b: selected place hidden by search is not scrolled', async () => {
|
||||
const user = userEvent.setup();
|
||||
const scrollIntoView = Element.prototype.scrollIntoView as unknown as ReturnType<typeof vi.fn>;
|
||||
const places = [
|
||||
buildPlace({ id: 10, name: 'Visible Cafe' }),
|
||||
buildPlace({ id: 42, name: 'Hidden Museum' }),
|
||||
];
|
||||
const { rerender } = render(<PlacesSidebar {...defaultProps} places={places} selectedPlaceId={null} />);
|
||||
|
||||
await user.type(screen.getByPlaceholderText(/Search places/i), 'Visible');
|
||||
scrollIntoView.mockClear();
|
||||
rerender(<PlacesSidebar {...defaultProps} places={places} selectedPlaceId={42} />);
|
||||
|
||||
expect(screen.queryByText('Hidden Museum')).not.toBeInTheDocument();
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('FE-COMP-PLACES-010: shows place count', () => {
|
||||
const places = [buildPlace({ name: 'P1' }), buildPlace({ name: 'P2' }), buildPlace({ name: 'P3' })];
|
||||
render(<PlacesSidebar {...defaultProps} places={places} />);
|
||||
|
||||
@@ -5,7 +5,7 @@ export function PlacesList(S: SidebarState) {
|
||||
const {
|
||||
filtered, scrollContainerRef, onScrollTopChange, filter, t, canEditPlaces, onAddPlace,
|
||||
categories, selectedPlaceId, plannedIds, inDaySet, selectedIds, selectMode, selectedDayId,
|
||||
isMobile, onPlaceClick, openContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace,
|
||||
isMobile, onPlaceClick, openContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace, registerPlaceRow,
|
||||
} = S
|
||||
return (
|
||||
<div className="trek-stagger" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }} ref={scrollContainerRef} onScroll={(e) => onScrollTopChange?.((e.currentTarget as HTMLElement).scrollTop)}>
|
||||
@@ -44,6 +44,7 @@ export function PlacesList(S: SidebarState) {
|
||||
onAssignToDay={onAssignToDay}
|
||||
toggleSelected={toggleSelected}
|
||||
setDayPickerPlace={setDayPickerPlace}
|
||||
registerPlaceRow={registerPlaceRow}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -21,17 +21,21 @@ interface MemoPlaceRowProps {
|
||||
onAssignToDay: (placeId: number, dayId?: number) => void
|
||||
toggleSelected: (id: number) => void
|
||||
setDayPickerPlace: (place: any) => void
|
||||
registerPlaceRow: (placeId: number, element: HTMLDivElement | null) => void
|
||||
}
|
||||
|
||||
export const MemoPlaceRow = React.memo(function MemoPlaceRow({
|
||||
place, category: cat, isSelected, isPlanned, inDay, isChecked,
|
||||
selectMode, selectedDayId, canEditPlaces, isMobile, t,
|
||||
onPlaceClick, onContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace,
|
||||
onPlaceClick, onContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace, registerPlaceRow,
|
||||
}: MemoPlaceRowProps) {
|
||||
const hasGeometry = Boolean(place.route_geometry)
|
||||
return (
|
||||
<div
|
||||
key={place.id}
|
||||
ref={element => registerPlaceRow(place.id, element)}
|
||||
aria-selected={isSelected}
|
||||
data-place-id={place.id}
|
||||
draggable={!selectMode}
|
||||
onDragStart={e => {
|
||||
e.dataTransfer.setData('placeId', String(place.id))
|
||||
|
||||
@@ -59,6 +59,8 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
||||
const [sidebarDragOver, setSidebarDragOver] = useState(false)
|
||||
const sidebarDragCounter = useRef(0)
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const placeRowRefs = useRef(new Map<number, HTMLDivElement>())
|
||||
const lastAutoScrolledPlaceIdRef = useRef<number | null>(null)
|
||||
useLayoutEffect(() => {
|
||||
if (scrollContainerRef.current && initialScrollTop) {
|
||||
scrollContainerRef.current.scrollTop = initialScrollTop
|
||||
@@ -197,6 +199,28 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
||||
return true
|
||||
}), [places, filter, categoryFilters, search, plannedIds])
|
||||
|
||||
const registerPlaceRow = useCallback((placeId: number, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
placeRowRefs.current.set(placeId, element)
|
||||
} else {
|
||||
placeRowRefs.current.delete(placeId)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.selectedPlaceId) {
|
||||
lastAutoScrolledPlaceIdRef.current = null
|
||||
return
|
||||
}
|
||||
if (lastAutoScrolledPlaceIdRef.current === props.selectedPlaceId) return
|
||||
if (!filtered.some(place => place.id === props.selectedPlaceId)) return
|
||||
|
||||
const selectedRow = placeRowRefs.current.get(props.selectedPlaceId)
|
||||
if (!selectedRow) return
|
||||
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
lastAutoScrolledPlaceIdRef.current = props.selectedPlaceId
|
||||
}, [filtered, props.selectedPlaceId])
|
||||
|
||||
const isAssignedToSelectedDay = (placeId) =>
|
||||
selectedDayId && (assignments[String(selectedDayId)] || []).some(a => a.place?.id === placeId)
|
||||
|
||||
@@ -234,7 +258,7 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
||||
selectMode, setSelectMode, selectedIds, setSelectedIds, pendingDeleteIds, setPendingDeleteIds,
|
||||
exitSelectMode, toggleSelected, toggleCategoryFilter, dayPickerPlace, setDayPickerPlace,
|
||||
catDropOpen, setCatDropOpen, mobileShowDays, setMobileShowDays,
|
||||
hasTracks, plannedIds, filtered, isAssignedToSelectedDay, inDaySet, openContextMenu,
|
||||
hasTracks, plannedIds, filtered, registerPlaceRow, isAssignedToSelectedDay, inDaySet, openContextMenu,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user