Skip to content

Commit

Permalink
Make it easier to go to prev/next occurrence (#659)
Browse files Browse the repository at this point in the history
* feat: add buttons for navigating to prev and next occurrence

* feat: add keyboard quick actions for occurrence navigation

* fix: define keyboard quick actions exceptions

* fix: scroll current occurrence into view view on navigate

* fix: cleanup
  • Loading branch information
annavik authored Dec 19, 2024
1 parent c5c9eb5 commit 894eb4d
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 15 deletions.
1 change: 1 addition & 0 deletions ui/src/app.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
height: 100dvh;
overflow-x: auto;
overflow-y: auto;
scroll-padding: 64px 0;
}

.main {
Expand Down
13 changes: 7 additions & 6 deletions ui/src/components/gallery/gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,22 @@ export const Gallery = ({
(item) =>
renderItem?.(item) ??
(item.to ? (
<Link key={item.id} to={item.to}>
<Link id={item.id} key={item.id} to={item.to}>
<Card
title={item.title}
subTitle={item.subTitle}
image={item.image}
size={cardSize}
subTitle={item.subTitle}
title={item.title}
/>
</Link>
) : (
<Card
key={item.id}
title={item.title}
subTitle={item.subTitle}
id={item.id}
image={item.image}
key={item.id}
size={cardSize}
subTitle={item.subTitle}
title={item.title}
/>
))
)}
Expand Down
4 changes: 3 additions & 1 deletion ui/src/design-system/components/card/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum CardSize {

interface CardProps {
children?: ReactNode
id?: string
image?: {
src: string
alt?: string
Expand All @@ -24,6 +25,7 @@ interface CardProps {

export const Card = ({
children,
id,
image,
maxWidth,
size = CardSize.Medium,
Expand All @@ -32,7 +34,7 @@ export const Card = ({
to,
}: CardProps) => {
return (
<div className={styles.container} style={{ maxWidth }}>
<div id={id} className={styles.container} style={{ maxWidth }}>
<div className={styles.square}>
{image ? (
to ? (
Expand Down
7 changes: 3 additions & 4 deletions ui/src/design-system/components/dialog/dialog.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ $dialog-padding-medium: 32px;
max-width: calc(100% - (2 * $dialog-padding-large));
height: calc(100vh - (2 * $dialog-padding-large));
height: calc(100dvh - (2 * $dialog-padding-large));
border-radius: 4px;
background-color: $color-generic-white;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
z-index: 3;
overflow: hidden;

&.compact {
height: fit-content;
Expand All @@ -54,6 +50,9 @@ $dialog-padding-medium: 32px;
.dialogContent {
height: inherit;
max-width: 100%;
border-radius: 4px;
background-color: $color-generic-white;
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
overflow-y: auto;
overflow-x: auto;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
height: 2px;
width: 100%;
background-color: $color-neutral-100;
z-index: -1;
z-index: 0;
}

.circle {
Expand Down
5 changes: 4 additions & 1 deletion ui/src/pages/occurrences/occurrence-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const columns: (
sortField: 'determination__name',
renderCell: (item: Occurrence) => (
<TaxonCell
id={item.id}
item={item}
projectId={projectId}
showQuickActions={showQuickActions}
Expand Down Expand Up @@ -168,10 +169,12 @@ export const columns: (
]

const TaxonCell = ({
id,
item,
projectId,
showQuickActions,
}: {
id?: string
item: Occurrence
projectId: string
showQuickActions?: boolean
Expand All @@ -189,7 +192,7 @@ const TaxonCell = ({
const agreed = userInfo ? item.userAgreed(userInfo.id) : false

return (
<div className={styles.taxonCell}>
<div id={id} className={styles.taxonCell}>
<BasicTableCell>
<div className={styles.taxonCellContent}>
<Link to={detailsRoute}>
Expand Down
112 changes: 112 additions & 0 deletions ui/src/pages/occurrences/occurrence-navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Occurrence } from 'data-services/models/occurrence'
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
import { Button } from 'nova-ui-kit'
import { useCallback, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { APP_ROUTES } from 'utils/constants'
import { getAppRoute } from 'utils/getAppRoute'

const useOccurrenceNavigation = (occurrences?: Occurrence[]) => {
const { projectId, id } = useParams()
const navigate = useNavigate()
const currentIndex = occurrences?.findIndex((o) => o.id === id)
const prevId =
currentIndex !== undefined ? occurrences?.[currentIndex - 1]?.id : undefined
const nextId =
currentIndex !== undefined ? occurrences?.[currentIndex + 1]?.id : undefined

const goToPrev = useCallback(() => {
if (!prevId) {
return
}

navigate(
getAppRoute({
to: APP_ROUTES.OCCURRENCE_DETAILS({
projectId: projectId as string,
occurrenceId: prevId,
}),
keepSearchParams: true,
})
)
}, [nextId])

const goToNext = useCallback(() => {
if (!nextId) {
return
}

navigate(
getAppRoute({
to: APP_ROUTES.OCCURRENCE_DETAILS({
projectId: projectId as string,
occurrenceId: nextId,
}),
keepSearchParams: true,
})
)
}, [nextId])

return {
prevId,
nextId,
goToPrev,
goToNext,
}
}

export const OccurrenceNavigation = ({
occurrences,
}: {
occurrences?: Occurrence[]
}) => {
const { prevId, nextId, goToPrev, goToNext } =
useOccurrenceNavigation(occurrences)

// Listen to key down events
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (
document.activeElement?.matches('input') ||
document.activeElement?.role === 'tab'
) {
return
}

if (e.key === 'ArrowLeft') {
e.preventDefault()
goToPrev()
} else if (e.key === 'ArrowRight') {
e.preventDefault()
goToNext()
}
}

document.addEventListener('keydown', onKeyDown)

return () => document.removeEventListener('keydown', onKeyDown)
}, [goToPrev, goToNext])

return (
<>
<Button
className="absolute top-[50%] -left-8 -translate-x-full -translate-y-1/2 z-50"
disabled={!prevId}
onClick={goToPrev}
size="icon"
variant="outline"
>
<ChevronLeftIcon className="w-4 h-4" />
</Button>
<Button
className="absolute top-[50%] -right-8 translate-x-full -translate-y-1/2 z-50"
disabled={!nextId}
onClick={goToNext}
size="icon"
variant="outline"
>
<ChevronRightIcon className="w-4 h-4" />
</Button>
</>
)
}
27 changes: 25 additions & 2 deletions ui/src/pages/occurrences/occurrences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FilterSection } from 'components/filtering/filter-section'
import { someActive } from 'components/filtering/utils'
import { useOccurrenceDetails } from 'data-services/hooks/occurrences/useOccurrenceDetails'
import { useOccurrences } from 'data-services/hooks/occurrences/useOccurrences'
import { Occurrence } from 'data-services/models/occurrence'
import { BulkActionBar } from 'design-system/components/bulk-action-bar/bulk-action-bar'
import * as Dialog from 'design-system/components/dialog/dialog'
import { IconType } from 'design-system/components/icon/icon'
Expand All @@ -29,6 +30,7 @@ import { useSort } from 'utils/useSort'
import { OccurrenceActions } from './occurrence-actions'
import { columns } from './occurrence-columns'
import { OccurrenceGallery } from './occurrence-gallery'
import { OccurrenceNavigation } from './occurrence-navigation'

export const Occurrences = () => {
const { user } = useUser()
Expand Down Expand Up @@ -68,6 +70,18 @@ export const Occurrences = () => {
)
const { selectedView, setSelectedView } = useSelectedView('table')

useEffect(() => {
document.getElementById('app')?.scrollTo({ top: 0 })
}, [pagination.page])

useEffect(() => {
if (id) {
document
.getElementById(id)
?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
}, [id])

return (
<>
<div className="flex flex-col gap-6 md:flex-row">
Expand Down Expand Up @@ -181,12 +195,20 @@ export const Occurrences = () => {
/>
) : null}
</PageFooter>
{id ? <OccurrenceDetailsDialog id={id} /> : null}
{id ? (
<OccurrenceDetailsDialog id={id} occurrences={occurrences} />
) : null}
</>
)
}

const OccurrenceDetailsDialog = ({ id }: { id: string }) => {
const OccurrenceDetailsDialog = ({
id,
occurrences,
}: {
id: string
occurrences?: Occurrence[]
}) => {
const navigate = useNavigate()
const { projectId } = useParams()
const { setDetailBreadcrumb } = useContext(BreadcrumbContext)
Expand Down Expand Up @@ -220,6 +242,7 @@ const OccurrenceDetailsDialog = ({ id }: { id: string }) => {
error={error}
>
{occurrence ? <OccurrenceDetails occurrence={occurrence} /> : null}
<OccurrenceNavigation occurrences={occurrences} />
</Dialog.Content>
</Dialog.Root>
)
Expand Down

0 comments on commit 894eb4d

Please sign in to comment.