diff --git a/app/assets/stylesheets/lexxy-editor.css b/app/assets/stylesheets/lexxy-editor.css index 6456ffd27..e7550dc8e 100644 --- a/app/assets/stylesheets/lexxy-editor.css +++ b/app/assets/stylesheets/lexxy-editor.css @@ -636,7 +636,7 @@ padding: 1ch; position: relative; - &[aria-pressed="true"] { + &[aria-checked="true"] { background-color: var(--lexxy-color-selected); &:hover { @@ -782,6 +782,10 @@ grid-template-columns: repeat(auto-fill, minmax(var(--lexxy-toolbar-button-size), 1fr)); max-inline-size: var(--max-inline-size); + .lexxy-highlight-colors__group { + display: contents; + } + button { block-size: unset; inline-size: 100%; @@ -796,7 +800,7 @@ position: absolute; } - &[aria-pressed="true"] { + &[aria-checked="true"] { background-color: transparent; box-shadow: 0 0 0 2px currentColor inset; @@ -830,8 +834,7 @@ position: absolute; z-index: 2; - button, - summary { + button { align-items: center; background: transparent; block-size: var(--button-size); @@ -849,7 +852,7 @@ @media(any-hover: hover) { &:hover:not([aria-disabled="true"]), - [open] &:is(summary) { + &.lexxy-table-control__more-menu-trigger[aria-expanded="true"] { background: var(--lexxy-color-ink-medium); svg { @@ -880,8 +883,10 @@ } } - summary { + .lexxy-table-control__more-menu-trigger { + aspect-ratio: auto; box-shadow: 0 0 0 1px inset rgba(128, 128, 128, 0.3); + font-weight: normal; padding: 0 1.25ch; } @@ -909,6 +914,7 @@ white-space: nowrap; .lexxy-table-control__more-menu { + display: flex; gap: 0; position: relative; @@ -924,6 +930,10 @@ position: absolute; transform: translateX(-50%); + &[hidden] { + display: none; + } + button { aspect-ratio: unset; flex-direction: row; diff --git a/app/assets/stylesheets/lexxy-variables.css b/app/assets/stylesheets/lexxy-variables.css index e9d93ced0..1bc6b629e 100644 --- a/app/assets/stylesheets/lexxy-variables.css +++ b/app/assets/stylesheets/lexxy-variables.css @@ -49,11 +49,11 @@ --highlight-bg-1: rgba(229, 223, 6, 0.3); --highlight-bg-2: rgba(255, 185, 87, 0.3); - --highlight-bg-3: rgba(255, 118, 118, 0.3); + --highlight-bg-3: rgba(255, 118, 118, 0.3); --highlight-bg-4: rgba(248, 137, 216, 0.3); - --highlight-bg-5: rgba(190, 165, 255, 0.3); + --highlight-bg-5: rgba(190, 165, 255, 0.3); --highlight-bg-6: rgba(124, 192, 252, 0.3); - --highlight-bg-7: rgba(140, 255, 129, 0.3); + --highlight-bg-7: rgba(140, 255, 129, 0.3); --highlight-bg-8: rgba(221, 170, 123, 0.3); --highlight-bg-9: rgba(200, 200, 200, 0.3); @@ -84,4 +84,4 @@ --lexxy-radius: 0.5ch; --lexxy-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); --lexxy-z-popup: 1000; -} \ No newline at end of file +} diff --git a/src/config/lexxy.js b/src/config/lexxy.js index 1056f3669..8d79be9ac 100644 --- a/src/config/lexxy.js +++ b/src/config/lexxy.js @@ -1,5 +1,4 @@ import Configuration from "./configuration" -import { range } from "../helpers/array_helper.js" const global = new Configuration({ attachmentTagName: "action-text-attachment", @@ -21,8 +20,28 @@ const presets = new Configuration({ headings: [ "h2", "h3", "h4" ], highlight: { buttons: { - color: range(1, 9).map(n => `var(--highlight-${n})`), - "background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`), + color: [ + { value: "var(--highlight-1)", label: "Yellow" }, + { value: "var(--highlight-2)", label: "Orange" }, + { value: "var(--highlight-3)", label: "Red" }, + { value: "var(--highlight-4)", label: "Magenta" }, + { value: "var(--highlight-5)", label: "Purple" }, + { value: "var(--highlight-6)", label: "Blue" }, + { value: "var(--highlight-7)", label: "Green" }, + { value: "var(--highlight-8)", label: "Sand" }, + { value: "var(--highlight-9)", label: "Gray" }, + ], + "background-color": [ + { value: "var(--highlight-bg-1)", label: "Yellow" }, + { value: "var(--highlight-bg-2)", label: "Orange" }, + { value: "var(--highlight-bg-3)", label: "Red" }, + { value: "var(--highlight-bg-4)", label: "Magenta" }, + { value: "var(--highlight-bg-5)", label: "Purple" }, + { value: "var(--highlight-bg-6)", label: "Blue" }, + { value: "var(--highlight-bg-7)", label: "Green" }, + { value: "var(--highlight-bg-8)", label: "Sand" }, + { value: "var(--highlight-bg-9)", label: "Gray" }, + ], }, permit: { color: [], diff --git a/src/elements/dropdown/heading.js b/src/elements/dropdown/heading.js index ea4bfa37f..e84f153b2 100644 --- a/src/elements/dropdown/heading.js +++ b/src/elements/dropdown/heading.js @@ -64,8 +64,8 @@ export class HeadingDropdown extends HTMLElement { updateActiveHeading(tag) { this.#headingButtons.forEach(button => { const next = (button.dataset.heading === tag).toString() - if (button.getAttribute("aria-pressed") !== next) { - button.setAttribute("aria-pressed", next) + if (button.getAttribute("aria-checked") !== next) { + button.setAttribute("aria-checked", next) } }) } @@ -110,7 +110,7 @@ export class HeadingDropdown extends HTMLElement { button.classList.add("lexxy-heading-button") button.name = name button.title = label - button.setAttribute("role", "menuitem") + button.setAttribute("role", "menuitemradio") button.innerHTML = `${icon} ${label}` return button } diff --git a/src/elements/dropdown/highlight.js b/src/elements/dropdown/highlight.js index 15e1026a1..7e7f0ca1a 100644 --- a/src/elements/dropdown/highlight.js +++ b/src/elements/dropdown/highlight.js @@ -3,6 +3,7 @@ import { $getSelectionStyleValueForProperty } from "@lexical/selection" import { ToolbarDropdown } from "../toolbar_dropdown" import { registerEventListener } from "../../helpers/listener_helper" import { createElement } from "../../helpers/html_helper" +import { normalizeColorButtons } from "../../helpers/format_helper" const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button" const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']" @@ -35,27 +36,36 @@ export class HighlightDropdown extends ToolbarDropdown { const colorGroups = this.editorElement.config.get("highlight.buttons") - this.#populateButtonGroup("color", colorGroups.color) - this.#populateButtonGroup("background-color", colorGroups["background-color"]) + this.#appendColorGroup("Text color", "color", colorGroups.color) + this.#appendColorGroup("Background color", "background-color", colorGroups["background-color"]) const maxNumberOfColors = Math.max(colorGroups.color.length, colorGroups["background-color"].length) this.panel.style.setProperty("--max-colors", maxNumberOfColors) } - #populateButtonGroup(attribute, values) { - values.forEach((value, index) => { - this.#buttonContainer.appendChild(this.#createButton(attribute, value, index)) + #appendColorGroup(label, attribute, buttons) { + const group = createElement("div", { + role: "group", + class: "lexxy-highlight-colors__group", + "aria-label": label }) + + normalizeColorButtons(buttons).forEach((button, index) => { + group.appendChild(this.#createButton(attribute, button, index)) + }) + + this.#buttonContainer.append(group) } - #createButton(attribute, value, index) { + #createButton(attribute, button, index) { return createElement("button", { type: "button", - dataset: { value, style: attribute }, - style: `${attribute}: ${value}`, + dataset: { value: button.value, style: attribute }, + style: `${attribute}: ${button.value}`, class: "lexxy-editor__toolbar-button lexxy-highlight-button", name: `${attribute}-${index}`, - role: "menuitem" + role: "menuitemcheckbox", + "aria-label": button.label }) } @@ -79,11 +89,7 @@ export class HighlightDropdown extends ToolbarDropdown { const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", NO_STYLE) this.#colorButtons.forEach(button => { - const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor - const next = matchesSelection.toString() - if (button.getAttribute("aria-pressed") !== next) { - button.setAttribute("aria-pressed", next) - } + button.ariaChecked = button.dataset.value === textColor || button.dataset.value === backgroundColor }) const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE diff --git a/src/elements/editor.js b/src/elements/editor.js index 7a9564df2..91ad85347 100644 --- a/src/elements/editor.js +++ b/src/elements/editor.js @@ -29,7 +29,7 @@ import Contents from "../editor/contents" import Clipboard from "../editor/clipboard" import Extensions from "../editor/extensions" import { BrowserAdapter } from "../editor/adapters/browser_adapter" -import { getHighlightStyles } from "../helpers/format_helper" +import { getHighlightStyles, normalizeColorButtons } from "../helpers/format_helper" import { styleResolverRoot } from "../helpers/style_resolver_root" import { CustomActionTextAttachmentNode } from "../nodes/custom_action_text_attachment_node" @@ -867,21 +867,22 @@ export class LexicalEditorElement extends HTMLElement { // — triggering at most one forced reflow. The previous implementation interleaved // setProperty/getComputedStyle/removeProperty on the same element, forcing a style // recalc on every iteration during editor initialization. - #resolveColors(property, cssValues) { + #resolveColors(property, buttons) { const container = document.createElement("span") container.style.display = "none" - const resolvers = cssValues.map(cssValue => { + const resolvers = normalizeColorButtons(buttons).map(({ value, label }) => { const element = document.createElement("span") - element.style.setProperty(property, cssValue) + element.style.setProperty(property, value) container.appendChild(element) - return { element, name: cssValue } + return { element, name: value, label } }) styleResolverRoot().appendChild(container) - const resolved = resolvers.map(({ element, name }) => ({ + const resolved = resolvers.map(({ element, name, label }) => ({ name, + label, value: window.getComputedStyle(element).getPropertyValue(property) })) diff --git a/src/elements/table/table_tools.js b/src/elements/table/table_tools.js index 14f822c71..3fe2be7a1 100644 --- a/src/elements/table/table_tools.js +++ b/src/elements/table/table_tools.js @@ -13,7 +13,7 @@ export class TableTools extends HTMLElement { #listeners = new ListenerBin() connectedCallback() { - this.tableController = new TableController(this.#editorElement) + this.tableController = new TableController(this.editorElement) this.classList.add("lexxy-floating-controls") this.role = "toolbar" @@ -34,16 +34,34 @@ export class TableTools extends HTMLElement { this.tableController = null } - get #editor() { - return this.#editorElement.editor + get editorElement() { + return this.closest("lexxy-editor") } - get #editorElement() { - return this.closest("lexxy-editor") + get editor() { + return this.editorElement.editor + } + + getEditorElement() { + return this.editorElement + } + + closeDropdowns({ except } = {}) { + this.#moreMenus.forEach(menu => { + if (menu !== except) menu.close({ focusEditor: false }) + }) + } + + get #moreMenus() { + return this.querySelectorAll("lexxy-toolbar-dropdown") + } + + get #hasOpenMoreMenu() { + return Array.from(this.#moreMenus).some(menu => menu.isOpen) } get #tableToolsButtons() { - return Array.from(this.querySelectorAll("button, details > summary")) + return Array.from(this.querySelectorAll("button")).filter(button => !button.closest("[data-dropdown-panel]")) } #setUpButtons() { @@ -62,23 +80,37 @@ export class TableTools extends HTMLElement { const plusButton = this.#createButton(`Add ${childType}`, { action: "insert", childType, direction: "after" }, "+") const minusButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType }, "−") - const dropdown = createElement("details", { className: "lexxy-table-control__more-menu" }) - dropdown.setAttribute("name", "lexxy-dropdown") - dropdown.tabIndex = -1 - - const count = createElement("summary", {}, `_ ${childType}s`) - setCountProperty(count) - dropdown.appendChild(count) - - dropdown.appendChild(moreMenu) - container.appendChild(minusButton) - container.appendChild(dropdown) + container.appendChild(this.#createMoreMenu(childType, setCountProperty, moreMenu)) container.appendChild(plusButton) return container } + #createMoreMenu(childType, setCountProperty, menu) { + const dropdown = createElement("lexxy-toolbar-dropdown", { className: "lexxy-table-control__more-menu" }) + + const trigger = createElement("button", { + type: "button", + className: "lexxy-table-control__more-menu-trigger", + dataset: { dropdownTrigger: "" }, + "aria-haspopup": "menu", + "aria-expanded": "false" + }, `_ ${childType}s`) + trigger.tabIndex = -1 + setCountProperty(trigger) + + menu.dataset.dropdownPanel = "" + menu.role = "menu" + menu.setAttribute("aria-label", `${childType} options`) + menu.hidden = true + + dropdown.appendChild(trigger) + dropdown.appendChild(menu) + + return dropdown + } + #createRowButtonsContainer() { return this.#createButtonsContainer( "row", @@ -97,15 +129,18 @@ export class TableTools extends HTMLElement { #createMoreMenuSection(childType) { const section = createElement("div", { className: "lexxy-floating-controls__group lexxy-table-control__more-menu-details" }) - const addBeforeButton = this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" }) - const addAfterButton = this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" }) - const toggleStyleButton = this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType }) - const deleteButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType }) - section.appendChild(addBeforeButton) - section.appendChild(addAfterButton) - section.appendChild(toggleStyleButton) - section.appendChild(deleteButton) + const items = [ + this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" }), + this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" }), + this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType }), + this.#createButton(`Remove ${childType}`, { action: "delete", childType }) + ] + + items.forEach(item => { + item.role = "menuitem" + section.appendChild(item) + }) return section } @@ -146,7 +181,7 @@ export class TableTools extends HTMLElement { } #registerKeyboardShortcuts() { - this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#focusToolbarOnAltF10, COMMAND_PRIORITY_HIGH)) + this.#listeners.track(this.editor.registerCommand(KEY_DOWN_COMMAND, this.#focusToolbarOnAltF10, COMMAND_PRIORITY_HIGH)) } #focusToolbarOnAltF10 = (event) => { @@ -177,9 +212,9 @@ export class TableTools extends HTMLElement { const cell = this.tableController.currentCell if (!cell) return - this.#editor.update(() => { + this.editor.update(() => { cell.select() - this.#editor.focus() + this.editor.focus() }) this.#update() @@ -216,7 +251,7 @@ export class TableTools extends HTMLElement { if (!cellsToHighlight) return cellsToHighlight.forEach(cell => { - const cellElement = this.#editor.getElementByKey(cell.getKey()) + const cellElement = this.editor.getElementByKey(cell.getKey()) if (!cellElement) return cellElement.classList.toggle(theme.tableCellHighlight, true) @@ -225,8 +260,8 @@ export class TableTools extends HTMLElement { } #monitorForTableSelection() { - this.#listeners.track(this.#editor.registerUpdateListener(() => { - const tableNode = this.#editor.getRootElement() && this.tableController.updateSelectedTable() + this.#listeners.track(this.editor.registerUpdateListener(() => { + const tableNode = this.editor.getRootElement() && this.tableController.updateSelectedTable() if (tableNode) { this.#show() @@ -237,8 +272,12 @@ export class TableTools extends HTMLElement { } #executeTableCommand(command) { + const fromMoreMenu = this.#hasOpenMoreMenu + this.tableController.executeTableCommand(command) this.#update() + + if (fromMoreMenu) this.editor.focus() } #show() { @@ -262,18 +301,18 @@ export class TableTools extends HTMLElement { } #closeMoreMenu() { - this.querySelector("details[open]")?.removeAttribute("open") + this.closeDropdowns() } #updateButtonsPosition() { const tableNode = this.tableController.currentTableNode if (!tableNode) return - const tableElement = this.#editor.getElementByKey(tableNode.getKey()) + const tableElement = this.editor.getElementByKey(tableNode.getKey()) if (!tableElement) return const tableRect = tableElement.getBoundingClientRect() - const editorRect = this.#editorElement.getBoundingClientRect() + const editorRect = this.editorElement.getBoundingClientRect() const relativeTop = tableRect.top - editorRect.top const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left @@ -285,7 +324,7 @@ export class TableTools extends HTMLElement { const tableNode = this.tableController.currentTableNode if (!tableNode) return - const tableElement = $getElementForTableNode(this.#editor, tableNode) + const tableElement = $getElementForTableNode(this.editor, tableNode) if (!tableElement) return const rowCount = tableElement.rows @@ -299,18 +338,18 @@ export class TableTools extends HTMLElement { const cell = this.tableController.currentCell if (!cell) return - const cellElement = this.#editor.getElementByKey(cell.getKey()) + const cellElement = this.editor.getElementByKey(cell.getKey()) if (!cellElement) return cellElement.classList.add(theme.tableCellFocus) } #clearCellStyles() { - this.#editorElement.querySelectorAll(`.${theme.tableCellFocus}`)?.forEach(cell => { + this.editorElement.querySelectorAll(`.${theme.tableCellFocus}`)?.forEach(cell => { cell.classList.remove(theme.tableCellFocus) }) - this.#editorElement.querySelectorAll(`.${theme.tableCellHighlight}`)?.forEach(cell => { + this.editorElement.querySelectorAll(`.${theme.tableCellHighlight}`)?.forEach(cell => { cell.classList.remove(theme.tableCellHighlight) cell.removeAttribute("data-action") cell.removeAttribute("data-child-type") diff --git a/src/elements/toolbar.js b/src/elements/toolbar.js index 8f3ca160b..959c869ea 100644 --- a/src/elements/toolbar.js +++ b/src/elements/toolbar.js @@ -8,7 +8,7 @@ import { } from "lexical" import { getNonce } from "../helpers/csp_helper" import { ListenerBin, registerEventListener } from "../helpers/listener_helper" -import { handleRollingTabIndex } from "../helpers/accessibility_helper" +import { handleRollingTabIndex, isKeyboardActivation } from "../helpers/accessibility_helper" import ToolbarIcons from "./toolbar_icons" import { generateDomId, isActiveAndVisible } from "../helpers/html_helper" @@ -134,14 +134,21 @@ export class LexicalToolbarElement extends HTMLElement { } } - #dispatchButtonCommand(event, { dataset: { command, payload } }) { - const isKeyboard = event instanceof PointerEvent && event.pointerId === -1 + #dispatchButtonCommand(event, button) { + if (button.ariaDisabled === "true") return + + const { command, payload } = button.dataset + const shouldKeepToolbarFocus = isKeyboardActivation(event) && !this.#belongsToDropdown(button) this.editor.update(() => { this.editor.dispatchCommand(command, payload) - }, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined }) + }, { tag: shouldKeepToolbarFocus ? SKIP_DOM_SELECTION_TAG : undefined }) + + if (!shouldKeepToolbarFocus) this.editor.focus() + } - if (!isKeyboard) this.editor.focus() + #belongsToDropdown(button) { + return button.closest("[data-dropdown-panel]") != null } #bindHotkeys() { @@ -181,8 +188,8 @@ export class LexicalToolbarElement extends HTMLElement { } #handleEditorFocus = () => { - const firstVisible = this.#buttons.find(isActiveAndVisible) - if (firstVisible) firstVisible.tabIndex = 0 + const firstVisibleButton = this.#toolbarButtons.find(isActiveAndVisible) + if (firstVisibleButton) firstVisibleButton.tabIndex = 0 } #handleEditorBlur = () => { @@ -190,11 +197,11 @@ export class LexicalToolbarElement extends HTMLElement { } #handleKeydown = (event) => { - handleRollingTabIndex(this.#buttons, event) + handleRollingTabIndex(this.#toolbarButtons, event) } #resetTabIndexValues() { - this.#buttons.forEach((button) => { + this.#toolbarButtons.forEach((button) => { button.tabIndex = -1 }) } @@ -231,7 +238,7 @@ export class LexicalToolbarElement extends HTMLElement { this.#setButtonPressed("underline", isUnderline) this.#setButtonPressed("format", isInHeading) - this.#setButtonPressed("paragraph", !isInHeading) + this.#setButtonChecked("paragraph", !isInHeading) this.querySelector("lexxy-heading-dropdown") ?.updateActiveHeading(headingTag) @@ -250,25 +257,17 @@ export class LexicalToolbarElement extends HTMLElement { #setButtonPressed(name, isPressed) { const button = this.querySelector(`[name="${name}"]`) - if (button) { - const next = isPressed.toString() - if (button.getAttribute("aria-pressed") !== next) { - button.setAttribute("aria-pressed", next) - } - } + if (button) button.ariaPressed = isPressed + } + + #setButtonChecked(name, isChecked) { + const button = this.querySelector(`[name="${name}"]`) + if (button) button.ariaChecked = isChecked } #setButtonDisabled(name, isDisabled) { const button = this.querySelector(`[name="${name}"]`) - if (button) { - if (button.disabled !== isDisabled) { - button.disabled = isDisabled - } - const next = isDisabled.toString() - if (button.getAttribute("aria-disabled") !== next) { - button.setAttribute("aria-disabled", next) - } - } + if (button) button.ariaDisabled = isDisabled } #refreshOverflow() { @@ -369,8 +368,10 @@ export class LexicalToolbarElement extends HTMLElement { return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow])")) } - get #buttons() { - return Array.from(this.querySelectorAll(":scope button")) + get #toolbarButtons() { + return Array.from(this.querySelectorAll(":scope button")).filter((button) => { + return !button.closest("[data-dropdown-panel]") + }) } get #toolbarItems() { @@ -409,8 +410,8 @@ export class LexicalToolbarElement extends HTMLElement { -