Skip to content

Commit

Permalink
Merge a24487c into 7d93d8e
Browse files Browse the repository at this point in the history
  • Loading branch information
rkaraivanov committed May 13, 2024
2 parents 7d93d8e + a24487c commit c5c0a6a
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 44 deletions.
24 changes: 3 additions & 21 deletions src/components/checkbox/checkbox-base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LitElement } from 'lit';
import { property, query, queryAssignedNodes, state } from 'lit/decorators.js';

import { createFocusRing } from '../common/controllers/focus-ring.js';
import { alternateName } from '../common/decorators/alternateName.js';
import { blazorDeepImport } from '../common/decorators/blazorDeepImport.js';
import { blazorTwoWayBind } from '../common/decorators/blazorTwoWayBind.js';
Expand All @@ -25,6 +26,7 @@ export class IgcCheckboxBaseComponent extends FormAssociatedRequiredMixin(
) {
protected override validators: Validator<this>[] = [requiredBooleanValidator];

protected _focusManager = createFocusRing(this);
protected _value!: string;
protected _checked = false;

Expand All @@ -34,9 +36,6 @@ export class IgcCheckboxBaseComponent extends FormAssociatedRequiredMixin(
@queryAssignedNodes({ flatten: true })
protected label!: Array<Node>;

@state()
protected focused = false;

@state()
protected hideLabel = false;

Expand Down Expand Up @@ -83,11 +82,6 @@ export class IgcCheckboxBaseComponent extends FormAssociatedRequiredMixin(
@property({ reflect: true, attribute: 'label-position' })
public labelPosition: 'before' | 'after' = 'after';

constructor() {
super();
this.addEventListener('keyup', this.handleKeyUp);
}

public override connectedCallback() {
super.connectedCallback();
this.updateValidity();
Expand Down Expand Up @@ -123,26 +117,14 @@ export class IgcCheckboxBaseComponent extends FormAssociatedRequiredMixin(

protected handleBlur() {
this.emitEvent('igcBlur');
this.focused = false;
this._focusManager.reset();
}

protected handleFocus() {
this._dirty = true;
this.emitEvent('igcFocus');
}

protected handleMouseDown(event: PointerEvent) {
event.preventDefault();
this.input.focus();
this.focused = false;
}

protected handleKeyUp() {
if (!this.focused) {
this.focused = true;
}
}

protected handleSlotChange() {
this.hideLabel = this.label.length < 1;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ export default class IgcCheckboxComponent extends IgcCheckboxBaseComponent {
part=${partNameMap({
base: true,
checked: this.checked,
focused: this.focused,
focused: this._focusManager.focused,
})}
for=${this.inputId}
@pointerdown=${this.handleMouseDown}
@pointerdown=${this._focusManager.reset}
>
<input
id=${this.inputId}
Expand Down
4 changes: 2 additions & 2 deletions src/components/checkbox/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default class IgcSwitchComponent extends IgcCheckboxBaseComponent {
<label
part=${partNameMap({ base: true, checked: this.checked })}
for=${this.inputId}
@pointerdown=${this.handleMouseDown}
@pointerdown=${this._focusManager.reset}
>
<input
id=${this.inputId}
Expand All @@ -69,7 +69,7 @@ export default class IgcSwitchComponent extends IgcCheckboxBaseComponent {
part=${partNameMap({
control: true,
checked: this.checked,
focused: this.focused,
focused: this._focusManager.focused,
})}
>
<span
Expand Down
107 changes: 107 additions & 0 deletions src/components/common/controllers/focus-ring.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
defineCE,
elementUpdated,
expect,
fixture,
html,
unsafeStatic,
} from '@open-wc/testing';
import { LitElement, css } from 'lit';
import { partNameMap } from '../util.js';
import {
simulateClick,
simulateKeyboard,
simulatePointerDown,
} from '../utils.spec.js';
import { createFocusRing } from './focus-ring.js';
import { tabKey } from './key-bindings.js';

describe('Focus ring controller', () => {
let tag: string;
let instance: LitElement & { button: HTMLButtonElement };

before(() => {
tag = defineCE(
class extends LitElement {
public static override styles = css`
[part='focused'] {
outline: 2px solid red;
}
`;

public manager = createFocusRing(this);

public get button() {
return this.renderRoot.querySelector('button')!;
}

protected override render() {
return html`<button
part=${partNameMap({ focused: this.manager.focused })}
@blur=${this.manager.reset}
@pointerdown=${this.manager.reset}
>
Button
</button>`;
}
}
);
});

beforeEach(async () => {
const tagName = unsafeStatic(tag);
instance = await fixture(html`<${tagName}></${tagName}`);
});

it('should apply keyboard focus styles on keyup', async () => {
simulateKeyboard(instance.button, tabKey);
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.true;
});

it('should not apply keyboard focus styles on click', async () => {
simulateClick(instance.button);
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.false;
});

it('should not apply keyboard focus styles on focus', async () => {
instance.button.focus();
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.false;
});

it('it should remove keyboard focus styles on blur', async () => {
simulateKeyboard(instance.button, tabKey);
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.true;

instance.button.dispatchEvent(new Event('blur'));
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.false;
});

it('should remove keyboard focus styles on subsequent pointer events', async () => {
simulateKeyboard(instance.button, tabKey);
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.true;

simulatePointerDown(instance.button);
await elementUpdated(instance);

expect(hasKeyboardFocusStyles(instance.button)).to.be.false;
});
});

function hasKeyboardFocusStyles(node: HTMLElement) {
return (
getComputedStyle(node).getPropertyValue('outline') ===
'rgb(255, 0, 0) solid 2px'
);
}
40 changes: 40 additions & 0 deletions src/components/common/controllers/focus-ring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ReactiveController, ReactiveControllerHost } from 'lit';

/* blazorSuppress */
export class FocusRingController implements ReactiveController {
private readonly _host: ReactiveControllerHost & HTMLElement;
private _focused = false;

public get focused() {
return this._focused;
}

constructor(readonly host: ReactiveControllerHost & HTMLElement) {
this._host = host;
host.addController(this);
}

public hostConnected() {
this._host.addEventListener('keyup', this);
}

public hostDisconnected() {
this._host.removeEventListener('keyup', this);
}

public handleEvent() {
if (!this._focused) {
this._focused = true;
}
this._host.requestUpdate();
}

public reset = () => {
this._focused = false;
this._host.requestUpdate();
};
}

export function createFocusRing(host: ReactiveControllerHost & HTMLElement) {
return new FocusRingController(host);
}
24 changes: 5 additions & 19 deletions src/components/radio/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';

import { themes } from '../../theming/theming-decorator.js';
import { createFocusRing } from '../common/controllers/focus-ring.js';
import {
addKeybindings,
arrowDown,
Expand Down Expand Up @@ -74,6 +75,7 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(

private inputId = `radio-${IgcRadioComponent.increment()}`;
private labelId = `radio-label-${this.inputId}`;
private _focusManager = createFocusRing(this);

protected _checked = false;
protected _value!: string;
Expand All @@ -87,9 +89,6 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
@state()
private _tabIndex = 0;

@state()
private focused = false;

@state()
protected hideLabel = false;

Expand Down Expand Up @@ -144,7 +143,6 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(

constructor() {
super();
this.addEventListener('keyup', this.handleKeyUp);

addKeybindings(this, {
skip: () => this.disabled,
Expand Down Expand Up @@ -237,32 +235,20 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
}
}

protected handleMouseDown(event: PointerEvent) {
event.preventDefault();
this.input.focus();
this.focused = false;
}

protected handleClick() {
this.checked = true;
this.emitEvent('igcChange', { detail: this.checked });
}

protected handleBlur() {
this.emitEvent('igcBlur');
this.focused = false;
this._focusManager.reset();
}

protected handleFocus() {
this.emitEvent('igcFocus');
}

protected handleKeyUp() {
if (!this.focused) {
this.focused = true;
}
}

protected navigate(idx: number) {
const { active } = this.group;
const nextIdx = wrap(0, active.length - 1, active.indexOf(this) + idx);
Expand All @@ -285,10 +271,10 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
part=${partNameMap({
base: true,
checked: this.checked,
focused: this.focused,
focused: this._focusManager.focused,
})}
for=${this.inputId}
@pointerdown=${this.handleMouseDown}
@pointerdown=${this._focusManager.reset}
>
<input
id=${this.inputId}
Expand Down

0 comments on commit c5c0a6a

Please sign in to comment.