Skip to content

Commit

Permalink
Merge branch 'main' into feat/layouts/spacing-css-props
Browse files Browse the repository at this point in the history
  • Loading branch information
DiegoCardoso committed Jan 21, 2025
2 parents fc0b2af + dc87442 commit d6054de
Show file tree
Hide file tree
Showing 30 changed files with 842 additions and 371 deletions.
6 changes: 6 additions & 0 deletions packages/a11y-base/src/keyboard-direction-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ export declare class KeyboardDirectionMixinClass {
* Focus the given item. Override this method to add custom logic.
*/
protected _focusItem(item: Element, navigating: boolean): void;

/**
* Returns whether the item is focusable. By default,
* returns true if the item is not disabled.
*/
protected _isItemFocusable(item: Element): boolean;
}
14 changes: 13 additions & 1 deletion packages/a11y-base/src/keyboard-direction-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export const KeyboardDirectionMixin = (superclass) =>

const item = items[idx];

if (!item.hasAttribute('disabled') && this.__isMatchingItem(item, condition)) {
if (this._isItemFocusable(item) && this.__isMatchingItem(item, condition)) {
return idx;
}
}
Expand All @@ -189,4 +189,16 @@ export const KeyboardDirectionMixin = (superclass) =>
__isMatchingItem(item, condition) {
return typeof condition === 'function' ? condition(item) : true;
}

