{
  "id": "SU02",
  "title": "Find in the page",
  "topic": "search",
  "use": "A search that finds a place in the page rather than a record: it marks every match, steps through them with Enter, and Tab goes into the field it stands on. Use it on a page with more fields than the user can scan, such as the long form (F02).",
  "decisions": [
    "Typing marks every match and scrolls to the first, and the focus stays in the search field: moving the focus while the user types would take the field from under their fingers.",
    "Enter steps to the next match and Shift+Enter to the previous, as a browser's or an editor's find does; at either end the key stays where it is, where the arrow buttons stop too.",
    "Tab goes into the field of the current match, as the next Tab after a browser's find lands behind what it found. Without a match Tab moves on as usual.",
    "The marks stay while the user is in the field, so they can go back to the search and step on.",
    "Every find is marked in its label, and the label the search stands on is filled as a whole: the focus is in the search field, so no focus ring can say where the search is, and the current match differs in area, not only in tone.",
    "The count says 'Match n of m', and Previous and Next say why when they are disabled: 'No earlier match', 'No later match'.",
    "Escape empties the search field and takes the marks with it, as any field with a clear button does."
  ],
  "try": [
    {
      "do": "Type “number” into Find a field",
      "then": "every label with “number” shows the find marked, the label of Client number is filled as a whole, the count reads “Match 1 of 5”, and the focus stays in the search field"
    },
    {
      "do": "Press Enter, then Shift+Enter",
      "then": "the search moves to Tax number and back to Client number; at the first and at the last the key stays where it is"
    },
    {
      "do": "Step to Company number and press Tab",
      "then": "the cursor is in Company number, and the marks and the count stay"
    },
    {
      "do": "Click back into Find a field and press Enter",
      "then": "the search goes on from Company number to Membership number"
    },
    {
      "do": "Type something no label contains",
      "then": "the count reads “No matches”, and Tab moves on to Save as usual"
    },
    {
      "do": "Press Escape in Find a field",
      "then": "the field empties, and the marks and the count go away"
    }
  ],
  "revision": 1,
  "changes": [],
  "reviewed": false,
  "source": "<style>\n  .su02-toolbar {\n    position: sticky;\n    inset-block-start: 0;\n    z-index: 4;\n    background: var(--ed-color-surface-default);\n  }\n\n  /* The sticky toolbar covers the top of the page; a heading scrolled to lands below it. */\n  .su02-form h2 {\n    scroll-margin-block-start: var(--ed-space-16);\n  }\n\n  /* Every find on the warning fill, the label the search stands on on the light one as a whole. The\n     shadow widens that fill without moving the label's text. */\n  .su02-form mark {\n    background-color: var(--ed-color-warning-fill-normal);\n    color: var(--ed-color-warning-on-normal);\n  }\n\n  .su02-current {\n    border-radius: var(--ed-border-radius-1x);\n    background-color: var(--ed-color-warning-fill-light);\n    box-shadow: 0 0 0 0.2em var(--ed-color-warning-fill-light);\n  }\n\n  /* Forced colors replace both fills with the canvas; the system's own mark colors and an outline\n     keep the finds and the current label apart. */\n  @media (forced-colors: active) {\n    .su02-form mark {\n      forced-color-adjust: none;\n      background-color: Mark;\n      color: MarkText;\n    }\n\n    .su02-current {\n      outline: 2px solid Highlight;\n    }\n  }\n</style>\n\n<div class=\"ed-stack ed-gap-2\">\n  <hgroup class=\"ed-stack ed-gap-0 ed-pb-4\">\n    <h1>Payroll settings</h1>\n    <p class=\"ed-caption-m\">Client 10482 · Bergmann &amp; Söhne Fahrzeugtechnik GmbH &amp; Co. KG</p>\n  </hgroup>\n\n  <div class=\"su02-toolbar ed-py-2\">\n    <ed-toolbar label=\"Payroll settings\">\n      <search slot=\"search\" class=\"ed-cluster ed-gap-2\">\n        <ed-input id=\"su02-search\" type=\"search\" with-clear placeholder=\"Find a field\" aria-label=\"Find a field\">\n          <ed-icon slot=\"start\" name=\"search\"></ed-icon>\n        </ed-input>\n        <div id=\"su02-hits\" class=\"ed-cluster ed-gap-2\" hidden>\n          <ed-button id=\"su02-previous\" appearance=\"plain\" aria-label=\"Previous match\" disabled>\n            <ed-icon name=\"previous\"></ed-icon>\n          </ed-button>\n          <ed-button id=\"su02-next\" appearance=\"plain\" aria-label=\"Next match\" disabled>\n            <ed-icon name=\"next\"></ed-icon>\n          </ed-button>\n        </div>\n        <span id=\"su02-count\" role=\"status\"></span>\n        <ed-tooltip for=\"su02-previous\">Previous match</ed-tooltip>\n        <ed-tooltip for=\"su02-next\">Next match</ed-tooltip>\n      </search>\n      <ed-button><ed-icon slot=\"start\" name=\"check\"></ed-icon>Save</ed-button>\n    </ed-toolbar>\n  </div>\n\n  <form id=\"su02-form\" class=\"su02-form ed-stack ed-gap-12\" style=\"max-inline-size: 64rem\">\n    <section class=\"ed-stack ed-gap-4\">\n      <h2>Client</h2>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6 ed-span-md-3\" name=\"client-number\" label=\"Client number\" value=\"10482\" readonly></ed-input>\n        <ed-input class=\"ed-span-sm-6 ed-span-md-9\" name=\"client-name\" label=\"Client name\" value=\"Bergmann &amp; Söhne Fahrzeugtechnik GmbH &amp; Co. KG\"></ed-input>\n      </div>\n      <div class=\"ed-grid\">\n        <ed-select class=\"ed-span-sm-6\" name=\"legal-form\" label=\"Legal form\" value=\"kg\">\n          <ed-option value=\"gmbh\">GmbH</ed-option>\n          <ed-option value=\"kg\">GmbH &amp; Co. KG</ed-option>\n          <ed-option value=\"ag\">AG</ed-option>\n        </ed-select>\n        <ed-input class=\"ed-span-sm-6\" name=\"industry\" label=\"Industry\" value=\"Vehicle engineering\"></ed-input>\n      </div>\n    </section>\n\n    <section class=\"ed-stack ed-gap-4\">\n      <h2>Tax</h2>\n      <div class=\"ed-grid\">\n        <ed-select class=\"ed-span-sm-6\" name=\"tax-office\" label=\"Tax office\" value=\"neunkirchen\">\n          <ed-option value=\"neunkirchen\">Neunkirchen</ed-option>\n          <ed-option value=\"saarbruecken\">Saarbrücken</ed-option>\n        </ed-select>\n        <ed-input class=\"ed-span-sm-6\" name=\"tax-number\" label=\"Tax number\" value=\"040/123/45678\"></ed-input>\n      </div>\n      <div class=\"ed-grid\">\n        <ed-select class=\"ed-span-sm-6\" name=\"filing-period\" label=\"Payroll tax filing period\" value=\"monthly\">\n          <ed-option value=\"monthly\">Monthly</ed-option>\n          <ed-option value=\"quarterly\">Quarterly</ed-option>\n          <ed-option value=\"annually\">Annually</ed-option>\n        </ed-select>\n      </div>\n      <ed-switch name=\"flat-rate-church-tax\">Flat-rate church tax</ed-switch>\n    </section>\n\n    <section class=\"ed-stack ed-gap-4\">\n      <h2>Social insurance</h2>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6\" name=\"company-number\" label=\"Company number\" value=\"12345678\"></ed-input>\n        <ed-input class=\"ed-span-sm-6\" name=\"accident-insurer\" label=\"Accident insurance carrier\" value=\"BGHM\"></ed-input>\n      </div>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6\" name=\"membership-number\" label=\"Membership number\" value=\"417 0815\"></ed-input>\n        <ed-select class=\"ed-span-sm-6\" name=\"contribution-statement\" label=\"Contribution statement via\" value=\"sv-net\">\n          <ed-option value=\"sv-net\">sv.net</ed-option>\n          <ed-option value=\"payroll\">The payroll software</ed-option>\n        </ed-select>\n      </div>\n      <ed-switch name=\"u1-levy\" checked>Take part in the U1 levy</ed-switch>\n    </section>\n\n    <section class=\"ed-stack ed-gap-4\">\n      <h2>Payroll run</h2>\n      <div class=\"ed-grid\">\n        <ed-select class=\"ed-span-sm-6\" name=\"payroll-period\" label=\"Payroll period\" value=\"calendar-month\">\n          <ed-option value=\"calendar-month\">Calendar month</ed-option>\n          <ed-option value=\"four-weeks\">Four weeks</ed-option>\n        </ed-select>\n        <ed-date-picker class=\"ed-span-sm-6\" name=\"first-run\" label=\"First run\" value=\"2026-11-30\"></ed-date-picker>\n      </div>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6\" name=\"default-cost-center\" label=\"Default cost center\" value=\"4700\"></ed-input>\n      </div>\n      <ed-switch name=\"payslips-by-email\" checked>Send payslips by email</ed-switch>\n    </section>\n\n    <section class=\"ed-stack ed-gap-4\">\n      <h2>Bank details</h2>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6\" name=\"account-holder\" label=\"Account holder\" value=\"Bergmann &amp; Söhne Fahrzeugtechnik GmbH &amp; Co. KG\"></ed-input>\n        <ed-input class=\"ed-span-sm-6\" name=\"bank-name\" label=\"Bank\" value=\"Kreissparkasse Neunkirchen\"></ed-input>\n      </div>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6 ed-span-md-8\" name=\"iban\" label=\"IBAN\" value=\"DE02 5925 2046 0000 1234 56\"></ed-input>\n        <ed-input class=\"ed-span-sm-6 ed-span-md-4\" name=\"bic\" label=\"BIC\" value=\"SALADE51NKS\"></ed-input>\n      </div>\n      <ed-switch name=\"direct-debit\">Collect contributions by direct debit</ed-switch>\n    </section>\n\n    <section class=\"ed-stack ed-gap-4\">\n      <h2>Contact</h2>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6\" name=\"contact-person\" label=\"Contact person\" value=\"Hans-Jürgen Odenthal\"></ed-input>\n        <ed-input class=\"ed-span-sm-6\" name=\"phone\" label=\"Phone number\" type=\"tel\" value=\"06821 4711\"></ed-input>\n      </div>\n      <div class=\"ed-grid\">\n        <ed-input class=\"ed-span-sm-6\" name=\"email\" label=\"Email\" type=\"email\" value=\"lohn@bergmann-soehne.example\"></ed-input>\n      </div>\n      <ed-textarea name=\"note\" label=\"Note\" rows=\"3\"></ed-textarea>\n    </section>\n  </form>\n</div>\n\n<script type=\"module\">\n  const form = document.getElementById('su02-form');\n  const search = document.getElementById('su02-search');\n  const hits = document.getElementById('su02-hits');\n  const count = document.getElementById('su02-count');\n  const previous = document.getElementById('su02-previous');\n  const next = document.getElementById('su02-next');\n  const previousTip = document.querySelector('ed-tooltip[for=\"su02-previous\"]');\n  const nextTip = document.querySelector('ed-tooltip[for=\"su02-next\"]');\n\n  // The search runs over the labels. A label moves from its attribute into the label slot, where a\n  // mark can sit in it; a checkbox and a switch carry their label as content already.\n  const entries = [...form.querySelectorAll('[label], ed-checkbox, ed-switch')].map(field => {\n    const text = (field.getAttribute('label') ?? field.textContent).trim();\n    const holder = document.createElement('span');\n    holder.textContent = text;\n    if (field.hasAttribute('label')) {\n      holder.slot = 'label';\n      field.removeAttribute('label');\n      field.prepend(holder);\n    } else {\n      field.replaceChildren(holder);\n    }\n    return { field, text, holder, found: false };\n  });\n\n  let matches = [];\n  let current = -1;\n\n  const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n  const motion = () => (reducedMotion.matches ? 'auto' : 'smooth');\n\n  // scrollIntoView() scrolls every scrollable ancestor, across a frame's border too: a page standing\n  // in an iframe would move the page that holds it. The jump does what it does inside this document\n  // only — every box that scrolls the field centers it, through the shadow trees it is slotted into,\n  // the innermost first and at once, the page last — and leaves a field that is not rendered alone.\n  function reveal(field) {\n    if (!field.checkVisibility()) return;\n    const page = document.scrollingElement;\n    const scrollers = [];\n    for (let node = field; node; ) {\n      const next = node.assignedSlot ?? node.parentNode;\n      node = next instanceof ShadowRoot ? next.host : next instanceof Element ? next : null;\n      if (!node || node === document.documentElement || node === document.body) continue;\n      if (/auto|scroll/.test(getComputedStyle(node).overflowY) && node.scrollHeight > node.clientHeight) scrollers.push(node);\n    }\n    scrollers.push(page);\n    scrollers.forEach((scroller, index) => {\n      // The scrollport, less its scroll-padding: a sticky bar the product sets it for stays clear.\n      const style = getComputedStyle(scroller);\n      const padTop = parseFloat(style.scrollPaddingTop) || 0;\n      const padBottom = parseFloat(style.scrollPaddingBottom) || 0;\n      const top = scroller === page ? 0 : scroller.getBoundingClientRect().top + scroller.clientTop;\n      const height = scroller === page ? document.documentElement.clientHeight : scroller.clientHeight;\n      const box = field.getBoundingClientRect();\n      const offset = box.top - top - padTop - (height - padTop - padBottom - box.height) / 2;\n      scroller.scrollBy({ top: offset, behavior: index === scrollers.length - 1 ? motion() : 'instant' });\n    });\n  }\n\n  function paint(entry, term) {\n    const at = term ? entry.text.toLowerCase().indexOf(term) : -1;\n    entry.found = at >= 0;\n    if (!entry.found) {\n      entry.holder.textContent = entry.text;\n      return;\n    }\n    const mark = Object.assign(document.createElement('mark'), { textContent: entry.text.slice(at, at + term.length) });\n    entry.holder.replaceChildren(entry.text.slice(0, at), mark, entry.text.slice(at + term.length));\n  }\n\n  function go(index) {\n    matches[current]?.holder.classList.remove('su02-current');\n    current = index;\n    const focused = [previous, next].find(button => button === document.activeElement);\n    previous.disabled = current <= 0;\n    next.disabled = current < 0 || current >= matches.length - 1;\n    // A disabled button can be pointed at but not focused, so what it shows on hover says why it is\n    // unavailable. Its aria-label stays its name.\n    previousTip.textContent = previous.disabled ? 'No earlier match' : 'Previous match';\n    nextTip.textContent = next.disabled ? 'No later match' : 'Next match';\n    // A button disabled while focused would drop the focus to the page; its partner takes it, or the\n    // search field when both are disabled.\n    if (focused?.disabled) {\n      const partner = focused === next ? previous : next;\n      (partner.disabled ? search : partner).focus();\n    }\n    if (!search.value.trim()) count.textContent = '';\n    else count.textContent = matches.length ? `Match ${current + 1} of ${matches.length}` : 'No matches';\n    if (current < 0) return;\n    matches[current].holder.classList.add('su02-current');\n    reveal(matches[current].field);\n  }\n\n  // The first match is jumped to as soon as there is one: \"1 of 5\" with the field out of view would\n  // say where it is without showing it.\n  function find() {\n    const term = search.value.trim().toLowerCase();\n    // The fill sits on the label, which outlives the search; the old current one loses it here.\n    matches[current]?.holder.classList.remove('su02-current');\n    for (const entry of entries) paint(entry, term);\n    matches = entries.filter(entry => entry.found);\n    hits.hidden = !term;\n    current = -1;\n    go(matches.length ? 0 : -1);\n  }\n\n  search.addEventListener('input', find);\n  search.addEventListener('ed-clear', find);\n\n  search.addEventListener('keydown', event => {\n    // Safari ends the composition before the Enter that confirms it, and reports that press as 229.\n    if (event.isComposing || event.keyCode === 229) return;\n    // Enter steps and submits nothing; at either end it stays, where the arrows stop too.\n    if (event.key === 'Enter') {\n      event.preventDefault();\n      if (event.shiftKey) {\n        if (current > 0) go(current - 1);\n      } else if (current < matches.length - 1) {\n        go(current + 1);\n      }\n      return;\n    }\n    // Tab goes into the field the search stands on, as the next Tab after a browser's find lands\n    // behind its match. A field that cannot take the focus, disabled or hidden, leaves Tab its own way.\n    if (event.key === 'Tab' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && current >= 0) {\n      const { field } = matches[current];\n      field.focus({ preventScroll: true });\n      if (!field.matches(':focus-within')) return;\n      event.preventDefault();\n      reveal(field);\n    }\n  });\n\n  previous.addEventListener('click', () => current > 0 && go(current - 1));\n  next.addEventListener('click', () => current < matches.length - 1 && go(current + 1));\n</script>",
  "page": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/search/su02-find-in-the-page/",
  "topicPage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/search/"
}
