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();
|
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', () => {
|
it('FE-COMP-PLACES-010: shows place count', () => {
|
||||||
const places = [buildPlace({ name: 'P1' }), buildPlace({ name: 'P2' }), buildPlace({ name: 'P3' })];
|
const places = [buildPlace({ name: 'P1' }), buildPlace({ name: 'P2' }), buildPlace({ name: 'P3' })];
|
||||||
render(<PlacesSidebar {...defaultProps} places={places} />);
|
render(<PlacesSidebar {...defaultProps} places={places} />);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function PlacesList(S: SidebarState) {
|
|||||||
const {
|
const {
|
||||||
filtered, scrollContainerRef, onScrollTopChange, filter, t, canEditPlaces, onAddPlace,
|
filtered, scrollContainerRef, onScrollTopChange, filter, t, canEditPlaces, onAddPlace,
|
||||||
categories, selectedPlaceId, plannedIds, inDaySet, selectedIds, selectMode, selectedDayId,
|
categories, selectedPlaceId, plannedIds, inDaySet, selectedIds, selectMode, selectedDayId,
|
||||||
isMobile, onPlaceClick, openContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace,
|
isMobile, onPlaceClick, openContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace, registerPlaceRow,
|
||||||
} = S
|
} = S
|
||||||
return (
|
return (
|
||||||
<div className="trek-stagger" style={{ flex: 1, overflowY: 'auto', minHeight: 0 }} ref={scrollContainerRef} onScroll={(e) => onScrollTopChange?.((e.currentTarget as HTMLElement).scrollTop)}>
|
<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}
|
onAssignToDay={onAssignToDay}
|
||||||
toggleSelected={toggleSelected}
|
toggleSelected={toggleSelected}
|
||||||
setDayPickerPlace={setDayPickerPlace}
|
setDayPickerPlace={setDayPickerPlace}
|
||||||
|
registerPlaceRow={registerPlaceRow}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,17 +21,21 @@ interface MemoPlaceRowProps {
|
|||||||
onAssignToDay: (placeId: number, dayId?: number) => void
|
onAssignToDay: (placeId: number, dayId?: number) => void
|
||||||
toggleSelected: (id: number) => void
|
toggleSelected: (id: number) => void
|
||||||
setDayPickerPlace: (place: any) => void
|
setDayPickerPlace: (place: any) => void
|
||||||
|
registerPlaceRow: (placeId: number, element: HTMLDivElement | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemoPlaceRow = React.memo(function MemoPlaceRow({
|
export const MemoPlaceRow = React.memo(function MemoPlaceRow({
|
||||||
place, category: cat, isSelected, isPlanned, inDay, isChecked,
|
place, category: cat, isSelected, isPlanned, inDay, isChecked,
|
||||||
selectMode, selectedDayId, canEditPlaces, isMobile, t,
|
selectMode, selectedDayId, canEditPlaces, isMobile, t,
|
||||||
onPlaceClick, onContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace,
|
onPlaceClick, onContextMenu, onAssignToDay, toggleSelected, setDayPickerPlace, registerPlaceRow,
|
||||||
}: MemoPlaceRowProps) {
|
}: MemoPlaceRowProps) {
|
||||||
const hasGeometry = Boolean(place.route_geometry)
|
const hasGeometry = Boolean(place.route_geometry)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={place.id}
|
key={place.id}
|
||||||
|
ref={element => registerPlaceRow(place.id, element)}
|
||||||
|
aria-selected={isSelected}
|
||||||
|
data-place-id={place.id}
|
||||||
draggable={!selectMode}
|
draggable={!selectMode}
|
||||||
onDragStart={e => {
|
onDragStart={e => {
|
||||||
e.dataTransfer.setData('placeId', String(place.id))
|
e.dataTransfer.setData('placeId', String(place.id))
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
|||||||
const [sidebarDragOver, setSidebarDragOver] = useState(false)
|
const [sidebarDragOver, setSidebarDragOver] = useState(false)
|
||||||
const sidebarDragCounter = useRef(0)
|
const sidebarDragCounter = useRef(0)
|
||||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const placeRowRefs = useRef(new Map<number, HTMLDivElement>())
|
||||||
|
const lastAutoScrolledPlaceIdRef = useRef<number | null>(null)
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (scrollContainerRef.current && initialScrollTop) {
|
if (scrollContainerRef.current && initialScrollTop) {
|
||||||
scrollContainerRef.current.scrollTop = initialScrollTop
|
scrollContainerRef.current.scrollTop = initialScrollTop
|
||||||
@@ -197,6 +199,28 @@ export function usePlacesSidebar(props: PlacesSidebarProps) {
|
|||||||
return true
|
return true
|
||||||
}), [places, filter, categoryFilters, search, plannedIds])
|
}), [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) =>
|
const isAssignedToSelectedDay = (placeId) =>
|
||||||
selectedDayId && (assignments[String(selectedDayId)] || []).some(a => a.place?.id === 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,
|
selectMode, setSelectMode, selectedIds, setSelectedIds, pendingDeleteIds, setPendingDeleteIds,
|
||||||
exitSelectMode, toggleSelected, toggleCategoryFilter, dayPickerPlace, setDayPickerPlace,
|
exitSelectMode, toggleSelected, toggleCategoryFilter, dayPickerPlace, setDayPickerPlace,
|
||||||
catDropOpen, setCatDropOpen, mobileShowDays, setMobileShowDays,
|
catDropOpen, setCatDropOpen, mobileShowDays, setMobileShowDays,
|
||||||
hasTracks, plannedIds, filtered, isAssignedToSelectedDay, inDaySet, openContextMenu,
|
hasTracks, plannedIds, filtered, registerPlaceRow, isAssignedToSelectedDay, inDaySet, openContextMenu,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user