diff --git a/packages/core/index.js b/packages/core/index.js index 06781fe61..8a418776c 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -52,3 +52,4 @@ export { DomHelpersMixin } from './src/DomHelpersMixin.js'; export { LionSingleton } from './src/LionSingleton.js'; export { SlotMixin } from './src/SlotMixin.js'; export { DisabledMixin } from './src/DisabledMixin.js'; +export { DisabledWithTabIndexMixin } from './src/DisabledWithTabIndexMixin.js'; diff --git a/packages/core/src/DisabledWithTabIndexMixin.js b/packages/core/src/DisabledWithTabIndexMixin.js new file mode 100644 index 000000000..ac9189fab --- /dev/null +++ b/packages/core/src/DisabledWithTabIndexMixin.js @@ -0,0 +1,86 @@ +import { dedupeMixin } from './dedupeMixin.js'; +import { DisabledMixin } from './DisabledMixin.js'; + +/** + * #DisabledWithTabIndexMixin + * + * @polymerMixin + * @mixinFunction + */ +export const DisabledWithTabIndexMixin = dedupeMixin( + superclass => + // eslint-disable-next-line no-shadow + class DisabledWithTabIndexMixin extends DisabledMixin(superclass) { + static get properties() { + return { + // we use a property here as if we use the native tabIndex we can not set a default value + // in the constructor as it synchronously sets the attribute which is not allowed in the + // constructor phase + tabIndex: { + type: Number, + reflect: true, + attribute: 'tabindex', + }, + }; + } + + constructor() { + super(); + this.__isUserSettingTabIndex = true; + + this.__restoreTabIndexTo = 0; + this.__internalSetTabIndex(0); + } + + makeRequestToBeDisabled() { + super.makeRequestToBeDisabled(); + if (this.__requestedToBeDisabled === false) { + this.__restoreTabIndexTo = this.tabIndex; + } + } + + retractRequestToBeDisabled() { + super.retractRequestToBeDisabled(); + if (this.__requestedToBeDisabled === true) { + this.__internalSetTabIndex(this.__restoreTabIndexTo); + } + } + + __internalSetTabIndex(value) { + this.__isUserSettingTabIndex = false; + this.tabIndex = value; + this.__isUserSettingTabIndex = true; + } + + _requestUpdate(name, oldValue) { + super._requestUpdate(name, oldValue); + + if (name === 'disabled') { + if (this.disabled) { + this.__internalSetTabIndex(-1); + } else { + this.__internalSetTabIndex(this.__restoreTabIndexTo); + } + } + + if (name === 'tabIndex') { + if (this.__isUserSettingTabIndex) { + this.__restoreTabIndexTo = this.tabIndex; + } + + if (this.tabIndex !== -1 && this.__requestedToBeDisabled === true) { + this.__internalSetTabIndex(-1); + } + } + } + + firstUpdated(changedProperties) { + super.firstUpdated(changedProperties); + // for ShadyDom the timing is a little different and we need to make sure + // the tabindex gets correctly updated here + if (this.disabled) { + this.__internalSetTabIndex(-1); + } + } + }, +); diff --git a/packages/core/test/DisabledWithTabIndexMixin.test.js b/packages/core/test/DisabledWithTabIndexMixin.test.js new file mode 100644 index 000000000..3a037f8bc --- /dev/null +++ b/packages/core/test/DisabledWithTabIndexMixin.test.js @@ -0,0 +1,107 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import { LitElement } from '../index.js'; +import { DisabledWithTabIndexMixin } from '../src/DisabledWithTabIndexMixin.js'; + +describe('DisabledWithTabIndexMixin', () => { + before(() => { + class WithTabIndex extends DisabledWithTabIndexMixin(LitElement) {} + customElements.define('can-be-disabled-with-tab-index', WithTabIndex); + }); + + it('has an initial tabIndex of 0', async () => { + const el = await fixture(html` + + `); + expect(el.tabIndex).to.equal(0); + expect(el.getAttribute('tabindex')).to.equal('0'); + }); + + it('sets tabIndex to -1 if disabled', async () => { + const el = await fixture(html` + + `); + el.disabled = true; + expect(el.tabIndex).to.equal(-1); + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('-1'); + }); + + it('disabled does not override user provided tabindex', async () => { + const el = await fixture(html` + + `); + expect(el.getAttribute('tabindex')).to.equal('-1'); + el.disabled = false; + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('5'); + }); + + it('can be disabled imperatively', async () => { + const el = await fixture(html` + + `); + expect(el.getAttribute('tabindex')).to.equal('-1'); + + el.disabled = false; + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('0'); + expect(el.hasAttribute('disabled')).to.equal(false); + + el.disabled = true; + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('-1'); + expect(el.hasAttribute('disabled')).to.equal(true); + }); + + it('will not allow to change tabIndex after makeRequestToBeDisabled()', async () => { + const el = await fixture(html` + + `); + el.makeRequestToBeDisabled(); + + el.tabIndex = 5; + expect(el.tabIndex).to.equal(-1); + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('-1'); + }); + + it('will restore last tabIndex after retractRequestToBeDisabled()', async () => { + const el = await fixture(html` + + `); + el.makeRequestToBeDisabled(); + expect(el.tabIndex).to.equal(-1); + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('-1'); + el.retractRequestToBeDisabled(); + expect(el.tabIndex).to.equal(5); + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('5'); + + el.makeRequestToBeDisabled(); + el.tabIndex = 12; + el.retractRequestToBeDisabled(); + expect(el.tabIndex).to.equal(12); + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('12'); + + el.makeRequestToBeDisabled(); + el.tabIndex = 13; + el.tabIndex = 14; + el.retractRequestToBeDisabled(); + expect(el.tabIndex).to.equal(14); + await el.updateComplete; + expect(el.getAttribute('tabindex')).to.equal('14'); + }); + + it('may allow multiple calls to retractRequestToBeDisabled', async () => { + const el = await fixture(html` + + `); + el.retractRequestToBeDisabled(); + el.retractRequestToBeDisabled(); + expect(el.disabled).to.be.true; + expect(el.tabIndex).to.be.equal(-1); + }); +});