Files
accounted/components/ui/open-in-new-tab.tsx
T
Jakob Wennberg fa394e3759 fix(skattekonto): look-alike beslut rows, the list-to-voucher round trip, makulerad rendering, huvudbok discoverability (#1297)
Four fixes from the exit mail Anders Orback (Center Node AB) sent hours
after churning. His five points were mostly one job: reconciling
skattekontot against banken before årsredovisningen.

Skattekonto look-alike rows. Skatteverket splits a retroactive
omprövningsbeslut across every month it re-charges and sends one
transaction per month, sharing date, text and amount; only
ranteberakningsdatum separates them, and we stored it but rendered it
nowhere. A real company posted 15 such vouchers (67 785 kr across Feb
2025-Apr 2026) unable to tell them from duplicates of the automatic
hämtning. Surface the field when it carries information: its month
differs from the Datum column, or another row in the same band is
otherwise indistinguishable.

The list-to-voucher round trip. The verifikat list collapsed to a
skeleton on every refetch and sprang back, moving rows under the
pointer; only the first load shows a skeleton now. Filter state is
React-only, so leaving the list loses it: add a hover-revealed
open-in-new-tab affordance on the voucher list and the skattekonto page,
where the link had been behind a hand-rolled opacity-0 that coarse
pointers never trigger.

Makulerad rendering. A stornoed verifikat now reads as struck out, per
data cell rather than on the row, because text-decoration propagates and
a child cannot opt out.

Vouchers-per-account discoverability. /reports/huvudbok?account=1930
already existed; the palette matcher requires every token and the entry
never contained the word "verifikat". Add ReportDescriptor.searchTerms
plus a report-library search box.

Also fixes a false "Saknar underlag" compliance chip that flashed before
attachment counts resolved, and a keyboard-access regression where
HOVER_REVEAL_CLASS carried focus-visible only, hiding controls inside a
non-focusable wrapper from keyboard users.

No migration. No write paths, storno paths or posted entries touched.

Follow-ups filed: #1300 #1301 #1302 #1303 #1304 #1305 #1306 #1307 #1308.
Open decision: #1305 (Omförd vs Makulerad).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:27:29 +02:00

68 lines
2.6 KiB
TypeScript

'use client'
import Link from 'next/link'
import { ExternalLink } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import { HOVER_REVEAL_CLASS } from '@/components/ui/dry-table'
/**
* Escape hatch out of a list without losing it.
*
* Working a filtered list (kontoavstämning, granskning) means opening one
* record, fixing it, and coming back to the same list. Navigating in place
* throws away the filter, the page and the scroll position, so the only way
* to keep them is to open the record in a second tab. Most of our row links
* are plain anchors, so cmd-click already works, but nothing on screen ever
* says so and a few call sites are buttons where it does not work at all.
*
* Sits next to the record's own link rather than replacing it: the primary
* click keeps navigating in place, which is what people expect. Hover-revealed
* per the row-control convention, and always visible on coarse pointers.
*/
export function OpenInNewTab({
href,
label,
className,
}: {
href: string
/** Overrides the default "Öppna i ny flik" for a more specific target. */
label?: string
className?: string
}) {
const t = useTranslations('common')
const text = label ?? t('open_in_new_tab')
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
// Named by the visually-hidden text below rather than aria-label, so the
// title renders a tooltip for sighted users without screen readers
// announcing the same string twice.
title={text}
onClick={(e) => e.stopPropagation()}
// Rows that own this control are themselves interactive: JournalEntryList
// gives its <tr> an Enter/Space handler that calls preventDefault() and
// expands the row. Without this, Enter on a focused link would expand the
// row instead of opening the voucher, so the control would be usable with
// a mouse but not a keyboard.
onKeyDown={(e) => e.stopPropagation()}
className={cn(
HOVER_REVEAL_CLASS,
'relative inline-flex shrink-0 items-center rounded p-1 text-muted-foreground',
// The icon stays 14px so the row keeps its density, but the pointer
// target is padded out to the 40px the design rules require.
'before:absolute before:left-1/2 before:top-1/2 before:h-10 before:w-10',
'before:-translate-x-1/2 before:-translate-y-1/2 before:content-[""]',
'transition-colors duration-150 hover:text-foreground',
className,
)}
>
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
<span className="sr-only">{text}</span>
</Link>
)
}