diff --git a/frontend/src/components/BackLink.css b/frontend/src/components/BackLink.css new file mode 100644 index 0000000..dbac36b --- /dev/null +++ b/frontend/src/components/BackLink.css @@ -0,0 +1,33 @@ +/* A link styled like the other secondary buttons (Load more, Clear + filters). Inline-block so the padding keeps its space outside a grid + too, as on the submit page. */ +.back-link { + display: inline-block; + justify-self: start; + padding: 7px 14px; + border-radius: 12px; + color: var(--accent-strong); + background: var(--accent-bg); + font-size: 15px; + font-weight: 600; + text-decoration: none; + transition: background-color 0.15s; + + &:hover { + background: var(--accent-bg-strong); + } + + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + + /* High-contrast modes drop the tint, so a border keeps the shape. */ + @media (forced-colors: active) { + border: 1px solid LinkText; + } + + @media (prefers-reduced-motion: reduce) { + transition: none; + } +} diff --git a/frontend/src/components/BackLink.tsx b/frontend/src/components/BackLink.tsx new file mode 100644 index 0000000..d1cc690 --- /dev/null +++ b/frontend/src/components/BackLink.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' +import { Link, type To } from 'react-router' +import './BackLink.css' + +interface BackLinkProps { + to: To + children: ReactNode +} + +/** + * The link back to the list at the top of the detail and submit pages, so + * it has the same shape, spacing and arrow on both. The arrow is hidden from + * screen readers, which read just the link text. + */ +function BackLink({ to, children }: BackLinkProps) { + return ( + + + {children} + + ) +} + +export default BackLink diff --git a/frontend/src/pages/SightingDetailPage.css b/frontend/src/pages/SightingDetailPage.css index 99f5692..bfe9f1d 100644 --- a/frontend/src/pages/SightingDetailPage.css +++ b/frontend/src/pages/SightingDetailPage.css @@ -15,38 +15,6 @@ border: 0; } -/* A link styled like the other secondary buttons (Load more, Clear - filters). */ -.sighting-detail__back { - justify-self: start; - padding: 7px 14px; - border-radius: 12px; - color: var(--accent-strong); - background: var(--accent-bg); - font-size: 15px; - font-weight: 600; - text-decoration: none; - transition: background-color 0.15s; - - &:hover { - background: var(--accent-bg-strong); - } - - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } - - /* High-contrast modes drop the tint, so a border keeps the shape. */ - @media (forced-colors: active) { - border: 1px solid LinkText; - } - - @media (prefers-reduced-motion: reduce) { - transition: none; - } -} - .sighting-detail h1 { font-size: 40px; margin: 0 0 4px; diff --git a/frontend/src/pages/SightingDetailPage.tsx b/frontend/src/pages/SightingDetailPage.tsx index bc1b7f8..a572bab 100644 --- a/frontend/src/pages/SightingDetailPage.tsx +++ b/frontend/src/pages/SightingDetailPage.tsx @@ -1,6 +1,7 @@ import { CombinedGraphQLErrors } from '@apollo/client' import { useEffect } from 'react' -import { Link, useLocation, useParams } from 'react-router' +import { useLocation, useParams } from 'react-router' +import BackLink from '../components/BackLink' import SightingMap from '../components/SightingMap' import UserSubmittedBadge from '../components/UserSubmittedBadge' import { useDocumentTitle } from '../hooks/use-document-title' @@ -51,11 +52,7 @@ function SightingDetailPage() { useDocumentTitle(title) - const backLink = ( - - Back to results - - ) + const backLink = Back to results return ( <> diff --git a/frontend/src/pages/SubmitSightingPage.css b/frontend/src/pages/SubmitSightingPage.css index 5b65773..e82ebe7 100644 --- a/frontend/src/pages/SubmitSightingPage.css +++ b/frontend/src/pages/SubmitSightingPage.css @@ -3,13 +3,9 @@ margin: 0 auto; } -.submit-sighting__back { - color: var(--accent); - text-underline-offset: 4px; -} - +/* The same 24px below the back link as on the detail page. */ .submit-sighting h1 { - margin-bottom: 16px; + margin: 24px 0 16px; } .submit-sighting__intro { @@ -86,8 +82,7 @@ .submit-sighting__field select:focus-visible, .submit-sighting__field textarea:focus-visible, .submit-sighting button:focus-visible, -.submit-sighting__button:focus-visible, -.submit-sighting__back:focus-visible { +.submit-sighting__button:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } diff --git a/frontend/src/pages/SubmitSightingPage.tsx b/frontend/src/pages/SubmitSightingPage.tsx index 483d020..08bd82e 100644 --- a/frontend/src/pages/SubmitSightingPage.tsx +++ b/frontend/src/pages/SubmitSightingPage.tsx @@ -6,6 +6,7 @@ import { CREATE_SIGHTING_MUTATION, type SightingFieldError, } from '../graphql/mutations' +import BackLink from '../components/BackLink' import LocationPicker from '../components/LocationPicker' import { SHAPES_QUERY } from '../graphql/queries' @@ -254,9 +255,7 @@ function SubmitSightingPage() { if (submittedId) { return (
- - Back to sightings - + Back to sightings

Sighting submitted

@@ -286,9 +285,7 @@ function SubmitSightingPage() { return (
- - Back to sightings - + Back to sightings

Report a sighting

diff --git a/frontend/src/tests/BackLink.test.tsx b/frontend/src/tests/BackLink.test.tsx new file mode 100644 index 0000000..4697ea9 --- /dev/null +++ b/frontend/src/tests/BackLink.test.tsx @@ -0,0 +1,20 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter } from 'react-router' +import { describe, expect, it } from 'vitest' +import BackLink from '../components/BackLink' + +describe('BackLink', () => { + it('links to its target and is named by its text, not the arrow', () => { + render( + + + Back to results + + , + ) + + const link = screen.getByRole('link', { name: 'Back to results' }) + expect(link).toHaveAttribute('href', '/?q=disk') + expect(link).toHaveTextContent('← Back to results') + }) +})