Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions projects/core/src/internal/base/button.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('base button', () => {
});

it('should remove aria-disabled if readonly', async () => {
element.readonly = true;
element.readOnly = true;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native input form apis use readOnly for properties and readonly for attributes. This aligns our APIs to the platform read only APis. This updates all of our internal references to use readOnly but is backwards compatible with our pre-existing readonly property. The attribute behavior remains the same as all lowercase readonly. This is important to align on as we expand the form control capabilities.

await elementIsStable(element);
expect(element._internals.ariaDisabled).toBe(null);
expect(element.matches(':state(disabled)')).toBe(false);
Expand Down Expand Up @@ -117,7 +117,7 @@ describe('base button', () => {
expect(element._internals.ariaExpanded).toBe('true');
expect(element.matches(':state(expanded)')).toBe(true);

element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaExpanded).toBe(null);
expect(element.matches(':state(expanded)')).toBe(false);
Expand Down Expand Up @@ -149,7 +149,7 @@ describe('base button', () => {
expect(element._internals.ariaPressed).toBe('true');
expect(element.matches(':state(pressed)')).toBe(true);

element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaPressed).toBe(null);
expect(element.matches(':state(pressed)')).toBe(false);
Expand All @@ -172,31 +172,54 @@ describe('base button', () => {
});

it('should remove tabindex and role if readonly', async () => {
element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element.tabIndex).toBe(-1);
expect(element._internals.role).toBe('none');
expect(element.getAttribute('role')).toBe(null);
});

it('should map readonly attribute to readOnly property', async () => {
element.setAttribute('readonly', '');
await elementIsStable(element);
expect(element.readOnly).toBe(true);
});

it('should reflect readOnly property to readonly attribute', async () => {
element.readOnly = true;
await elementIsStable(element);
expect(element.hasAttribute('readonly')).toBe(true);

element.readOnly = false;
await elementIsStable(element);
expect(element.hasAttribute('readonly')).toBe(false);
});

it('should support deprecated readonly property alias', async () => {
element.readonly = true;
await elementIsStable(element);
expect(element.readOnly).toBe(true);
expect(element.hasAttribute('readonly')).toBe(true);
});

it('should set the button type to submit if not defined within a form element', async () => {
await elementIsStable(element);
expect(element.type).toBe(undefined);
expect(buttonInForm.type).toBe('button');
expect(submitButtonInForm.type).toBe('submit');
});

it('should add or remove button event listeners when readonly updates', async () => {
it('should add or remove button event listeners when readOnly updates', async () => {
await elementIsStable(submitButtonInForm);
expect(submitButtonInForm.readonly).toBe(undefined);
expect(submitButtonInForm.readOnly).toBe(false);

vi.spyOn(submitButtonInForm, 'removeEventListener');
submitButtonInForm.readonly = true;
submitButtonInForm.readOnly = true;
await elementIsStable(submitButtonInForm);
expect(submitButtonInForm.removeEventListener).toBeCalledTimes(3); // 2x button controller, 1x command controller

vi.spyOn(submitButtonInForm, 'addEventListener');
submitButtonInForm.readonly = false;
submitButtonInForm.readOnly = false;
await elementIsStable(submitButtonInForm);
expect(submitButtonInForm.addEventListener).toBeCalledTimes(3); // 2x button controller, 1x command controller
});
Expand Down
13 changes: 12 additions & 1 deletion projects/core/src/internal/base/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,18 @@ export class BaseButton extends LitElement {
* Like input readonly, sets a button semantically as visual treatment only
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly
*/
@property({ type: Boolean, reflect: true }) readonly: boolean;
@property({ type: Boolean, attribute: 'readonly', reflect: true }) readOnly = false;

/**
* @deprecated Use `readOnly`. The `readonly` attribute remains supported.
*/
get readonly(): boolean {
return this.readOnly;
}

set readonly(value: boolean) {
this.readOnly = value; // eslint-disable-line local/stateless-property
}

#form: string | HTMLFormElement | null = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { stateCurrent } from '@nvidia-elements/core/internal';
@customElement('state-current-controller-test-element')
class StateCurrentControllerTestElement extends LitElement {
@property({ type: String }) current: 'page' | 'step';
@property({ type: Boolean }) readonly: boolean;
@property({ type: Boolean, attribute: 'readonly' }) readOnly = false;
declare _internals: ElementInternals;

render() {
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('state-current.controller', () => {
expect(element._internals.ariaCurrent).toBe('page');
expect(element.matches(':state(current)')).toBe(true);

element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaCurrent).toBe(null);
expect(element.matches(':state(current)')).toBe(false);
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('state-current.controller', () => {
a.href = '#';
element.appendChild(a);
element.current = 'page';
element.readonly = true;
element.readOnly = true;
element._internals.states.add('anchor');
element.requestUpdate();
await elementIsStable(element);
Expand All @@ -119,12 +119,12 @@ describe('state-current.controller', () => {

it('should restore current state when readonly is removed', async () => {
element.current = 'page';
element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaCurrent).toBe(null);
expect(element.matches(':state(current)')).toBe(false);

element.readonly = false;
element.readOnly = false;
await elementIsStable(element);
expect(element._internals.ariaCurrent).toBe('page');
expect(element.matches(':state(current)')).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function stateCurrent<T extends Current>(): ClassDecorator {
target.addInitializer!((instance: T) => new StateCurrentController(instance));
}

type Current = ReactiveElement & { current: 'page' | 'step'; readonly?: boolean; _internals?: ElementInternals };
type Current = ReactiveElement & { current: 'page' | 'step'; readOnly?: boolean; _internals?: ElementInternals };

export class StateCurrentController<T extends Current> implements ReactiveController {
constructor(private host: T) {
Expand All @@ -26,7 +26,7 @@ export class StateCurrentController<T extends Current> implements ReactiveContro
}

hostUpdated() {
if (this.host.readonly) {
if (this.host.readOnly) {
this.host._internals!.ariaCurrent = null;
this.host._internals!.states.delete('current');
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { stateDisabled } from '@nvidia-elements/core/internal';
@customElement('state-disabled-controller-test-element')
class StateDisabledControllerTestElement extends LitElement {
@property({ type: Boolean }) disabled = false;
@property({ type: Boolean }) readonly = false;
@property({ type: Boolean, attribute: 'readonly' }) readOnly = false;
declare _internals: ElementInternals;
}

Expand Down Expand Up @@ -54,7 +54,7 @@ describe('state-disabled.controller', () => {
});

it('should remove aria-disabled if readonly', async () => {
element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaDisabled).toBe(null);
expect(element.matches(':state(disabled)')).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function stateDisabled<T extends Disabled>(): ClassDecorator {
target.addInitializer!((instance: T) => new StateDisabledController(instance));
}

export type Disabled = ReactiveElement & { disabled: boolean; readonly?: boolean; _internals?: ElementInternals };
export type Disabled = ReactiveElement & { disabled: boolean; readOnly?: boolean; _internals?: ElementInternals };

export class StateDisabledController<T extends Disabled> implements ReactiveController {
constructor(private host: T) {
Expand All @@ -39,7 +39,7 @@ export class StateDisabledController<T extends Disabled> implements ReactiveCont
this.host._internals!.states.delete('disabled');
}

if (this.host.readonly) {
if (this.host.readOnly) {
this.host._internals!.ariaDisabled = null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { stateExpanded } from '@nvidia-elements/core/internal';
@customElement('state-expanded-controller-test-element')
class StateExpandedControllerTestElement extends LitElement {
@property({ type: Boolean }) expanded: boolean;
@property({ type: Boolean }) readonly: boolean;
@property({ type: Boolean, attribute: 'readonly' }) readOnly = false;
declare _internals: ElementInternals;
}

Expand Down Expand Up @@ -66,7 +66,7 @@ describe('state-expanded.controller', () => {
expect(element._internals.ariaExpanded).toBe('true');
expect(element.matches(':state(expanded)')).toBe(true);

element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaExpanded).toBe(null);
expect(element.matches(':state(expanded)')).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function stateExpanded<T extends Expanded>(): ClassDecorator {
target.addInitializer!((instance: T) => new StateExpandedController(instance));
}

export type Expanded = ReactiveElement & { expanded: boolean; readonly?: boolean; _internals?: ElementInternals };
export type Expanded = ReactiveElement & { expanded: boolean; readOnly?: boolean; _internals?: ElementInternals };

export class StateExpandedController<T extends Expanded> implements ReactiveController {
constructor(private host: T) {
Expand All @@ -37,7 +37,7 @@ export class StateExpandedController<T extends Expanded> implements ReactiveCont
this.host._internals!.states.delete('expanded');
}

if (this.host.readonly) {
if (this.host.readOnly) {
this.host._internals!.ariaExpanded = null;
this.host._internals!.states.delete('expanded');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { statePressed } from '@nvidia-elements/core/internal';
@customElement('state-pressed-controller-test-element')
class StatePressedControllerTestElement extends LitElement {
@property({ type: Boolean }) pressed: boolean;
@property({ type: Boolean }) readonly: boolean;
@property({ type: Boolean, attribute: 'readonly' }) readOnly = false;
declare _internals: ElementInternals;
}

Expand Down Expand Up @@ -60,7 +60,7 @@ describe('state-pressed.controller', () => {
expect(element._internals.ariaPressed).toBe('true');
expect(element.matches(':state(pressed)')).toBe(true);

element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaPressed).toBe(null);
expect(element.matches(':state(pressed)')).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function statePressed<T extends Pressed>(): ClassDecorator {
target.addInitializer!((instance: T) => new StatePressedController(instance));
}

export type Pressed = ReactiveElement & { pressed: boolean; readonly?: boolean; _internals?: ElementInternals };
export type Pressed = ReactiveElement & { pressed: boolean; readOnly?: boolean; _internals?: ElementInternals };

export class StatePressedController<T extends Pressed> implements ReactiveController {
constructor(private host: T) {
Expand All @@ -37,7 +37,7 @@ export class StatePressedController<T extends Pressed> implements ReactiveContro
this.host._internals!.states.delete('pressed');
}

if (this.host.readonly) {
if (this.host.readOnly) {
this.host._internals!.ariaPressed = null;
this.host._internals!.states.delete('pressed');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { stateSelected } from '@nvidia-elements/core/internal';
@customElement('state-selected-controller-test-element')
class StateSelectedControllerTestElement extends LitElement {
@property({ type: Boolean }) selected: boolean;
@property({ type: Boolean }) readonly: boolean;
@property({ type: Boolean, attribute: 'readonly' }) readOnly = false;
declare _internals: ElementInternals;

render() {
Expand Down Expand Up @@ -70,7 +70,7 @@ describe('state-selected.controller', () => {
expect(element._internals.ariaSelected).toBe('true');
expect(element.matches(':state(selected)')).toBe(true);

element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaSelected).toBe(null);
expect(element.matches(':state(selected)')).toBe(false);
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('state-selected.controller', () => {
a.href = '#';
element.appendChild(a);
element.selected = true;
element.readonly = true;
element.readOnly = true;
element._internals.states.add('anchor');
element.requestUpdate();
await elementIsStable(element);
Expand All @@ -119,12 +119,12 @@ describe('state-selected.controller', () => {

it('should restore selected state when readonly is removed', async () => {
element.selected = true;
element.readonly = true;
element.readOnly = true;
await elementIsStable(element);
expect(element._internals.ariaSelected).toBe(null);
expect(element.matches(':state(selected)')).toBe(false);

element.readonly = false;
element.readOnly = false;
await elementIsStable(element);
expect(element._internals.ariaSelected).toBe('true');
expect(element.matches(':state(selected)')).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function stateSelected<T extends Selected>(): ClassDecorator {
target.addInitializer!((instance: T) => new StateSelectedController(instance));
}

export type Selected = ReactiveElement & { selected: boolean; readonly?: boolean; _internals?: ElementInternals };
export type Selected = ReactiveElement & { selected: boolean; readOnly?: boolean; _internals?: ElementInternals };

export class StateSelectedController<T extends Selected> implements ReactiveController {
constructor(private host: T) {
Expand All @@ -27,7 +27,7 @@ export class StateSelectedController<T extends Selected> implements ReactiveCont
}

hostUpdated() {
if (this.host.readonly) {
if (this.host.readOnly) {
this.host._internals!.ariaSelected = null;
this.host._internals!.states.delete('selected');
return;
Comment thread
coryrylan marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { createFixture, removeFixture, emulateClick, elementIsStable } from '@in
@customElement('type-anchor-test-element')
class TypeAnchorTestElement extends LitElement {
@property({ type: Boolean }) disabled = false;
@property({ type: Boolean }) readonly = false;
@property({ type: Boolean, attribute: 'readonly' }) readOnly = false;

declare _internals: ElementInternals;

Expand Down Expand Up @@ -51,7 +51,7 @@ describe('type-anchor.controller', () => {
element.disabled = true;
emulateClick(anchor);

expect(element.readonly).toBe(true);
expect(element.readOnly).toBe(true);
expect(clicks).toBe(1);

element.disabled = false;
Expand All @@ -66,7 +66,7 @@ describe('type-anchor.controller', () => {
emulateClick(anchor);
expect(clicks).toBe(1);

expect(element.readonly).toBe(true);
expect(element.readOnly).toBe(true);
expect(anchor.style.textDecoration).toBe('');
expect(element.style.cursor).toBe('');
expect(element.matches(':state(anchor)')).toBe(true);
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('type-anchor.controller wrapped element', () => {
element.disabled = true;
emulateClick(anchor);

expect(element.readonly).toBe(true);
expect(element.readOnly).toBe(true);
expect(clicks).toBe(1);

element.disabled = false;
Expand All @@ -117,7 +117,7 @@ describe('type-anchor.controller wrapped element', () => {
emulateClick(anchor);
expect(clicks).toBe(1);

expect(element.readonly).toBe(true);
expect(element.readOnly).toBe(true);
expect(anchor.style.textDecoration).toBe('none');
expect(element.style.cursor).toBe('pointer');
expect(element.matches(':state(anchor)')).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function typeAnchor<T extends Anchor>(): ClassDecorator {

export interface Anchor extends ReactiveElement {
disabled: boolean;
readonly: boolean;
readOnly: boolean;
_internals: ElementInternals;
}

Expand Down Expand Up @@ -55,7 +55,7 @@ export class TypeAnchorController<T extends Anchor> implements ReactiveControlle
this.#updateAnchorSlotAssignment();

if (this.#anchor) {
this.host.readonly = true;
this.host.readOnly = true;
this.host._internals?.states.add('anchor');
} else {
this.host._internals?.states.delete('anchor');
Expand Down
Loading