diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c2deba..0ac9960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v0.3.3] - 2026-04-27 + +### Added +- Add focus styles for custom buttons +- Add `pointer-events: none` to disabled custom buttons + +### Changed +- Custom buttons now use `:state(disabled)` instead of `:state(--disabled)` + ## [v0.3.2] - 2026-04-10 ### Changed diff --git a/button.js b/button.js index 01d4845..beb6f71 100644 --- a/button.js +++ b/button.js @@ -9,7 +9,7 @@ import { btnLinkActive, linkColor, } from './palette/aegis.js'; -const DISABLED_STATE = SUPPORTS_CUSTOM_STATES ? ':state(--disabled)' : '._state--disabled'; +const DISABLED_STATE = SUPPORTS_CUSTOM_STATES ? ':state(disabled)' : '._state--disabled'; const DISABLED = `:disabled, .disabled, ${DISABLED_STATE}`; const LAYER = 'components.aegisjsproject.button'; @@ -32,6 +32,10 @@ export const btn = css`@layer ${LAYER} { transition: background-color 150ms ease-in-out, border-color 150ms ease-in-out, color 150ms ease-in-out; } + .btn${DISABLED} { + pointer-events: none; + } + .btn.btn-sm { padding: 0.5em 1em; font-size: 0.7rem; diff --git a/custom-button.js b/custom-button.js index e412598..e9b9f80 100644 --- a/custom-button.js +++ b/custom-button.js @@ -18,12 +18,35 @@ export const customButton = css`@layer components.aegisjsproject.button { border-radius: 2px; padding: 2px 6px; font-family: system-ui, -apple-system, sans-serif; + font-size: small; + } + + :host(:focus-visible) { + /* Universal fallback for older engines or high contrast overrides */ + outline: auto; + outline: 2px solid CanvasText; + outline-offset: 2px; + } + + /* WebKit & Blink (Chrome, Safari, Edge) native focus ring targeting */ + @supports (outline-color: -webkit-focus-ring-color) { + :host(:focus-visible) { + outline: 5px auto -webkit-focus-ring-color; + } + } + + /* Firefox cross-platform fallback (Windows, Android, Linux) */ + @supports (-moz-appearance: none) { + :host(:focus-visible) { + outline: 2px solid AccentColor; + } } :host(${DISABLED_SELECTOR}) { color: GrayText; border-color: color-mix(in srgb, GrayText, transparent 50%); background-color: color-mix(in srgb, ButtonFace, transparent 50%); + pointer-events: none; } :host(:hover:not(${DISABLED_SELECTOR})) { diff --git a/package-lock.json b/package-lock.json index f3b1eb8..c5492bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aegisjsproject/styles", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aegisjsproject/styles", - "version": "0.3.2", + "version": "0.3.3", "funding": [ { "type": "librepay", @@ -851,6 +851,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1156,6 +1157,7 @@ "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2034,6 +2036,7 @@ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, diff --git a/package.json b/package.json index 713e513..d6f56c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aegisjsproject/styles", - "version": "0.3.2", + "version": "0.3.3", "description": "Pre-made and reusable styles for `@aegisjsproject/core`", "keywords": [ "aegis", diff --git a/test/index.js b/test/index.js index 628facd..9427e45 100644 --- a/test/index.js +++ b/test/index.js @@ -72,6 +72,8 @@ customElements.define('test-el', class TestElement extends HTMLElement { customElements.define('test-button', class TestButton extends HTMLElement { #shadow = this.attachShadow({ mode: 'open' }); #internals = this.attachInternals(); + #oldTabIndex = NaN; + #controller; connectedCallback() { const slot = document.createElement('slot'); @@ -79,15 +81,29 @@ customElements.define('test-button', class TestButton extends HTMLElement { slot.textContent = 'No content'; this.#shadow.append(slot); this.#internals.role = 'button'; - this.tabIndex = 0; + this.#controller = new AbortController(); + + if (! (this.hasAttribute('tabindex') || this.disabled)) { + this.tabIndex = 0; + } + this.#shadow.adoptedStyleSheets = [layers, customButton]; this.addEventListener('keydown', event => { - if (event.key === ' ' || event.key === 'Enter') { + if (event.key === 'Enter' && ! this.disabled) { event.preventDefault(); event.currentTarget.click(); + } else if (event.key === ' ' && ! this.disabled) { + event.preventDefault(); } - }); + }, { signal: this.#controller.signal }); + + this.addEventListener('keyup', event => { + if (event.key === ' ' && ! this.disabled) { + event.preventDefault(); + event.currentTarget.click(); + } + }, { signal: this.#controller.signal }); this.addEventListener('click', ({ currentTarget }) => { if (currentTarget.classList.contains('btn')) { @@ -99,15 +115,30 @@ customElements.define('test-button', class TestButton extends HTMLElement { } attributeChangedCallback(name, oldVal, newVal) { - if (typeof newVal === 'string') { - this.#internals.states.add('disabled'); - this.inert = true; - } else { - this.#internals.states.delete('disabled'); - this.inert = false; + switch(name) { + case 'disabled': + if (typeof newVal === 'string') { + this.#oldTabIndex = this.hasAttribute('tabindex') ? this.tabIndex : 0; + this.tabIndex = -1; + this.#internals.states.add('disabled'); + this.#internals.ariaDisabled = 'true'; + + if (this.ownerDocument.activeElement.isSameNode(this)) { + this.blur(); + } + } else { + this.#internals.states.delete('disabled'); + this.#internals.ariaDisabled = null; + this.tabIndex = Number.isNaN(this.#oldTabIndex) ? 0 : this.#oldTabIndex; + } + break; } } + disconnectedCallback() { + this.#controller.abort(); + } + get disabled() { return this.hasAttribute('disabled'); }