feat: Add jump-to-page dialog and improve pagination logic and styling (#1303)

* feat: add jump-to-page dialog on pagination ellipsis

- Replace pagination windowing algorithm with a tighter 5-item window
  (curr-1 / curr / curr+1, clamped at edges) and hide-on-mobile for
  pages more than 1 step away from the current page
- Ellipsis spans now carry the pagination-jump-trigger class and open
  a native <dialog> on click instead of being static decorations
- Add <dialog id="pagination-jump-dialog"> with number input, Go button,
  and Enter-key hint; data-total / data-first-url / data-format-url
  attributes on <nav> give the JS all it needs to build the target URL
- Add assets/ts/pagination.ts: setupPaginationJump() with Fisher-Yates
  backdrop-click and ESC handling, animated open/close via .closing class
- Wire setupPaginationJump() into Stack.init() in main.ts
- Extend pagination.scss with .hide-on-mobile, .pagination-jump-trigger
  hover style, and the full #pagination-jump-dialog styling + keyframe
  scale-up/scale-down animations
- Add [pagination] i18n keys (jumpToPage / jump / pressEnter) to
  en / zh / zh-hant-tw / zh-hant-hk / ja

* style: align pagination code with upstream conventions

- pagination.html: collapse multi-line span/class/aria attrs to single
  lines; use HTML5 void element syntax; fix hint div indentation
- pagination.scss: 2-space indent → 4-space indent
- pagination.ts: tab indent → 4-space indent; export const arrow fn →
  export function declaration

* chore: update layouts/_partials/pagination.html

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: add aria-labelledby attrributes for better accesibility

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: use { once: true } for the listener and guard against re-entry when .closing is already set.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* style: replace hardcoded values with design tokens and improve dialog UX

- Replace all hardcoded px spacing in .page-link with --spacing-* tokens
- Use @include card-style in #pagination-jump-dialog; override with --shadow-l2
- Replace padding: 24px with var(--card-padding) for responsive dialog padding
- Align header title/subtitle to space-between for cleaner layout
- Replace hardcoded gaps/padding in form with spacing tokens
- Fix input/button border-radius from 8px to 6px (better proportion for 44px height)
- Fix focus ring: use color-mix(accent-color 20%) so it's visible in dark mode
- Add :focus-visible + outline:none to fully suppress native browser outline
- Remove translateY from button hover for more restrained interaction
- Scope transition to specific properties (border-color, box-shadow, background-color)

* chore: add pagination test articles for easier debugging

* style: enhance backdrop animations for pagination jump dialog

* style: replace pagination ellipsis spans with buttons for improved accessibility

* fix: check if browser supports dialog API before continue with pagination jump setup

* fix: retain focus on last active element when closing pagination jump dialog

* fix: style for page-link button (pagination-jump)

* style: adjust width of pagination jump dialog, add container padding

* style: simplify

* refactor: simplify pagination link logic

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jimmy Cai <jimmy@cai.im>
This commit is contained in:
powerfullz
2026-04-26 12:41:31 +02:00
committed by GitHub
co-authored by Copilot Jimmy Cai
parent 8e3687431f
commit 37f6a77a1a
22 changed files with 646 additions and 67 deletions
+129 -61
View File
@@ -1,70 +1,138 @@
{{- $pag := $.Paginator -}}
{{- if gt $pag.TotalPages 1 -}}
<nav class="pagination" role="navigation" aria-label="pagination">
{{- /* Previous page button */ -}}
<a class="page-link {{ if not $pag.Prev }}disabled{{ end }}"
{{- if $pag.Prev -}}href="{{ $pag.Prev.URL }}" {{- end -}}
aria-label="Previous page"
{{- if not $pag.Prev -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-left" }}
</a>
{{- /* Page numbers logic */ -}}
{{- $window := 4 -}}
{{- $showFirst := true -}}
{{- $showLast := true -}}
{{- /* First page */ -}}
<a class="page-link {{ if eq $pag.PageNumber 1 }}current{{ end }}" aria-label="Page 1"
{{- if eq $pag.PageNumber 1 -}}aria-current="page" {{- end -}}
{{- if ne $pag.PageNumber 1 -}}href="{{ $pag.First.URL }}"{{- end -}}>
1
</a>
{{- /* Left ellipsis - show if current page is > 3 */ -}}
{{- if gt $pag.PageNumber 3 -}}
<span class="page-link pagination-link">
{{ partial "helper/icon" "dots" }}
</span>
{{- end -}}
{{- /* Middle pages - show current and neighbors */ -}}
{{- /* Find page 2's URL to extract the pagination base path for JavaScript */ -}}
{{- $page2Url := "" -}}
{{- range $pag.Pagers -}}
{{- if and (gt .PageNumber 1) (lt .PageNumber $pag.TotalPages) -}}
{{- if and (ge .PageNumber (sub $pag.PageNumber $window)) (le .PageNumber (add $pag.PageNumber $window)) -}}
<a class="page-link {{ if eq .PageNumber $pag.PageNumber }}current{{ end }}"
{{- if eq .PageNumber $pag.PageNumber -}}aria-current="page" {{- end -}} href="{{ .URL }}"
aria-label="Page {{ .PageNumber }}">
{{ .PageNumber }}
</a>
{{- end -}}
{{- if eq .PageNumber 2 -}}
{{- $page2Url = .URL -}}
{{- end -}}
{{- end -}}
{{- /* Right ellipsis - show if current page is < last-2 */ -}}
{{- if lt $pag.PageNumber (sub $pag.TotalPages 2) -}}
<span class="page-link pagination-link">
{{ partial "helper/icon" "dots" }}
</span>
{{- end -}}
<nav
class="pagination"
role="navigation"
aria-label="pagination"
data-total="{{ $pag.TotalPages }}"
data-first-url="{{ $pag.First.URL }}"
data-format-url="{{ $page2Url }}">
{{- /* Previous page button */ -}}
<a
class="page-link {{ if not $pag.Prev }}disabled{{ end }}"
{{- if $pag.Prev -}}href="{{ $pag.Prev.URL }}"{{- end -}}
aria-label="Previous page"
{{- if not $pag.Prev -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-left" }}
</a>
{{- /* Last page (if more than 1 page total) */ -}}
{{- /* Page numbers logic */ -}}
{{- /* Compute visible middle window [start, end] (excluding first and last page). */ -}}
{{- /* Keep 3 middle pages when possible; if total <= 5, show all middle pages. */ -}}
{{- $curr := $pag.PageNumber -}}
{{- $total := $pag.TotalPages -}}
{{- $middleMinPage := 2 -}}
{{- $middleMaxPage := sub $total 1 -}}
{{- $start := $middleMinPage -}}
{{- $end := $middleMaxPage -}}
{{- if gt $pag.TotalPages 1 -}}
<a class="page-link {{ if eq $pag.PageNumber $pag.TotalPages }}current{{ end }}"
{{- if ne $pag.PageNumber $pag.TotalPages -}}href="{{ $pag.Last.URL }}" {{- end -}}
aria-label="Page {{ $pag.TotalPages }}"
{{- if eq $pag.PageNumber $pag.TotalPages -}}aria-current="page"{{- end -}}>
{{ $pag.TotalPages }}
</a>
{{- end -}}
{{- if gt $total 5 -}}
{{- $start = sub $curr 1 -}}
{{- if lt $start $middleMinPage -}}
{{- $start = $middleMinPage -}}
{{- end -}}
{{- $end = add $start 2 -}}
{{- if gt $end $middleMaxPage -}}
{{- $end = $middleMaxPage -}}
{{- $start = sub $end 2 -}}
{{- end -}}
{{- end -}}
{{- /* Next page button */ -}}
<a class="page-link {{ if not $pag.Next }}disabled{{ end }}"
{{- if $pag.Next -}}href="{{ $pag.Next.URL }}" {{- end -}}
aria-label="Next page"
{{- if not $pag.Next -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-right" }}
</a>
</nav>
{{- end -}}
{{- /* First page */ -}}
<a
class="page-link {{ if eq $curr 1 }}current{{ end }}"
aria-label="Page 1"
{{- if eq $curr 1 -}}aria-current="page"{{- end -}}
{{- if ne $curr 1 -}}href="{{ $pag.First.URL }}"{{- end -}}>
1
</a>
{{- /* Left ellipsis - show if the window start is beyond page 2 */ -}}
{{- if gt $start $middleMinPage -}}
<button
type="button"
class="page-link pagination-link pagination-jump-trigger"
aria-label="{{ i18n "pagination.jumpToPage" | default "Jump to page" }}">
{{ partial "helper/icon" "dots" }}
</button>
{{- end -}}
{{- /* Middle pages - show pages in [start, end] */ -}}
{{- range $pag.Pagers -}}
{{- if and (ge .PageNumber $start) (le .PageNumber $end) -}}
{{- $hideOnMobile := and (ne .PageNumber $curr) (ne .PageNumber (sub $curr 1)) (ne .PageNumber (add $curr 1)) -}}
<a
class="page-link {{ if eq .PageNumber $curr }}current{{ end }} {{ if $hideOnMobile }}hide-on-mobile{{ end }}"
{{- if eq .PageNumber $curr -}}aria-current="page"{{- end -}}
href="{{ .URL }}"
aria-label="Page {{ .PageNumber }}">
{{ .PageNumber }}
</a>
{{- end -}}
{{- end -}}
{{- /* Right ellipsis - show if the window end is before the second-to-last page */ -}}
{{- if lt $end $middleMaxPage -}}
<button
type="button"
class="page-link pagination-link pagination-jump-trigger"
aria-label="{{ i18n "pagination.jumpToPage" | default "Jump to page" }}">
{{ partial "helper/icon" "dots" }}
</button>
{{- end -}}
{{- /* Last page */ -}}
<a
class="page-link {{ if eq $curr $total }}current{{ end }}"
{{- if ne $curr $total -}}href="{{ $pag.Last.URL }}"{{- end -}}
aria-label="Page {{ $total }}"
{{- if eq $curr $total -}}aria-current="page"{{- end -}}>
{{ $total }}
</a>
{{- /* Next page button */ -}}
<a
class="page-link {{ if not $pag.Next }}disabled{{ end }}"
{{- if $pag.Next -}}href="{{ $pag.Next.URL }}"{{- end -}}
aria-label="Next page"
{{- if not $pag.Next -}}aria-disabled="true"{{- end -}}>
{{ partial "helper/icon" "chevron-right" }}
</a>
</nav>
<dialog id="pagination-jump-dialog" aria-labelledby="pagination-jump-title">
<form method="dialog" class="pagination-jump-form">
<header>
<label id="pagination-jump-title" class="title" for="pagination-jump-input">
{{ i18n "pagination.jumpToPage" | default "Jump to page:" }}
</label>
<span class="subtitle">(1 - {{ $pag.TotalPages }})</span>
</header>
<div class="pagination-jump-input-group">
<input
type="number"
id="pagination-jump-input"
min="1"
max="{{ $pag.TotalPages }}"
autofocus
autocomplete="off"
required>
<button type="submit" class="btn btn-primary">
{{ i18n "pagination.jump" | default "Go" }}
</button>
</div>
<div class="pagination-jump-hint">
<kbd>Enter</kbd>
<span>{{ i18n "pagination.pressEnter" | default "Press Enter to jump" }}</span>
</div>
</form>
</dialog>
{{- end -}}