/**
* Returns whether the item is focusable. By default,
* returns true if the item is not disabled.
*
* @param {Element} item
* @return {boolean}
* @protected
*/
_isItemFocusable(item) {
return !item.hasAttribute('disabled');
}
};
21 changes: 20 additions & 1 deletion packages/a11y-base/src/tabindex-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const TabindexMixin = (superclass) =>
_disabledChanged(disabled, oldDisabled) {
super._disabledChanged(disabled, oldDisabled);

if (this.__shouldAllowFocusWhenDisabled()) {
return;
}

if (disabled) {
if (this.tabindex !== undefined) {
this._lastTabIndex = this.tabindex;
Expand All @@ -70,6 +74,10 @@ export const TabindexMixin = (superclass) =>
* @protected
*/
_tabindexChanged(tabindex) {
if (this.__shouldAllowFocusWhenDisabled()) {
return;
}

if (this.disabled && tabindex !== -1) {
this._lastTabIndex = tabindex;
this.tabindex = -1;
Expand All @@ -86,8 +94,19 @@ export const TabindexMixin = (superclass) =>
* @override
*/
focus() {
if (!this.disabled) {
if (!this.disabled || this.__shouldAllowFocusWhenDisabled()) {
super.focus();
}
}

/**
* Returns whether the component should be focusable when disabled.
* Returns false by default.
*
* @private
* @return {boolean}
*/
__shouldAllowFocusWhenDisabled() {
return false;
}
};
3 changes: 2 additions & 1 deletion packages/button/src/vaadin-button-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const buttonStyles = css`
}
:host([disabled]) {
pointer-events: none;
pointer-events: var(--_vaadin-button-disabled-pointer-events, none);
cursor: not-allowed;
}
/* Aligns the button with form fields when placed on the same line.
Expand Down
16 changes: 15 additions & 1 deletion packages/button/src/vaadin-button-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export const ButtonMixin = (superClass) =>
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button');
}

if (this.__shouldAllowFocusWhenDisabled()) {
this.style.setProperty('--_vaadin-button-disabled-pointer-events', 'auto');
}
}

/**
Expand Down Expand Up @@ -88,8 +92,18 @@ export const ButtonMixin = (superClass) =>

/** @private */
__onInteractionEvent(event) {
if (this.disabled) {
if (this.__shouldSuppressInteractionEvent(event)) {
event.stopImmediatePropagation();
}
}

/**
* Returns whether to suppress interaction events like `click`, `keydown`, etc.
* By default suppresses all interaction events when the button is disabled.
*
* @private
*/
__shouldSuppressInteractionEvent(_event) {
return this.disabled;
}
};
21 changes: 20 additions & 1 deletion packages/button/src/vaadin-button.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,26 @@ import { ButtonMixin } from './vaadin-button-mixin.js';
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*/
declare class Button extends ButtonMixin(ElementMixin(ThemableMixin(ControllerMixin(HTMLElement)))) {}
declare class Button extends ButtonMixin(ElementMixin(ThemableMixin(ControllerMixin(HTMLElement)))) {
/**
* When set to true, prevents all user interactions with the button such as
* clicking or hovering, and removes the button from the tab order, which
* makes it unreachable via the keyboard navigation.
*
* While the default behavior effectively prevents accidental interactions,
* it has an accessibility drawback: screen readers skip disabled buttons
* entirely, and users can't see tooltips that might explain why the button
* is disabled. To address this, an experimental enhancement allows disabled
* buttons to receive focus and show tooltips, while still preventing other
* interactions. This feature can be enabled with the following feature flag:
*
* ```
* // Set before any button is attached to the DOM.
* window.Vaadin.featureFlags.accessibleDisabledButtons = true
* ```
*/
disabled: boolean;
}

declare global {
interface HTMLElementTagNameMap {
Expand Down
31 changes: 31 additions & 0 deletions packages/button/src/vaadin-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ registerStyles('vaadin-button', buttonStyles, { moduleId: 'vaadin-button-styles'
* @mixes ThemableMixin
*/
class Button extends ButtonMixin(ElementMixin(ThemableMixin(ControllerMixin(PolymerElement)))) {
static get properties() {
return {
/**
* When set to true, prevents all user interactions with the button such as
* clicking or hovering, and removes the button from the tab order, which
* makes it unreachable via the keyboard navigation.
*
* While the default behavior effectively prevents accidental interactions,
* it has an accessibility drawback: screen readers skip disabled buttons
* entirely, and users can't see tooltips that might explain why the button
* is disabled. To address this, an experimental enhancement allows disabled
* buttons to receive focus and show tooltips, while still preventing other
* interactions. This feature can be enabled with the following feature flag:
*
* ```
* // Set before any button is attached to the DOM.
* window.Vaadin.featureFlags.accessibleDisabledButtons = true
* ```
*/
disabled: {
type: Boolean,
value: false,
},
};
}

static get is() {
return 'vaadin-button';
}
Expand All @@ -65,6 +91,11 @@ class Button extends ButtonMixin(ElementMixin(ThemableMixin(ControllerMixin(Poly
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
}

/** @override */
__shouldAllowFocusWhenDisabled() {
return window.Vaadin.featureFlags.accessibleDisabledButtons;
}
}

defineCustomElement(Button);
Expand Down
5 changes: 5 additions & 0 deletions packages/button/src/vaadin-lit-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class Button extends ButtonMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElem
this._tooltipController = new TooltipController(this);
this.addController(this._tooltipController);
}

/** @override */
__shouldAllowFocusWhenDisabled() {
return window.Vaadin.featureFlags.accessibleDisabledButtons;
}
}

defineCustomElement(Button);
Expand Down
46 changes: 46 additions & 0 deletions packages/button/test/button.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,50 @@ describe('vaadin-button', () => {
});
});
});

describe('disabled and accessible', () => {
let lastGlobalFocusable: HTMLInputElement;

before(() => {
window.Vaadin.featureFlags ??= {};
window.Vaadin.featureFlags.accessibleDisabledButtons = true;
});

after(() => {
window.Vaadin.featureFlags!.accessibleDisabledButtons = false;
});

beforeEach(async () => {
[button, lastGlobalFocusable] = fixtureSync(
`<div>
<vaadin-button disabled>Press me</vaadin-button>
<input id="last-global-focusable" />
</div>`,
).children as unknown as [Button, HTMLInputElement];
await nextRender(button);
});

afterEach(async () => {
await resetMouse();
});

it('should allow programmatic focus when disabled', () => {
button.focus();
expect(document.activeElement).to.equal(button);
});

it('should allow pointer focus when disabled', async () => {
const { x, y } = middleOfNode(button);
await sendMouse({ type: 'click', position: [Math.floor(x), Math.floor(y)] });
expect(document.activeElement).to.equal(button);
});

it('should allow keyboard focus when disabled', async () => {
await sendKeys({ press: 'Tab' });
expect(document.activeElement).to.equal(button);

await sendKeys({ press: 'Tab' });
expect(document.activeElement).to.equal(lastGlobalFocusable);
});
});
});
4 changes: 2 additions & 2 deletions packages/button/theme/lumo/vaadin-button-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const button = css`
/* Hover */
@media (any-hover: hover) {
:host(:hover)::before {
:host(:not([disabled]):hover)::before {
opacity: 0.02;
}
}
Expand Down Expand Up @@ -159,7 +159,7 @@ const button = css`
}
@media (any-hover: hover) {
:host([theme~='primary']:hover)::before {
:host([theme~='primary']:not([disabled]):hover)::before {
opacity: 0.05;
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/button/theme/material/vaadin-button-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const button = css`
vertical-align: middle;
}
:host(:hover)::before,
:host(:hover:not([disabled]))::before,
:host([focus-ring])::before {
opacity: 0.08;
transition-duration: 0.2s;
Expand All @@ -77,7 +77,7 @@ const button = css`
transition: 0s;
}
:host(:hover:not([active]))::after {
:host(:hover:not([active]):not([disabled]))::after {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
}
Expand Down Expand Up @@ -106,7 +106,7 @@ const button = css`
background-color: var(--material-secondary-background-color);
}
:host([theme~='contained']:hover) {
:host([theme~='contained']:not([disabled]):hover) {
box-shadow: var(--material-shadow-elevation-4dp);
}
Expand Down Expand Up @@ -149,7 +149,7 @@ const button = css`
transform: translate(50%, -50%) scale(0.0000001);
}
:host(:hover:not([active])[dir='rtl'])::after {
:host(:hover:not([active]):not([disabled])[dir='rtl'])::after {
transform: translate(50%, -50%) scale(1);
}
Expand Down
32 changes: 29 additions & 3 deletions packages/card/src/vaadin-card.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,39 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

/**
* `<vaadin-card>` is a visual content container.
* `<vaadin-card>` is a versatile container for grouping related content and actions.
* It presents information in a structured and visually appealing manner, with
* customization options to fit various design requirements.
*
* ```html
* <vaadin-card>
* <div>Card content</div>
* <vaadin-card theme="outlined cover-media">
* <img slot="media" width="200" src="..." alt="">
* <div slot="title">Lapland</div>
* <div slot="subtitle">The Exotic North</div>
* <div>Lapland is the northern-most region of Finland and an active outdoor destination.</div>
* <vaadin-button slot="footer">Book Vacation</vaadin-button>
* </vaadin-card>
* ```
*
* ### Styling
*
* The following shadow DOM parts are available for styling:
*
* Part name | Description
* ----------|-------------
* `media` | The container for the media element (e.g., image, video, icon). Shown above of before the card content.
* `header` | The container for title and subtitle - or for custom header content - and header prefix and suffix elements.
* `content` | The container for the card content (usually text content).
* `footer` | The container for footer elements. This part is always at the bottom of the card.
*
* The following custom properties are available for styling:
*
* Custom property | Description | Default
* ----------------|-------------|-------------
* `--vaadin-card-padding` | The space between the card edge and its content. Needs to a unified value for all edges, i.e., a single length value. | `1em`
* `--vaadin-card-gap` | The space between content elements within the card. | `1em`
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*/
declare class Card extends ElementMixin(ThemableMixin(HTMLElement)) {}

Expand Down
Loading

0 comments on commit d6054de

Please sign in to comment.