{
  "id": "BE02",
  "title": "Bulk edit",
  "topic": "bulk-editing",
  "use": "Select several records and set one field for all of them — the most common case of bulk editing. A hundred documents sit in the inbox, the system has classified sixty of them, and the rest need a document type. Whoever wants to give each record a value of its own walks through the selection instead — BE03.",
  "decisions": [
    "The selection stands in the dialog, not only its count: the dialog covers the list, and a bare count makes the user guess whether the right records are in it. A combobox above the fields shows them as badges, lets the user search in them and take single ones out.",
    "Three states per field, one control: the first entry leaves the field unchanged, a value sets it, and where clearing makes sense it is an entry of its own, worded in the domain. No field is ever locked.",
    "An empty field never means clear: otherwise every dialog that is opened and closed again deletes data. Clearing is a named action.",
    "A target value after which the records accept no further changes says so beforehand, in the lines above the footer where the decision is made, not in a question afterwards.",
    "Differing values are named, not guessed, and quietly: the hint under the field says so, and each option counts how many of the selection have it. A placeholder would say 'nothing here', not 'several here'.",
    "What does not go along is named: the records that cannot be changed stand as badges, up to eight and then 'and N more', with the choice to take them out of the selection or accept them. A count allows neither.",
    "The lines above the footer say what the change really does — per field the target value and how many records it reaches, and how many stay as they are. Without an undo this is the only brake."
  ],
  "try": [
    {
      "do": "Select a few documents and choose Edit documents",
      "then": "the dialog's title counts them, and the combobox above the fields shows the documents one by one"
    },
    {
      "do": "Open a field whose values differ",
      "then": "the hint under the field says several values, and each option shows how many documents have it"
    },
    {
      "do": "Choose Not assigned for Clerk",
      "then": "the value is cleared, and that is an entry of its own, not the first one in the list"
    },
    {
      "do": "Choose Close without changes",
      "then": "a question asks before the values set are discarded, and Discard entries closes the dialog"
    },
    {
      "do": "Select finalized documents as well",
      "then": "the dialog names them above the selection and offers to take them out"
    },
    {
      "do": "Look at the lines above the footer",
      "then": "they say how many records the change really reaches"
    },
    {
      "do": "Run the change with finalized documents",
      "then": "the dialog stays open and shows the failed ones with their reason, to copy; they stay selected, and the change is not offered again for them"
    },
    {
      "do": "Set Status to Finalized",
      "then": "the lines above the footer say beforehand that the documents accept no further changes afterwards — and so it is, a second run leaves them out"
    }
  ],
  "revision": 3,
  "changes": [
    {
      "revision": 3,
      "change": "The distribution of a field's values stands in the field's hint, not in its label or a tooltip."
    },
    {
      "revision": 2,
      "change": "Why a toolbar action is disabled, and the distribution of a field's values in the dialog, are ed-tooltips, not titles; a reason shows only while its action is disabled."
    }
  ],
  "reviewed": true,
  "source": "<!-- Demo only, not part of the pattern. The table takes the height the screen leaves it; here it is set to fit the frame. -->\n\n<ed-data-grid id=\"be02-grid\" label=\"Incoming documents\" selectable selection-mode=\"multiple\" with-search data-ed-preload=\"ed-format-date\">\n  <ed-button slot=\"toolbar-actions\" id=\"be02-edit\" data-needs-selection disabled>\n    <ed-icon slot=\"start\" name=\"edit\"></ed-icon>\n    Edit documents\n  </ed-button>\n  <ed-button slot=\"toolbar-actions\" id=\"be02-export\" appearance=\"plain\" data-needs-selection disabled>\n    <ed-icon slot=\"start\" name=\"export\"></ed-icon>\n    Export\n  </ed-button>\n  <ed-button slot=\"toolbar-actions\" id=\"be02-print\" appearance=\"plain\" data-needs-selection disabled>\n    <ed-icon slot=\"start\" name=\"print\"></ed-icon>\n    Print\n  </ed-button>\n  <ed-button slot=\"toolbar-actions\" id=\"be02-finalize\" appearance=\"plain\" disabled>\n    <ed-icon slot=\"start\" name=\"lock\"></ed-icon>\n    Finalize\n  </ed-button>\n  <ed-button slot=\"toolbar-actions\" id=\"be02-reversal\" appearance=\"plain\" disabled>\n    <ed-icon slot=\"start\" name=\"undo\"></ed-icon>\n    Post reversal\n  </ed-button>\n  <ed-divider slot=\"toolbar-actions\" orientation=\"vertical\"></ed-divider>\n  <ed-button slot=\"toolbar-actions\" id=\"be02-delete\" appearance=\"plain\" data-needs-selection disabled>\n    <ed-icon slot=\"start\" name=\"delete\"></ed-icon>\n    Delete documents\n  </ed-button>\n  <ed-radio-group slot=\"toolbar-view\" id=\"be02-view\" appearance=\"button\" aria-label=\"View\" value=\"all\">\n    <ed-radio value=\"all\">All</ed-radio>\n    <ed-radio value=\"open\">Open</ed-radio>\n    <ed-radio value=\"finalized\">Finalized</ed-radio>\n  </ed-radio-group>\n</ed-data-grid>\n<ed-tooltip for=\"be02-edit\">Select at least one document.</ed-tooltip>\n<ed-tooltip for=\"be02-export\">Select the documents to export.</ed-tooltip>\n<ed-tooltip for=\"be02-print\">Select the documents to print.</ed-tooltip>\n<ed-tooltip for=\"be02-finalize\">Only the accounting firm releases finalizing. Ask your clerk.</ed-tooltip>\n<ed-tooltip for=\"be02-reversal\">Only finalized documents can be reversed. This selection is open.</ed-tooltip>\n<ed-tooltip for=\"be02-delete\">Select the documents to delete.</ed-tooltip>\n\n<ed-dialog id=\"be02-dialog\" light-dismiss label=\"Edit documents\" style=\"--ed-dialog-width: 48rem\">\n  <div class=\"ed-stack ed-gap-6\">\n    <section class=\"ed-stack ed-gap-4\">\n      <h2 class=\"ed-heading-s\">Selection</h2>\n      <!-- Silent: it changes with the selection, and the lines above the footer announce that change. -->\n      <ed-notification id=\"be02-exceptions\" variant=\"warning\" without-live-region hidden>\n        <span id=\"be02-exceptions-title\"></span>\n        <span slot=\"description\" id=\"be02-exceptions-list\" class=\"ed-cluster ed-gap-1\"></span>\n        <ed-button slot=\"action\" id=\"be02-exclude\" appearance=\"plain\" density=\"compact\"></ed-button>\n      </ed-notification>\n      <ed-combobox id=\"be02-selection\" label=\"Documents\" multiple max-options-visible=\"12\" placeholder=\"Search for a document\" style=\"--ed-combobox-tag-max-size: 28ch\"></ed-combobox>\n      <p id=\"be02-empty\" hidden>No document is selected. Select at least one document in the list.</p>\n    </section>\n\n    <section class=\"ed-stack ed-gap-4\">\n      <h2 class=\"ed-heading-s\">Changes</h2>\n      <div id=\"be02-fields\" class=\"ed-grid ed-gap-4\" style=\"--ed-grid-min-column-size: 18rem\">\n        <ed-select id=\"be02-type\" name=\"type\" label=\"Document type\" value=\"unchanged\" autofocus>\n          <ed-option value=\"unchanged\">Leave unchanged</ed-option>\n          <ed-option value=\"outgoing\">Outgoing invoice</ed-option>\n          <ed-option value=\"incoming\">Incoming invoice</ed-option>\n          <ed-option value=\"cash\">Cash receipt</ed-option>\n          <ed-option value=\"reclassification\">Reclassification</ed-option>\n        </ed-select>\n        <ed-select id=\"be02-clerk\" name=\"clerk\" label=\"Clerk\" value=\"unchanged\">\n          <ed-option value=\"unchanged\">Leave unchanged</ed-option>\n          <ed-option value=\"none\">Not assigned</ed-option>\n          <ed-option value=\"SB\">Sophie Brandt</ed-option>\n          <ed-option value=\"TR\">Thorsten Reinke</ed-option>\n          <ed-option value=\"AJ\">Andrea Jansen</ed-option>\n          <ed-option value=\"MH\">Marie-Luise Hoffmann-Grote</ed-option>\n        </ed-select>\n        <ed-combobox id=\"be02-cost-center\" name=\"costCenter\" label=\"Cost center\" allow-custom-value value=\"unchanged\">\n          <ed-option value=\"unchanged\">Leave unchanged</ed-option>\n          <ed-option value=\"none\">No cost center</ed-option>\n          <ed-option value=\"4700\">4700 Administration</ed-option>\n          <ed-option value=\"4711\">4711 Workshop</ed-option>\n          <ed-option value=\"4712\">4712 Vehicle fleet</ed-option>\n          <ed-option value=\"4713\">4713 Travel</ed-option>\n        </ed-combobox>\n        <ed-select id=\"be02-status\" name=\"status\" label=\"Status\" value=\"unchanged\">\n          <ed-option value=\"unchanged\">Leave unchanged</ed-option>\n          <ed-option value=\"open\">Open</ed-option>\n          <ed-option value=\"checked\">Checked</ed-option>\n          <ed-option value=\"finalized\">Finalized</ed-option>\n        </ed-select>\n      </div>\n    </section>\n\n    <div id=\"be02-run\" role=\"status\" hidden>\n      <ed-progress-bar id=\"be02-progress\"></ed-progress-bar>\n    </div>\n\n    <!-- One region for the result and the lines below it: the end of a run changes both at once. -->\n    <div class=\"ed-stack ed-gap-6\" role=\"status\">\n      <ed-notification id=\"be02-result\" variant=\"warning\" without-live-region hidden>\n        <span id=\"be02-result-title\"></span>\n        <ol slot=\"description\" id=\"be02-failed\"></ol>\n        <ed-copy-button slot=\"action\" id=\"be02-copy\" copy-label=\"Copy list\"></ed-copy-button>\n      </ed-notification>\n\n      <div class=\"ed-stack ed-gap-3\">\n        <ed-divider></ed-divider>\n        <div class=\"ed-stack ed-gap-1\">\n          <div id=\"be02-effect-lines\" class=\"ed-stack ed-gap-1\"></div>\n          <p id=\"be02-effect-rest\" class=\"ed-caption-s\"></p>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <ed-button slot=\"footer\" id=\"be02-apply\" disabled>Change documents</ed-button>\n  <ed-button slot=\"footer\" appearance=\"outlined\" data-dialog=\"close\">Close without changes</ed-button>\n</ed-dialog>\n\n<ed-alert-dialog id=\"be02-discard\" light-dismiss type=\"warning\" label=\"Discard your entries?\" confirm-text=\"Discard entries\" cancel-text=\"Keep editing\">\n  The values you set are not applied to the selected documents.\n</ed-alert-dialog>\n\n<ed-toast-stack id=\"be02-toasts\"></ed-toast-stack>\n\n<!-- Demo only, not part of the pattern. The product supplies documents, the inbox as { id, date, text, type, costCenter, clerk, status } with the date as YYYY-MM-DD and type, clerk and status as codes; clerks, the names behind the clerk codes; and changeDocument(id, change), which stores change, as [field, value] pairs, on one document and resolves once it is stored. What the screen does when storing fails is not part of this pattern. -->\n\n<script type=\"module\">\n  const types = { outgoing: 'Outgoing invoice', incoming: 'Incoming invoice', cash: 'Cash receipt', reclassification: 'Reclassification', bank: 'Bank document' };\n  const statuses = { open: ['Open', 'neutral'], checked: ['Checked', 'info'], finalized: ['Finalized', 'success'], error: ['Error', 'danger'] };\n  // How a stored value reads in a label, a tooltip and a line above the footer.\n  const shown = {\n    type: value => types[value] || value || '—',\n    clerk: value => clerks[value] || value || '—',\n    costCenter: value => value || '—',\n    status: value => statuses[value]?.[0] || value || '—'\n  };\n  const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;\n  const element = (tag, props) => Object.assign(document.createElement(tag), props);\n  const day = value => element('ed-format-date', { date: value, month: 'short', day: 'numeric', year: 'numeric' });\n\n  const grid = document.getElementById('be02-grid');\n  const view = document.getElementById('be02-view');\n  const edit = document.getElementById('be02-edit');\n  const needsSelection = [...document.querySelectorAll('[data-needs-selection]')];\n  const tooltipOf = element => document.querySelector(`ed-tooltip[for=\"${element.id}\"]`);\n  const dialog = document.getElementById('be02-dialog');\n  const exceptionsNote = document.getElementById('be02-exceptions');\n  const exceptionsTitle = document.getElementById('be02-exceptions-title');\n  const exceptionsList = document.getElementById('be02-exceptions-list');\n  const exclude = document.getElementById('be02-exclude');\n  const selection = document.getElementById('be02-selection');\n  const empty = document.getElementById('be02-empty');\n  const fields = [...document.querySelectorAll('#be02-fields [name]')];\n  const labels = new Map(fields.map(field => [field, field.getAttribute('label')]));\n  const statusField = document.querySelector('#be02-fields [name=\"status\"]');\n  const runRegion = document.getElementById('be02-run');\n  const progress = document.getElementById('be02-progress');\n  const result = document.getElementById('be02-result');\n  const resultTitle = document.getElementById('be02-result-title');\n  const failedList = document.getElementById('be02-failed');\n  const copy = document.getElementById('be02-copy');\n  const effectLines = document.getElementById('be02-effect-lines');\n  const effectRest = document.getElementById('be02-effect-rest');\n  const apply = document.getElementById('be02-apply');\n  const toasts = document.getElementById('be02-toasts');\n\n  grid.columns = [\n    { field: 'id', label: 'Document', rowheader: true, pinned: 'left', width: 150 },\n    { field: 'date', label: 'Date', width: 120, sortFn: 'datetime', formatter: day },\n    { field: 'text', label: 'Posting text', flex: 2, minWidth: 224 },\n    { id: 'type', label: 'Document type', width: 176, value: doc => types[doc.type] ?? '' },\n    { field: 'costCenter', label: 'Cost center', width: 128 },\n    { id: 'clerk', label: 'Clerk', width: 220, value: doc => clerks[doc.clerk] ?? '' },\n    {\n      field: 'status',\n      label: 'Status',\n      width: 140,\n      pinned: 'right',\n      formatter: value => element('ed-badge', { variant: statuses[value][1], textContent: statuses[value][0] })\n    }\n  ];\n\n  // Read before the radio group is defined as well, so the attribute stands in for the property.\n  const visible = () => {\n    const current = view.value ?? view.getAttribute('value');\n    return documents.filter(doc => current === 'all' || (current === 'finalized') === (doc.status === 'finalized'));\n  };\n  grid.data = visible();\n\n  let selected = [];\n  let initialCount = 0;\n  let running = false;\n  // Each opening of the dialog counts up, so a run that outlasts its dialog leaves the next one alone.\n  let session = 0;\n  const discard = document.getElementById('be02-discard');\n  // Set once closing is meant to lose nothing, or the user has agreed to discard the entries.\n  let allowClose = false;\n  const entered = () => !running && result.hidden && fields.some(field => field.value !== 'unchanged');\n\n  const selectedDocs = () => documents.filter(doc => selected.includes(doc.id));\n  const accepts = doc => doc.status !== 'finalized';\n  // A field's first entry leaves it unchanged; \"none\" is the named entry that clears it.\n  const isSet = field => Boolean(field.value) && field.value !== 'unchanged';\n  const target = field => (field.value === 'none' ? '' : field.value);\n\n  function setSelection(ids) {\n    selected = ids;\n    grid.selectedKeys = ids;\n    for (const button of needsSelection) {\n      button.disabled = ids.length === 0;\n      tooltipOf(button).disabled = ids.length > 0;\n    }\n  }\n\n  function fillSelection() {\n    selection.replaceChildren(\n      ...selectedDocs().map(doc => {\n        const option = element('ed-option', { textContent: `${doc.id} · ${doc.text}` });\n        option.setAttribute('value', doc.id);\n        return option;\n      })\n    );\n    selection.value = [...selected];\n  }\n\n  function selectionLabel() {\n    selection.label =\n      selected.length === initialCount ? `Documents · ${selected.length}` : `Documents · ${selected.length} of ${initialCount}`;\n  }\n\n  // The hint under a field names the distribution of its values in the selection, and each option counts its own value.\n  function showDistribution() {\n    for (const field of fields) {\n      const counts = new Map();\n      for (const doc of selectedDocs().filter(accepts)) counts.set(doc[field.name], (counts.get(doc[field.name]) ?? 0) + 1);\n      const pairs = [...counts].sort((a, b) => b[1] - a[1]);\n      const show = shown[field.name];\n      if (pairs.length === 0) {\n        field.hint = '';\n      } else if (pairs.length === 1) {\n        field.hint = `All ${pairs[0][1]}: ${show(pairs[0][0])}`;\n      } else {\n        const rest = pairs.length - 3;\n        field.hint = `Several values — ${pairs.slice(0, 3).map(([value, n]) => `${n} × ${show(value)}`).join(', ')}${rest > 0 ? ` and ${rest} more` : ''}`;\n      }\n      for (const option of field.querySelectorAll('ed-option:not([value=\"unchanged\"])')) {\n        option.querySelector('[slot=\"end\"]')?.remove();\n        const n = counts.get(option.value === 'none' ? '' : option.value);\n        if (n) option.append(element('span', { slot: 'end', className: 'ed-caption-s ed-text-tabular', textContent: `${n} in the selection` }));\n      }\n    }\n  }\n\n  function setEffect(lines, rest) {\n    effectLines.replaceChildren(...lines);\n    effectRest.replaceChildren(...rest.flatMap((sentence, index) => (index ? [element('br'), sentence] : [sentence])));\n  }\n\n  function updateEffect() {\n    const set = fields.filter(isSet);\n    const candidates = selectedDocs().filter(accepts);\n    const left = selectedDocs().filter(doc => !accepts(doc));\n    apply.disabled = true;\n    apply.textContent = 'Change documents';\n\n    if (!selected.length) return setEffect([], ['No document is selected.']);\n    if (!set.length) return setEffect([], ['No field is set.']);\n    if (!candidates.length) {\n      return setEffect([], [`All ${count(left.length, 'selected document')} are finalized. They cannot be changed.`]);\n    }\n\n    const lines = set.map(field => {\n      const n = candidates.filter(doc => doc[field.name] !== target(field)).length;\n      const line = element('p', {\n        className: 'ed-body-s',\n        textContent: field.value === 'none'\n          ? `${labels.get(field)} is cleared — `\n          : `${labels.get(field)} is set to ${shown[field.name](target(field))} — `\n      });\n      line.append(element('span', { className: 'ed-text-tabular', textContent: n }), n === 1 ? ' document' : ' documents');\n      return line;\n    });\n\n    // Everyone the click really moves: the union over all set fields, the number on the button.\n    const affected = candidates.filter(doc => set.some(field => doc[field.name] !== target(field)));\n    const same = candidates.length - affected.length;\n    const rest = [];\n    if (same) rest.push(same === 1 ? '1 document stays unchanged, its value is already set.' : `${same} documents stay unchanged, their values are already set.`);\n    if (left.length) rest.push(left.length === 1 ? '1 document is finalized and is left out.' : `${left.length} documents are finalized and are left out.`);\n    if (statusField.value === 'finalized') rest.push('Afterwards these documents are finalized. They accept no further changes.');\n    setEffect(lines, rest);\n\n    apply.disabled = affected.length === 0;\n    apply.textContent = `Change ${count(affected.length, 'document')}`;\n  }\n\n  function showSelection() {\n    dialog.label = selected.length ? `Edit ${count(selected.length, 'document')}` : 'Edit documents';\n    selectionLabel();\n    showDistribution();\n  }\n\n  function updateSelection() {\n    const left = selectedDocs().filter(doc => !accepts(doc));\n    exceptionsNote.hidden = !left.length;\n    exceptionsNote.open = left.length > 0;\n    exceptionsTitle.textContent = left.length === 1 ? '1 document is finalized' : `${left.length} documents are finalized`;\n    exceptionsList.replaceChildren(\n      ...left.slice(0, 8).map(doc => element('ed-badge', { variant: 'warning', textContent: doc.id })),\n      ...(left.length > 8 ? [element('span', { className: 'ed-caption-s', textContent: `and ${left.length - 8} more` })] : [])\n    );\n    exclude.textContent = `Remove ${count(left.length, 'document')} from the selection`;\n    empty.hidden = selected.length > 0;\n    showSelection();\n    updateEffect();\n  }\n\n  async function run() {\n    running = true;\n    const own = session;\n    const change = fields.filter(isSet).map(field => [field.name, target(field)]);\n    const candidates = selectedDocs().filter(accepts);\n    const left = selectedDocs().filter(doc => !accepts(doc));\n    let changed = 0;\n\n    apply.loading = true;\n    runRegion.hidden = false;\n    exceptionsNote.hidden = true;\n    exceptionsNote.open = false;\n    for (const [index, doc] of candidates.entries()) {\n      if (change.some(([key, value]) => doc[key] !== value)) changed += 1;\n      for (const [key, value] of change) doc[key] = value;\n      if (own === session) {\n        progress.value = ((index + 1) / candidates.length) * 100;\n        progress.label = `Changing documents — ${index + 1} of ${candidates.length}`;\n      }\n      await changeDocument(doc.id, change);\n    }\n    running = false;\n    apply.loading = false;\n    grid.data = visible();\n\n    // The documents of this run leave the selection, whatever it holds by now; the failed ones stay in it,\n    // so the dialog still shows whom the change did not reach.\n    const ran = new Set(candidates.map(doc => doc.id));\n    setSelection(selected.filter(id => !ran.has(id)));\n    if (own !== session) return;\n    runRegion.hidden = true;\n\n    if (!left.length) {\n      allowClose = true;\n      dialog.open = false;\n      toasts.create({ variant: 'success', title: `${count(changed, 'document')} changed` });\n      return;\n    }\n\n    const total = candidates.length + left.length;\n    result.hidden = false;\n    result.open = true;\n    resultTitle.textContent = `${changed} of ${count(total, 'document')} changed`;\n    failedList.replaceChildren(...left.map(doc => element('li', { textContent: `${doc.id} — finalized` })));\n    // Whoever repairs the list is often not who ran the change, so it carries when and how many.\n    copy.value = [\n      `Bulk edit ${new Date().toLocaleString('en-US')} · ${left.length} of ${total} not changed`,\n      ...left.map(doc => `${doc.id}\\tfinalized`)\n    ].join('\\n');\n    fillSelection();\n    showSelection();\n    // A finalized document fails again on every run, so the button offers no second one.\n    updateEffect();\n  }\n\n  grid.addEventListener('ed-data-grid-row-select', event => {\n    setSelection(event.detail.selectedKeys);\n  });\n\n  view.addEventListener('change', () => {\n    grid.data = visible();\n    grid.selectedKeys = selected;\n  });\n\n  edit.addEventListener('click', () => {\n    session += 1;\n    allowClose = false;\n    initialCount = selected.length;\n    for (const field of fields) field.value = 'unchanged';\n    result.hidden = true;\n    result.open = false;\n    runRegion.hidden = true;\n    progress.value = 0;\n    fillSelection();\n    updateSelection();\n    dialog.open = true;\n  });\n\n  // Taking a document out of the combobox takes it out of the selection.\n  selection.addEventListener('change', () => {\n    const keep = new Set([selection.value ?? []].flat());\n    const next = selected.filter(id => keep.has(id));\n    if (next.length === selected.length) return;\n    setSelection(next);\n    if (!selected.length) {\n      allowClose = true;\n      dialog.open = false;\n      return;\n    }\n    updateSelection();\n  });\n\n  for (const field of fields) {\n    field.addEventListener('change', () => {\n      selectionLabel();\n      showDistribution();\n      updateEffect();\n    });\n  }\n\n  // The exclusion keeps what was entered in the fields.\n  exclude.addEventListener('click', () => {\n    setSelection(selectedDocs().filter(accepts).map(doc => doc.id));\n    fillSelection();\n    updateSelection();\n    selection.focus();\n  });\n\n  apply.addEventListener('click', () => {\n    if (!running && !apply.disabled) run();\n  });\n\n  // Every way of closing — Close without changes, Escape, a click beside — asks once a value is set\n  // and nothing has run yet.\n  dialog.addEventListener('ed-hide', event => {\n    if (event.target !== dialog || allowClose || !entered()) return;\n    event.preventDefault();\n    discard.open = true;\n  });\n\n  discard.addEventListener('ed-alert-dialog-confirm', () => {\n    allowClose = true;\n  });\n\n  discard.addEventListener('ed-after-hide', () => {\n    if (allowClose) dialog.open = false;\n  });\n\n  // Escape clears the selection. With the dialog open it closes the dialog instead.\n  document.addEventListener('keydown', event => {\n    if (event.key !== 'Escape' || event.defaultPrevented || dialog.open || !selected.length) return;\n    event.preventDefault();\n    setSelection([]);\n  });\n</script>",
  "page": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/bulk-editing/be02-bulk-edit/",
  "topicPage": "https://test-apps.eurodata.de/a/eurodata-ui/docs/patterns/bulk-editing/"
}
