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>
68 lines
2.6 KiB
TypeScript
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>
|
|
)
|
|
}
|