Files
livewire-material/resources/js/list-rows.js
T
Andreas Reinhold / reiniandClaude Opus 5 7d3abdd506 Keep a list row's fill under its state layer
Plan step 36 review of 9ff5a7a8. The rewrite dropped the unlayered
selected rule into material.components beside the segmented fill and the
row tint, so on equal or greater weight a selected item in a segmented
list drew surface instead of secondary-container, and hovering, pressing
or focusing any selected row (or a table's selected row) swapped its
fill for a translucent tint. Each fill now names itself
--md-list-row-fill, and the row's layer is its ink mixed over that fill
at the state tokens, which is also C-08's second suggested fix.

A disabled item inks the whole row at the disabled-content token, as the
old root colour did, so the check, the leading slot and the end slot dim
with the text; the three-line padding is a literal 12px like alert.css;
list-rows.js named list.css.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 22:00:03 +02:00

147 lines
4.9 KiB
JavaScript

/**
* A row opens from anywhere on it.
*
* A row is `data-md-list-row` — an `<x-list-item>`, an `<x-card>`, a table row — and exactly one
* control inside it is its opener, `data-md-list-open`: the title button that opens a sheet, or
* the link that goes to the item's page. A click anywhere else on the row is handed to the opener,
* so the whole row is the target while the opener stays a real control — the tab stop, the name a
* screen reader reads, and the owner of the `wire:click` or the `href`. The row's other controls
* keep their own clicks. How a row looks while that happens is
* resources/css/components/card.css and list-item.css.
*
* Not a stretched link (`::after { inset: 0 }`): Safari makes no containing block of a <tr>, so in
* a table every overlay would cover the whole table; and not one <button> around the row, which
* could hold no other buttons. The listeners sit on `document` and are added once.
*
* A row that is also `data-md-list-actionable` is M3's **directly actionable card** (`<x-card
* actionable>`): the card is the one tab stop, so Enter and Space on the card reach the opener,
* and the card's other actions follow it in the tab order with their own keys. The opener itself
* carries `tabindex="-1"`, which the card's caller writes.
*/
/** Anything that answers a click itself — the row's own controls, and the opener. */
const CONTROLS = 'a, button, input, select, textarea, label, summary, [contenteditable]'
/** How far a pointer may travel between press and release and still be a click. */
const DRAG_PX = 6
/**
* Where the last press went down, so a click that was really a drag — a pan across a map inside
* a card — is not taken for a click. Cleared by the click it belongs to, so a click with no press
* behind it (a script's `.click()`) is never a drag.
*/
let pressedAt = null
/**
* The opener a click on a row should reach, or null when the click is not the
* row's to hand on: it landed on a control, outside any row, or it ends a text
* selection somebody was making.
*/
const openerFor = (event) => {
if (event.defaultPrevented || !(event.target instanceof Element)) {
return null
}
if (event.target.closest(CONTROLS)) {
return null
}
const opener = event.target.closest('[data-md-list-row]')?.querySelector('[data-md-list-open]')
if (!opener) {
return null
}
if (!(window.getSelection()?.isCollapsed ?? true)) {
return null
}
return opener
}
/** A link opens in a new tab the way it would have under the same click. */
const openInNewTab = (opener) => {
if (opener instanceof HTMLAnchorElement && opener.href) {
window.open(opener.href, '_blank', 'noopener')
}
}
document.addEventListener('pointerdown', (event) => {
pressedAt = event.isPrimary ? { x: event.clientX, y: event.clientY } : null
}, { capture: true, passive: true })
document.addEventListener('click', (event) => {
const from = pressedAt
pressedAt = null
if (event.button !== 0) {
return
}
const opener = openerFor(event)
if (!opener) {
return
}
if (from && Math.hypot(event.clientX - from.x, event.clientY - from.y) > DRAG_PX) {
return
}
// Cmd, Ctrl or Shift on a row that goes somewhere is a new tab, as on the link
// itself. On a row that opens a drawer it is nothing: a drawer has no address.
if (event.metaKey || event.ctrlKey || event.shiftKey) {
openInNewTab(opener)
return
}
if (event.altKey) {
return
}
// A script's click, which Livewire honours on both halves: `wire:click` runs,
// and a `wire:navigate` link navigates in place rather than reloading the page.
opener.click()
})
// A directly actionable card is a control, so it answers Enter and Space the way a
// button does — by reaching the opener, which owns the href or the wire:click.
document.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return
}
if (event.defaultPrevented || !(event.target instanceof Element) || !event.target.matches('[data-md-list-actionable]')) {
return
}
const opener = event.target.querySelector('[data-md-list-open]')
if (!opener) {
return
}
event.preventDefault()
opener.click()
})
// The middle button is not a `click`, and on a row that goes somewhere it means
// what it means on a link.
document.addEventListener('auxclick', (event) => {
if (event.button !== 1) {
return
}
const opener = openerFor(event)
if (opener) {
openInNewTab(opener)
}
})
// iOS Safari paints `:active` on an element that is not a link or a button only
// when some touch listener exists, and a row is neither — without this the press
// layer in list-item.css and card.css never shows on an iPhone.
document.addEventListener('touchstart', () => {}, { passive: true })