From f06e24de4496852f9426b77554f5d1a31306d567 Mon Sep 17 00:00:00 2001 From: carterleseman Date: Mon, 3 Mar 2025 23:24:01 -0600 Subject: [PATCH 1/2] add combo boxes --- docs/docs.css | 18 +++++++++ docs/index.html.ejs | 61 ++++++++++++++++++++++++++++ docs/script.js | 98 +++++++++++++++++++++++++++++++++++++++++++++ style.css | 93 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 270 insertions(+) create mode 100644 docs/script.js diff --git a/docs/docs.css b/docs/docs.css index a38ce5b..d747ff8 100644 --- a/docs/docs.css +++ b/docs/docs.css @@ -116,6 +116,24 @@ button.active { inset -2px -2px #dfdfdf, inset 2px 2px #808080; } +ul[role=listbox] > li.last-hovered, +.combo-box.key-nav > input[aria-haspopup] ~ ul > li[aria-selected=true] { + color: #fff; + background-color: #000080; +} + +.combo-box.key-nav > ul[role=listbox] > li.last-hovered, +.combo-box.key-nav > ul[role=listbox] > li:hover:not([aria-selected=true]) { + color: inherit; + background-color: inherit; +} + +.combo-box > input[aria-haspopup] ~ ul:has(.last-hovered) > li[aria-selected=true]:not(.last-hovered), +.combo-box:not(.key-nav) > input[aria-haspopup] ~ ul:has(.last-hovered) > li[aria-selected=true]:not(.last-hovered) { + color: inherit; + background-color: inherit; +} + @media (max-width: 480px) { aside { display: none; diff --git a/docs/index.html.ejs b/docs/index.html.ejs index 442af08..efffad4 100644 --- a/docs/index.html.ejs +++ b/docs/index.html.ejs @@ -31,6 +31,7 @@
  • GroupBox
  • TextBox
  • Slider
  • +
  • ComboBox
  • Dropdown
  • Window @@ -466,6 +467,65 @@ +
    +

    ComboBox

    +
    +
    + A combo box combines a text box with a list box. This allows the user to type an entry or choose one from the list. + +
    + — Microsoft Windows User Experience p. 183 +
    +
    + +

    + There are 2 ways you can render a combo box. The first is using a text input, a parent ul, and children li together, wrapped inside a container element with the combo-box class. For accessibility, follow the minimum requirements below: +

    + +
      +
    • Add a role="combobox" attribute to the text input
    • +
    • Add a role="listbox" attribute to the ul
    • +
    • Add a role="option" attribute to each li
    • +
    • + Specify the relationship between the list box and the text box by combining the id of the listbox with the aria-controls attribute on the text input +
    • +
    + + <%- example(` +
    + +
      +
    • Regular
    • +
    • Italic
    • +
    • Bold
    • +
    • Bold Italic
    • +
    +
    + `) %> + +

    + The second adds a button to toggle the visibility of the drop-down. For accessibility, follow these additional requirements: +

    + +
      +
    • Add aria-haspopup="listbox" and aria-expanded attributes to the text input
    • +
    + + <%- example(` +
    + + +
      +
    • h:mm:ss tt
    • +
    • hh:mm:ss tt
    • +
    • H:mm:ss
    • +
    • HH:mm:ss
    • +
    +
    + `) %> +
    +
    +
    @@ -1126,4 +1186,5 @@

    + diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..1de9e4d --- /dev/null +++ b/docs/script.js @@ -0,0 +1,98 @@ +// Combo Box +document.querySelectorAll('.combo-box').forEach(combobox => { + const input = combobox.querySelector('input'); + const listbox = combobox.querySelector('ul'); + const options = Array.from(listbox.querySelectorAll('li')); + let currentIndex = findIndex(); + let lastHovered = null; + + function findIndex() { + return options.findIndex(option => option.getAttribute('aria-selected') === "true"); + } + + function scroll(block) { + const index = currentIndex === -1 ? 0 : currentIndex; + options[index].scrollIntoView({ block, behavior: "instant"}); + } + + function updateSelected(value = null) { + if (value !== null) input.value = value; + options.forEach(option => { + option.setAttribute('aria-selected', option.textContent.trim() === input.value.trim() ? 'true' : 'false'); + }); + currentIndex = findIndex(); + if (currentIndex !== -1) scroll('nearest'); + } + + function removeHovered() { + lastHovered?.classList.remove('last-hovered'); + lastHovered = null; + } + + function keyNavigate() { + combobox.classList.add('key-nav'); + if (lastHovered) currentIndex = options.indexOf(lastHovered); + removeHovered(); + } + + function toggleDropdown() { + const isOpen = input.getAttribute('aria-expanded') === "true"; + input.setAttribute('aria-expanded', isOpen ? 'false' : 'true'); + input.focus(); + !isOpen ? (currentIndex = findIndex(), scroll('nearest')) : removeHovered(); + } + + function navigationHandler(event) { + if (event.altKey && (event.key === "ArrowDown" || event.key === "ArrowUp") && input.hasAttribute('aria-haspopup')) { + event.preventDefault(); + toggleDropdown(); + } else if ((event.key === "ArrowDown" && currentIndex < options.length - 1) || (event.key === "ArrowUp" && currentIndex > 0)) { + event.preventDefault(); + keyNavigate(); + currentIndex += event.key === "ArrowDown" ? 1 : -1; + updateSelected(options[currentIndex].textContent); + } else if (event.key === "Enter" && input.hasAttribute('aria-haspopup') && input.getAttribute('aria-expanded') === "true") { + event.preventDefault(); + updateSelected(lastHovered ? lastHovered.textContent : null); + toggleDropdown(); + } + } + + listbox.addEventListener('click', (event) => { + if (event.target.tagName === "LI") { + updateSelected(event.target.textContent); + if (input.hasAttribute('aria-haspopup')) toggleDropdown(); + } + }); + + listbox.addEventListener('mousedown', (event) => { + event.preventDefault(); + input.focus(); + }); + + listbox.addEventListener('mousemove', () => combobox.classList.remove('key-nav')); + + if (input.hasAttribute('aria-haspopup')) { + const button = combobox.querySelector('button'); + + listbox.addEventListener('mouseover', (event) => { + if (!combobox.classList.contains('key-nav') && event.target.tagName === "LI") { + removeHovered(); + lastHovered = event.target; + lastHovered.classList.add('last-hovered'); + } + }); + + button.addEventListener('click', () => toggleDropdown()); + input.addEventListener('blur', () => { + if (input.getAttribute('aria-expanded') === "true") { + setTimeout(() => { + if (document.activeElement !== input && document.activeElement !== button) toggleDropdown(); + }, 0); + } + }); + } + + input.addEventListener('input', () => updateSelected(input.value)); + input.addEventListener('keydown', navigationHandler); +}); \ No newline at end of file diff --git a/style.css b/style.css index 8c74873..ac3fbf0 100644 --- a/style.css +++ b/style.css @@ -112,6 +112,7 @@ textarea, select, option, table, +.combo-box, ul.tree-view, .window, .title-bar, @@ -690,6 +691,98 @@ select:active { background-image: svg-load("./icon/button-down-active.svg"); } +.combo-box { + display: inline-block; +} + +.combo-box > ul { + background: #fff; + border: 2px groove transparent; + border-image: svg-load('./icon/sunken-panel-border.svg') 2; + box-sizing: border-box; + overflow-y: scroll; + list-style: none; + padding: 0; + margin: 0; +} + +.combo-box > ul:focus-within { + outline: none; +} + +.combo-box > ul > li { + padding: 0 4px; + color: var(--text-color); +} + +ul[role=listbox] > li[aria-selected=true], +.combo-box > input[aria-haspopup] ~ ul > li:hover { + color: #fff; + background-color: var(--dialog-blue); +} + +.combo-box > input[aria-haspopup] ~ ul:hover > li[aria-selected=true]:not(:hover) { + color: inherit; + background-color: inherit; +} + +.combo-box.drop-down, +.combo-box:has(input[aria-haspopup]) { + position: relative; + box-shadow: var(--border-field); + display: inline-flex; + padding: 2px; +} + +.combo-box > input[aria-haspopup] { + height: 17px; + box-shadow: none; +} + +.combo-box > button { + width: 16px; + height: 17px; + background-image: svg-load("./icon/button-down.svg"); + background-repeat: no-repeat; + min-width: unset; + min-height: unset; + box-shadow: none; + padding: 0; +} + +.combo-box > button:focus { + outline: none; + outline-offset: 0; +} + +.combo-box > button:active { + box-shadow: none; +} + +.combo-box > input[aria-expanded=false] + button:active { + background-image: svg-load("./icon/button-down-active.svg"); +} + +.combo-box > input[aria-haspopup] ~ ul { + display: none; + position: absolute; + top: 100%; + left: 0; + width: 100%; + border: 1px solid #000; + overflow-y: auto; +} + +.combo-box > input[aria-expanded=true] ~ ul { + display: block; +} + +.combo-box > ul, +.combo-box > input:not([aria-haspopup]), +.combo-box > input[aria-expanded=true] { + cursor: default; +} + a { color: var(--link-blue); } From 75ea394fa5a49fdb913a73e0accc55ec9d389886 Mon Sep 17 00:00:00 2001 From: carterleseman Date: Mon, 10 Mar 2025 01:37:02 -0500 Subject: [PATCH 2/2] add list boxes; clean combo boxes/selects --- docs/docs.css | 12 +-- docs/index.html.ejs | 168 +++++++++++++++++++++++++++++++++------- docs/script.js | 146 ++++++++++++++++++++--------------- style.css | 182 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 381 insertions(+), 127 deletions(-) diff --git a/docs/docs.css b/docs/docs.css index d747ff8..4e675ac 100644 --- a/docs/docs.css +++ b/docs/docs.css @@ -116,20 +116,14 @@ button.active { inset -2px -2px #dfdfdf, inset 2px 2px #808080; } -ul[role=listbox] > li.last-hovered, .combo-box.key-nav > input[aria-haspopup] ~ ul > li[aria-selected=true] { color: #fff; background-color: #000080; } -.combo-box.key-nav > ul[role=listbox] > li.last-hovered, -.combo-box.key-nav > ul[role=listbox] > li:hover:not([aria-selected=true]) { - color: inherit; - background-color: inherit; -} - -.combo-box > input[aria-haspopup] ~ ul:has(.last-hovered) > li[aria-selected=true]:not(.last-hovered), -.combo-box:not(.key-nav) > input[aria-haspopup] ~ ul:has(.last-hovered) > li[aria-selected=true]:not(.last-hovered) { +div.key-nav > ul[role=listbox] > li[aria-current=true], +div.key-nav > ul[role=listbox] > li:hover:not([aria-selected=true]), +div:not(.key-nav) > input[aria-haspopup] ~ ul:has([aria-current=true]) > li[aria-selected=true]:not([aria-current=true]) { color: inherit; background-color: inherit; } diff --git a/docs/index.html.ejs b/docs/index.html.ejs index efffad4..3b87093 100644 --- a/docs/index.html.ejs +++ b/docs/index.html.ejs @@ -32,7 +32,7 @@
  • TextBox
  • Slider
  • ComboBox
  • -
  • Dropdown
  • +
  • ListBox
  • Window
      @@ -492,9 +492,9 @@
    <%- example(` -
    - -
      +
      + +
      • Regular
      • Italic
      • Bold
      • @@ -512,43 +512,43 @@
      <%- example(` -
      - +
      + -
        -
      • h:mm:ss tt
      • +
          +
        • h:mm:ss tt
        • hh:mm:ss tt
        • H:mm:ss
        • HH:mm:ss
      `) %> + +

      + For more options of the list box, see the ListBox section. +

  • - +

    ListBox

    - A drop-down list box allows the selection of only a - single item from a list. In its closed state, the control displays - the current value for the control. The user opens the list to change - the value. + A list box is a control for displaying a list of choices for the user.
    - — Microsoft Windows User Experience p. 175 + — Microsoft Windows User Experience p. 170

    - Dropdowns can be rendered by using the select and option - elements. + The simplest way to render a list box is by using the select element with a multiple attribute specified.

    <%- example(` - - + @@ -556,19 +556,131 @@ `) %>

    - By default, the first option will be selected. You can change this by - giving one of your option elements the selected - attribute. + The complex way is using a combination of the ul/li elements with the role attributes.

    <%- example(` - +
      +
    • Bitmap Image
    • +
    • Image Document
    • +
    • Media Clip
    • +
    • Wave Sound
    • +
    • WordPad Document
    • +
    + `) %> + +

    + To remove the scroll bar of the list box, use the no-scroll class. +

    + + <%- example(` +
      +
    • Edit
    • +
    • Open
    • +
    • Print
    • +
    + `) %> + +
    + A drop-down list box allows the selection of only a single item from a list; the difference is that the list is displayed on demand. In its closed state, the control displays the current value for the control. + +
    + — Microsoft Windows User Experience p. 175 +
    +
    + +

    + A drop-down can be rendered in 2 ways. The first is using the native select and option. The second is using a text input, a button, a parent ul, and children li together, wrapped inside a container element with the list-box class. For accessibility, follow the minimum requirements below: +

    + +
      +
    • Add aria-haspopup="listbox" and aria-expanded attributes to the text input
    • +
    • + Specify the relationship between the list box and the text box by combining the id of the listbox with the aria-controls attribute on the text input +
    • +
    + + <%- example(` +
    + + +
      +
    • Monochrome
    • +
    • 16 Color
    • +
    • 256 Color
    • +
    • High Color (16 bit)
    • +
    • True Color (24 bit)
    • +
    +
    + `) %> + +
    + Although most list boxes are single-selection lists, some contexts require the user to choose more than one item. Multiple-selection list boxes support this functionality. + +
    + — Microsoft Windows User Experience p. 176 +
    +
    + +

    A multiple-selection list box can be rendered by adding the aria-multiselectable attribute to the listbox, along with checkboxes.

    + + <%- example(` +
      +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    + `) %> + +

    Group options by wrapping them in a ul with parent li with the role="group" attribute. + To label a group of options, use a child span inside the parent li and reference its id in the aria-labelledby attribute of the li.

    + + <%- example(` +
      +
    • + Browsing +
        +
      • + + +
      • +
      • + + +
      • +
      +
    • +
    • + Multimedia +
        +
      • + + +
      • +
      • + + +
      • +
      • + + +
      • +
      +
    • +
    `) %>
    diff --git a/docs/script.js b/docs/script.js index 1de9e4d..0b54507 100644 --- a/docs/script.js +++ b/docs/script.js @@ -1,98 +1,120 @@ -// Combo Box -document.querySelectorAll('.combo-box').forEach(combobox => { - const input = combobox.querySelector('input'); - const listbox = combobox.querySelector('ul'); - const options = Array.from(listbox.querySelectorAll('li')); - let currentIndex = findIndex(); - let lastHovered = null; +// List Boxes & Combo Boxes +document.querySelectorAll('ul[role=listbox]').forEach(listbox => { + const input = document.getElementById(listbox.id.replace('-listbox', '-input')); + const expands = input?.hasAttribute('aria-haspopup'); + const multiselect = listbox.hasAttribute('aria-multiselectable'); + const options = Array.from(listbox.querySelectorAll('li[role="option"], li[role="group"] span')); + let currentIndex = findIndex(), current = null; function findIndex() { - return options.findIndex(option => option.getAttribute('aria-selected') === "true"); + let index = options.findIndex(option => option.getAttribute('aria-current') === "true"); + if (index === -1 && !multiselect) { + index = options.findIndex(option => option.getAttribute('aria-selected') === "true"); + } return index; } - function scroll(block) { + function scroll() { const index = currentIndex === -1 ? 0 : currentIndex; - options[index].scrollIntoView({ block, behavior: "instant"}); + options[index].scrollIntoView({ block: "nearest", behavior: "instant" }); } - function updateSelected(value = null) { - if (value !== null) input.value = value; - options.forEach(option => { - option.setAttribute('aria-selected', option.textContent.trim() === input.value.trim() ? 'true' : 'false'); - }); - currentIndex = findIndex(); - if (currentIndex !== -1) scroll('nearest'); - } - - function removeHovered() { - lastHovered?.classList.remove('last-hovered'); - lastHovered = null; + function removeCurrent() { + current?.setAttribute('aria-current', 'false'), current = null; } - function keyNavigate() { - combobox.classList.add('key-nav'); - if (lastHovered) currentIndex = options.indexOf(lastHovered); - removeHovered(); + function updateCurrent(value = null) { + if (input) { // Input-based listbox: update input value and aria-selected + if (value !== null) input.value = value; + options.forEach(option => option.setAttribute( + 'aria-selected', option.textContent.trim() === input.value.trim() ? 'true' : 'false' + )); + } else { // Non-input-based listbox: use current for aria-current + current = options[currentIndex]; + !multiselect ? + options.forEach(option => option.setAttribute('aria-selected', option === current ? 'true' : 'false')) : + (options.forEach(option => option.setAttribute('aria-current', 'false')), current.setAttribute('aria-current', 'true')); + } (currentIndex = findIndex()) !== -1 && scroll(); } function toggleDropdown() { const isOpen = input.getAttribute('aria-expanded') === "true"; input.setAttribute('aria-expanded', isOpen ? 'false' : 'true'); input.focus(); - !isOpen ? (currentIndex = findIndex(), scroll('nearest')) : removeHovered(); + !isOpen ? (currentIndex = findIndex(), scroll()) : removeCurrent(); + } + + function toggleCheck() { + const checkbox = current?.querySelector('input[type="checkbox"]'); + checkbox && (checkbox.checked = !checkbox.checked, current.setAttribute('aria-selected', checkbox.checked ? 'true' : 'false')); } function navigationHandler(event) { - if (event.altKey && (event.key === "ArrowDown" || event.key === "ArrowUp") && input.hasAttribute('aria-haspopup')) { + expands && (listbox.parentElement.classList.add('key-nav'), current && (currentIndex = options.indexOf(current), removeCurrent())); + + if (event.altKey && (event.key === "ArrowDown" || event.key === "ArrowUp") && expands) { event.preventDefault(); toggleDropdown(); } else if ((event.key === "ArrowDown" && currentIndex < options.length - 1) || (event.key === "ArrowUp" && currentIndex > 0)) { event.preventDefault(); - keyNavigate(); currentIndex += event.key === "ArrowDown" ? 1 : -1; - updateSelected(options[currentIndex].textContent); - } else if (event.key === "Enter" && input.hasAttribute('aria-haspopup') && input.getAttribute('aria-expanded') === "true") { + updateCurrent(options[currentIndex].textContent); + } else if (event.key === "Enter" && current?.role !== "group") { event.preventDefault(); - updateSelected(lastHovered ? lastHovered.textContent : null); - toggleDropdown(); + multiselect ? toggleCheck() : input && input.getAttribute('aria-expanded') === "true" && + (current = options[currentIndex], updateCurrent(current?.textContent), listbox.parentElement.classList.remove('key-nav'), toggleDropdown()); + } else if ((event.key === "Home" || event.key === "End") && !listbox.parentElement.classList.contains('combo-box')) { + event.preventDefault(); + currentIndex = event.key === "Home" ? 0 : options.length - 1; + updateCurrent(options[currentIndex].textContent); } } listbox.addEventListener('click', (event) => { - if (event.target.tagName === "LI") { - updateSelected(event.target.textContent); - if (input.hasAttribute('aria-haspopup')) toggleDropdown(); - } - }); + if (!input) listbox.focus(); - listbox.addEventListener('mousedown', (event) => { - event.preventDefault(); - input.focus(); + let target = event.target.tagName === "INPUT" || event.target.tagName === "LABEL" + ? event.target.closest('li[role="option"]') : event.target; + + if (target.tagName === "LI" && target.getAttribute('role') === "option" || target.tagName === "SPAN") { + currentIndex = Array.from(options).indexOf(target); + current = options[currentIndex]; + updateCurrent(current.textContent); + expands ? toggleDropdown() : (multiselect && toggleCheck()); + } }); - listbox.addEventListener('mousemove', () => combobox.classList.remove('key-nav')); + if (!input) { + listbox.addEventListener('keydown', navigationHandler); + listbox.addEventListener('mousedown', removeCurrent); + listbox.addEventListener('focus', () => currentIndex === -1 && (currentIndex = 0, updateCurrent())); + } else { + input.addEventListener('input', () => updateCurrent(input.value)); + input.addEventListener('keydown', navigationHandler); + listbox.addEventListener('mousedown', (e) => (e.preventDefault(), input.focus())); - if (input.hasAttribute('aria-haspopup')) { - const button = combobox.querySelector('button'); + if (expands) { + const button = listbox.parentElement.querySelector('button'); - listbox.addEventListener('mouseover', (event) => { - if (!combobox.classList.contains('key-nav') && event.target.tagName === "LI") { - removeHovered(); - lastHovered = event.target; - lastHovered.classList.add('last-hovered'); - } - }); - - button.addEventListener('click', () => toggleDropdown()); - input.addEventListener('blur', () => { - if (input.getAttribute('aria-expanded') === "true") { - setTimeout(() => { - if (document.activeElement !== input && document.activeElement !== button) toggleDropdown(); - }, 0); + function mouseHandler(event) { + if (event.target.tagName !== "LI") return; + listbox.parentElement.classList.contains('key-nav') + ? listbox.parentElement.classList.remove('key-nav') + : removeCurrent(); + current = event.target; + current.setAttribute('aria-current', 'true'); } - }); - } - input.addEventListener('input', () => updateSelected(input.value)); - input.addEventListener('keydown', navigationHandler); + listbox.addEventListener('mousemove', mouseHandler); + listbox.addEventListener('mouseover', mouseHandler); + + button.addEventListener('click', toggleDropdown); + input.addEventListener('blur', () => { + if (input.getAttribute('aria-expanded') === "true") { + setTimeout(() => { + if (![input, button].includes(document.activeElement)) toggleDropdown(); + }, 0); + } + }); + } + } }); \ No newline at end of file diff --git a/style.css b/style.css index ac3fbf0..b8be381 100644 --- a/style.css +++ b/style.css @@ -112,7 +112,7 @@ textarea, select, option, table, -.combo-box, +ul[role=listbox], ul.tree-view, .window, .title-bar, @@ -481,7 +481,7 @@ input[type="url"], input[type="tel"], input[type="number"], input[type="search"], -select, +select:not([multiple]), textarea { padding: 3px 4px; border: none; @@ -500,7 +500,7 @@ input[type="email"], input[type="url"], input[type="tel"], input[type="search"], -select { +select:not([multiple]) { height: 21px; } input[type="number"] { @@ -545,7 +545,7 @@ textarea:disabled { background-color: var(--surface); } -select { +select:not([multiple]) { appearance: none; -webkit-appearance: none; -moz-appearance: none; @@ -678,24 +678,42 @@ input[type="range"]::-moz-range-track { transform: translateY(0px) scaleX(-1); } -select:focus { +select:focus:not([multiple]) { color: var(--button-highlight); background-color: var(--dialog-blue); } + +select[multiple] option { + padding: 2px; +} + select:focus option { color: #000; background-color: #fff; } -select:active { +select:active:not([multiple]) { background-image: svg-load("./icon/button-down-active.svg"); } .combo-box { - display: inline-block; + display: flex; + flex-direction: column; +} + +.list-box, +.combo-box.drop-down, +.combo-box:has(input[aria-haspopup]) { + position: relative; + box-shadow: var(--border-field); + display: flex; + flex-direction: row; + height: 100%; + padding: 2px; + background: var(--button-highlight); } -.combo-box > ul { +ul[role=listbox] { background: #fff; border: 2px groove transparent; border-image: svg-load('./icon/sunken-panel-border.svg') 2; @@ -706,39 +724,70 @@ select:active { margin: 0; } -.combo-box > ul:focus-within { - outline: none; +ul[role=listbox].no-scroll { + overflow-y: auto; } -.combo-box > ul > li { - padding: 0 4px; +ul[role=listbox] > li, +ul[role=listbox] li[role=group] { + padding: 0 32px 0 4px; + white-space: nowrap; color: var(--text-color); } -ul[role=listbox] > li[aria-selected=true], +ul[role=listbox] > li[role=option] { + padding-block: 2px; +} + +ul[role=listbox]:focus-within { + outline: none; +} + +ul[role=listbox] > li[aria-current=true], +ul[role=listbox]:not([aria-multiselectable]) > li[aria-selected=true], .combo-box > input[aria-haspopup] ~ ul > li:hover { color: #fff; background-color: var(--dialog-blue); } -.combo-box > input[aria-haspopup] ~ ul:hover > li[aria-selected=true]:not(:hover) { - color: inherit; - background-color: inherit; +.list-box > input[aria-haspopup]:read-only { + color: var(--text-color); + background-color: #fff; } -.combo-box.drop-down, -.combo-box:has(input[aria-haspopup]) { - position: relative; - box-shadow: var(--border-field); - display: inline-flex; - padding: 2px; +.list-box > input[aria-haspopup]:read-only::selection { + color: inherit; + background-color: inherit; } +.list-box > input[aria-haspopup], .combo-box > input[aria-haspopup] { height: 17px; + flex: 1; box-shadow: none; } +:not(.combo-box) ul[role=listbox]:not([aria-multiselectable]):focus > li[aria-selected=true], +.list-box > input[aria-expanded=true] ~ ul[role=listbox] > li[aria-selected=true], +.list-box > input[aria-haspopup] ~ ul > li[aria-current=true], +ul[role=listbox][aria-multiselectable]:focus > li[role=option][aria-current=true] { + outline: 1px dotted yellow; + outline-offset: -1px; +} + +.list-box > input[aria-haspopup] ~ ul:has([aria-current=true]) > li[aria-selected=true]:not([aria-current=true]) { + outline: none; +} + +.list-box > input[aria-expanded=false]:focus { + background-color: var(--dialog-blue); + color: #fff; + outline: 1px dotted yellow; + outline-offset: -2px; + clip-path: inset(1px); +} + +.list-box > button, .combo-box > button { width: 16px; height: 17px; @@ -750,19 +799,18 @@ ul[role=listbox] > li[aria-selected=true], padding: 0; } -.combo-box > button:focus { +.list-box > button:focus, .combo-box > button:focus, +.list-box > button:active, .combo-box > button:active { outline: none; - outline-offset: 0; -} - -.combo-box > button:active { box-shadow: none; } +.list-box > input[aria-expanded=false] + button:active, .combo-box > input[aria-expanded=false] + button:active { background-image: svg-load("./icon/button-down-active.svg"); } +.list-box > ul, .combo-box > input[aria-haspopup] ~ ul { display: none; position: absolute; @@ -773,16 +821,94 @@ ul[role=listbox] > li[aria-selected=true], overflow-y: auto; } +.list-box > input[aria-expanded=true] ~ ul, .combo-box > input[aria-expanded=true] ~ ul { display: block; } -.combo-box > ul, +.list-box > input, +ul[role=listbox], .combo-box > input:not([aria-haspopup]), .combo-box > input[aria-expanded=true] { cursor: default; } +ul[role=listbox][aria-multiselectable] > li[role=option] { + display: flex; + align-items: center; + padding-block: 2px; +} + +ul[role=listbox][aria-multiselectable] li[role=option] > input[type=checkbox] + label { + margin-left: calc(var(--checkbox-width) + 3px); +} + +ul[role=listbox][aria-multiselectable] li[role=option] > input[type=checkbox] + label::before { + box-shadow: none; + outline: 1px solid var(--button-shadow); + outline-offset: -2px; + clip-path: inset(1px); +} + +ul[role=listbox][aria-multiselectable]:not(:focus) li[role=group] [aria-current=true], +ul[role=listbox][aria-multiselectable]:not(:focus) li[role=group] [aria-current=true] > label { + color: inherit; + background-color: inherit; + outline: none; +} + +ul[role=listbox][aria-multiselectable] li[role=option] > input[type="checkbox"]:active + label::before { + background: #fff; +} + +ul[role=listbox] li[role=group] { + padding-top: 2px; +} + +ul[role=listbox] li[role=group] ul { + list-style: none; + width: 100%; + padding-left: 14px; + padding-block: 2px; +} + +ul[role=listbox][aria-multiselectable] li[role=group] ul > li[role=option] > input[type="checkbox"] + label::before { + outline: 2px solid #000; + clip-path: none; +} + +ul[role=listbox] li[role=group]:not(:has(+ li)) > ul { + padding-bottom: 0; +} + +li[role=group] > ul > li[role=option] > label, +li[role=group] > span { + padding: 2px; +} + +ul[role=listbox]:not([aria-multiselectable]) li[role=group] > ul > li[role=option] { + width: fit-content; + padding: 2px; +} + +ul[role=listbox]:not([aria-multiselectable]) li[role=group] [aria-selected=true] { + background-color: var(--surface); +} + +li[role=group] > span[aria-current=true], +li[role=group] [aria-current=true] > label, +ul[role=listbox]:not([aria-multiselectable]):focus li[role=group] [aria-selected=true] { + color: #fff; + background-color: var(--dialog-blue); + outline: 1px dotted yellow; + outline-offset: -1px; +} + +ul[role=listbox][aria-multiselectable]:not(:focus) > li[role=option][aria-current=true] { + background-color: var(--dialog-blue); + color: #fff; +} + a { color: var(--link-blue); }