diff --git a/package-lock.json b/package-lock.json index 6e2f015f0..c5ac566e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@types/mocha": "^10.0.7", "@web/dev-server-esbuild": "0.4.3", "@web/test-runner": "0.18.2", + "@web/test-runner-commands": "^0.9.0", "@web/test-runner-playwright": "0.11.0", "autoprefixer": "10.4.17", "babel-loader": "9.1.3", diff --git a/package.json b/package.json index 036901162..146bdf3b6 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@types/mocha": "^10.0.7", "@web/dev-server-esbuild": "0.4.3", "@web/test-runner": "0.18.2", + "@web/test-runner-commands": "^0.9.0", "@web/test-runner-playwright": "0.11.0", "autoprefixer": "10.4.17", "babel-loader": "9.1.3", diff --git a/packages/uui-base/lib/mixins/SelectableMixin.ts b/packages/uui-base/lib/mixins/SelectableMixin.ts index af40771f9..2fa734f16 100644 --- a/packages/uui-base/lib/mixins/SelectableMixin.ts +++ b/packages/uui-base/lib/mixins/SelectableMixin.ts @@ -52,11 +52,16 @@ export const SelectableMixin = >( } set selectable(newVal) { const oldVal = this._selectable; + if (oldVal === newVal) return; this._selectable = newVal; + // Potentially problematic as a component might need focus for another feature when not selectable: - if (this.selectableTarget === this) { + if (this.#selectableTarget === this) { // If the selectable target, then make it self selectable. (A different selectable target should be made focusable by the component itself) - this.setAttribute('tabindex', `${newVal ? '0' : '-1'}`); + this.#selectableTarget.setAttribute( + 'tabindex', + `${newVal ? '0' : '-1'}`, + ); } this.requestUpdate('selectable', oldVal); } @@ -71,7 +76,31 @@ export const SelectableMixin = >( @property({ type: Boolean, reflect: true }) public selected = false; - protected selectableTarget: EventTarget = this; + #selectableTarget: Element = this; + protected get selectableTarget(): EventTarget { + return this.#selectableTarget; + } + protected set selectableTarget(target: EventTarget) { + const oldTarget = this.#selectableTarget; + + oldTarget.removeAttribute('tabindex'); + oldTarget.removeEventListener('click', this.#onClick); + oldTarget.removeEventListener( + 'keydown', + this.#onKeydown as EventListener, + ); + + this.#selectableTarget = target as Element; + if (this.#selectableTarget === this) { + // If the selectable target, then make it self selectable. (A different selectable target should be made focusable by the component itself) + this.#selectableTarget.setAttribute( + 'tabindex', + this._selectable ? '0' : '-1', + ); + } + target.addEventListener('click', this.#onClick); + target.addEventListener('keydown', this.#onKeydown as EventListener); + } constructor(...args: any[]) { super(...args); @@ -80,25 +109,41 @@ export const SelectableMixin = >( } readonly #onKeydown = (e: KeyboardEvent) => { - const composePath = e.composedPath(); - if ( - (this._selectable || (this.deselectable && this.selected)) && - composePath.indexOf(this.selectableTarget) === 0 - ) { - if (this.selectableTarget === this) { - if (e.code !== 'Space' && e.code !== 'Enter') return; - this.#toggleSelect(); - e.preventDefault(); - } + if (e.code !== 'Space' && e.code !== 'Enter') return; + if (e.composedPath().indexOf(this.#selectableTarget) === 0) { + this.#onClick(e); } }; readonly #onClick = (e: Event) => { + const isSelectable = + this._selectable || (this.deselectable && this.selected); + + if (isSelectable === false) return; + const composePath = e.composedPath(); - if ( - (this._selectable || (this.deselectable && this.selected)) && - composePath.indexOf(this.selectableTarget) === 0 - ) { + + if (this.#selectableTarget === this) { + // the selectableTarget is not specified which means we need to be selective about what we accept events from. + const isActionTag = composePath.some(el => { + const elementTagName = (el as HTMLElement).tagName; + return ( + elementTagName === 'A' || + elementTagName === 'BUTTON' || + elementTagName === 'INPUT' || + elementTagName === 'TEXTAREA' || + elementTagName === 'SELECT' + ); + }); + + // never select when clicking on a link or button + if (isActionTag) return; + } + + if (composePath.indexOf(this.#selectableTarget) !== -1) { + if (e.type === 'keydown') { + e.preventDefault(); // Do not want the space key to trigger a page scroll. + } this.#toggleSelect(); } }; diff --git a/packages/uui-card-media/lib/uui-card-media.story.ts b/packages/uui-card-media/lib/uui-card-media.story.ts index b4b3b4160..6da715196 100644 --- a/packages/uui-card-media/lib/uui-card-media.story.ts +++ b/packages/uui-card-media/lib/uui-card-media.story.ts @@ -64,6 +64,20 @@ export const Actions: Story = { }, }; +export const Href: Story = { + args: { + 'actions slot': html`Remove`, + selectable: true, + href: 'https://umbraco.com', + target: '_blank', + }, +}; + export const Image: Story = { args: { slot: html``, diff --git a/packages/uui-color-swatch/lib/uui-color-swatch.element.ts b/packages/uui-color-swatch/lib/uui-color-swatch.element.ts index 233cba458..aa52fa2f2 100644 --- a/packages/uui-color-swatch/lib/uui-color-swatch.element.ts +++ b/packages/uui-color-swatch/lib/uui-color-swatch.element.ts @@ -1,6 +1,7 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { property } from 'lit/decorators.js'; import { css, html, LitElement, nothing } from 'lit'; +import { ref } from 'lit/directives/ref.js'; import { iconCheck } from '@umbraco-ui/uui-icon-registry-essential/lib/svgs'; import { ActiveMixin, @@ -109,10 +110,19 @@ export class UUIColorSwatchElement extends LabelMixin( } } + focus(options?: FocusOptions | undefined): void { + (this.selectableTarget as HTMLElement | undefined)?.focus(options); + } + + #selectButtonChanged(button?: Element | undefined) { + this.selectableTarget = button || this; + } + render() { return html` ` : ''} - ${this.href ? this._renderLabelAsAnchor() : this._renderLabelAsButton()} + ${this.href && this.selectOnly !== true && this.selectable !== true + ? this._renderLabelAsAnchor() + : this._renderLabelAsButton()}
- ${this.selectOnly === false - ? html`` - : ''} + ${this.loading ? html`` : ''} diff --git a/packages/uui-menu-item/lib/uui-menu-item.story.ts b/packages/uui-menu-item/lib/uui-menu-item.story.ts index 525fca885..3c08cc08b 100644 --- a/packages/uui-menu-item/lib/uui-menu-item.story.ts +++ b/packages/uui-menu-item/lib/uui-menu-item.story.ts @@ -163,6 +163,12 @@ export const Selectable: Story = { selectable: true, }, }; +export const SelectOnly: Story = { + args: { + selectable: true, + selectOnly: true, + }, +}; export const Anchor: Story = { args: { diff --git a/packages/uui-menu-item/lib/uui-menu-item.test.ts b/packages/uui-menu-item/lib/uui-menu-item.test.ts index d86102763..dfc2aa15b 100644 --- a/packages/uui-menu-item/lib/uui-menu-item.test.ts +++ b/packages/uui-menu-item/lib/uui-menu-item.test.ts @@ -10,319 +10,391 @@ import '@umbraco-ui/uui-loader-bar/lib'; import { UUIMenuItemElement } from './uui-menu-item.element'; import { UUIMenuItemEvent } from './UUIMenuItemEvent'; import { UUISelectableEvent } from '@umbraco-ui/uui-base/lib/events'; +import { sendMouse } from '@web/test-runner-commands'; describe('UUIMenuItemElement', () => { - let element: UUIMenuItemElement; - - beforeEach(async () => { - element = await fixture( - html``, - ); - }); - - it('is defined', () => { - expect(element).to.be.instanceOf(UUIMenuItemElement); - }); - - it('passes the a11y audit', async () => { - await expect(element).shadowDom.to.be.accessible(); - }); - - it('passes the a11y audit with nesting', async () => { - element = await fixture( - html` - - - `, - ); - await expect(element).shadowDom.to.be.accessible(); - }); + describe('element', () => { + let element: UUIMenuItemElement; - describe('properties', () => { - it('has a disabled property', () => { - expect(element).to.have.property('disabled'); - }); - it('disable property defaults to false', () => { - expect(element.disabled).to.false; + beforeEach(async () => { + element = await fixture( + html``, + ); }); - it('has a showChildren property', () => { - expect(element).to.have.property('showChildren'); - }); - it('showChildren property defaults to false', () => { - expect(element.showChildren).to.false; + it('is defined', () => { + expect(element).to.be.instanceOf(UUIMenuItemElement); }); - it('has a hasChildren property', () => { - expect(element).to.have.property('hasChildren'); - }); - it('hasChildren property defaults to false', () => { - expect(element.hasChildren).to.false; + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(); }); - it('has a loading property', () => { - expect(element).to.have.property('loading'); - }); - it('loading property defaults to false', () => { - expect(element.loading).to.false; + it('passes the a11y audit with nesting', async () => { + element = await fixture( + html` + + + `, + ); + await expect(element).shadowDom.to.be.accessible(); }); - it('has a href property', () => { - expect(element).to.have.property('href'); - }); + describe('properties', () => { + it('has a disabled property', () => { + expect(element).to.have.property('disabled'); + }); + it('disable property defaults to false', () => { + expect(element.disabled).to.false; + }); - it('has a target property', () => { - expect(element).to.have.property('target'); - }); + it('has a showChildren property', () => { + expect(element).to.have.property('showChildren'); + }); + it('showChildren property defaults to false', () => { + expect(element.showChildren).to.false; + }); - it('has a rel property', () => { - expect(element).to.have.property('rel'); - }); + it('has a hasChildren property', () => { + expect(element).to.have.property('hasChildren'); + }); + it('hasChildren property defaults to false', () => { + expect(element.hasChildren).to.false; + }); - it('has a select-mode property', () => { - expect(element).to.have.property('selectMode'); - }); + it('has a loading property', () => { + expect(element).to.have.property('loading'); + }); + it('loading property defaults to false', () => { + expect(element.loading).to.false; + }); - it('select-mode property defaults to undefined', () => { - expect(element.selectMode).to.undefined; - }); - }); + it('has a href property', () => { + expect(element).to.have.property('href'); + }); + + it('has a target property', () => { + expect(element).to.have.property('target'); + }); + + it('has a rel property', () => { + expect(element).to.have.property('rel'); + }); - describe('events', () => { - it('emits a click-label event when button is clicked', async () => { - const listener = oneEvent(element, UUIMenuItemEvent.CLICK_LABEL, false); - - const buttonElement = element.shadowRoot!.querySelector( - 'button#label-button', - ) as HTMLButtonElement; - expect(buttonElement).to.exist; - buttonElement.click(); - - const event = await listener; - expect(event).to.exist; - expect(event.type).to.equal(UUIMenuItemEvent.CLICK_LABEL); - expect(event.bubbles).to.be.false; - expect(event.composed).to.be.false; - expect(event!.target).to.equal(element); + it('has a select-mode property', () => { + expect(element).to.have.property('selectMode'); + }); + + it('select-mode property defaults to undefined', () => { + expect(element.selectMode).to.undefined; + }); }); - describe('select', async () => { - it('emits a cancelable selected event when selectable', async () => { - element.selectable = true; - await elementUpdated(element); - const labelElement = element.shadowRoot!.querySelector( - '#label-button', - ) as HTMLElement; - element.addEventListener(UUISelectableEvent.SELECTED, e => { - e.preventDefault(); - }); - const listener = oneEvent(element, UUISelectableEvent.SELECTED, false); - labelElement.click(); + describe('events', () => { + it('emits a click-label event when button is clicked', async () => { + const listener = oneEvent(element, UUIMenuItemEvent.CLICK_LABEL); + + const buttonElement = element.shadowRoot!.querySelector( + 'button#label-button', + ) as HTMLButtonElement; + expect(buttonElement).to.exist; + buttonElement.click(); + const event = await listener; expect(event).to.exist; - expect(event.type).to.equal(UUISelectableEvent.SELECTED); - expect(element.selected).to.be.false; + expect(event.type).to.equal(UUIMenuItemEvent.CLICK_LABEL); + expect(event.bubbles).to.be.false; + expect(event.composed).to.be.false; + expect(event!.target).to.equal(element); }); - }); - describe('deselect', async () => { - it('emits a cancelable deselected event when preselected', async () => { - element.selectable = true; - element.selected = true; - await elementUpdated(element); - const labelElement = element.shadowRoot!.querySelector( - '#label-button', - ) as HTMLElement; - element.addEventListener(UUISelectableEvent.DESELECTED, e => { - e.preventDefault(); + describe('select', async () => { + it('emits a cancelable selected event when selectable', async () => { + element.selectable = true; + await elementUpdated(element); + const labelElement = element.shadowRoot!.querySelector( + '#label-button', + ) as HTMLElement; + element.addEventListener(UUISelectableEvent.SELECTED, e => { + e.preventDefault(); + }); + const listener = oneEvent(element, UUISelectableEvent.SELECTED); + labelElement.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUISelectableEvent.SELECTED); + expect(element.selected).to.be.false; }); - const listener = oneEvent( - element, - UUISelectableEvent.DESELECTED, - false, - ); - labelElement.click(); - const event = await listener; - expect(event).to.exist; - expect(event.type).to.equal(UUISelectableEvent.DESELECTED); - expect(element.selected).to.be.true; }); - }); - describe('show-children', async () => { - it('emits a show-children event when expanded', async () => { - element.hasChildren = true; - await elementUpdated(element); - const labelElement = element.shadowRoot!.querySelector( - '#caret-button', - ) as HTMLElement; - const listener = oneEvent( - element, - UUIMenuItemEvent.SHOW_CHILDREN, - false, - ); - labelElement.click(); - const event = await listener; - expect(event).to.exist; - expect(event.type).to.equal(UUIMenuItemEvent.SHOW_CHILDREN); - expect(element.showChildren).to.be.true; + describe('deselect', async () => { + it('emits a cancelable deselected event when preselected', async () => { + element.selectable = true; + element.selected = true; + await elementUpdated(element); + const labelElement = element.shadowRoot!.querySelector( + '#label-button', + ) as HTMLElement; + element.addEventListener(UUISelectableEvent.DESELECTED, e => { + e.preventDefault(); + }); + const listener = oneEvent(element, UUISelectableEvent.DESELECTED); + labelElement.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUISelectableEvent.DESELECTED); + expect(element.selected).to.be.true; + }); }); - it('emits a cancelable show-children event when expanded', async () => { - element.hasChildren = true; - await elementUpdated(element); - const labelElement = element.shadowRoot!.querySelector( - '#caret-button', - ) as HTMLElement; - element.addEventListener(UUIMenuItemEvent.SHOW_CHILDREN, e => { - e.preventDefault(); + + describe('show-children', async () => { + it('emits a show-children event when expanded', async () => { + element.hasChildren = true; + await elementUpdated(element); + const labelElement = element.shadowRoot!.querySelector( + '#caret-button', + ) as HTMLElement; + const listener = oneEvent(element, UUIMenuItemEvent.SHOW_CHILDREN); + labelElement.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUIMenuItemEvent.SHOW_CHILDREN); + expect(element.showChildren).to.be.true; }); - const listener = oneEvent( - element, - UUIMenuItemEvent.SHOW_CHILDREN, - false, - ); - labelElement.click(); - const event = await listener; - expect(event).to.exist; - expect(event.type).to.equal(UUIMenuItemEvent.SHOW_CHILDREN); - expect(element.showChildren).to.be.false; + it('emits a cancelable show-children event when expanded', async () => { + element.hasChildren = true; + await elementUpdated(element); + const labelElement = element.shadowRoot!.querySelector( + '#caret-button', + ) as HTMLElement; + element.addEventListener(UUIMenuItemEvent.SHOW_CHILDREN, e => { + e.preventDefault(); + }); + const listener = oneEvent(element, UUIMenuItemEvent.SHOW_CHILDREN); + labelElement.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUIMenuItemEvent.SHOW_CHILDREN); + expect(element.showChildren).to.be.false; + }); + }); + + describe('hide-children', async () => { + it('emits a hide-children event when collapsed', async () => { + element.hasChildren = true; + element.showChildren = true; + await elementUpdated(element); + const labelElement = element.shadowRoot!.querySelector( + '#caret-button', + ) as HTMLElement; + const listener = oneEvent(element, UUIMenuItemEvent.HIDE_CHILDREN); + labelElement.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUIMenuItemEvent.HIDE_CHILDREN); + expect(element.showChildren).to.be.false; + }); + it('emits a cancelable hide-children event when collapsed', async () => { + element.hasChildren = true; + element.showChildren = true; + await elementUpdated(element); + const labelElement = element.shadowRoot!.querySelector( + '#caret-button', + ) as HTMLElement; + element.addEventListener(UUIMenuItemEvent.HIDE_CHILDREN, e => { + e.preventDefault(); + }); + const listener = oneEvent(element, UUIMenuItemEvent.HIDE_CHILDREN); + labelElement.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal(UUIMenuItemEvent.HIDE_CHILDREN); + expect(element.showChildren).to.be.true; + }); + }); + }); + + describe('template', () => { + it('renders a default slot', () => { + const slot = element.shadowRoot!.querySelector('slot')!; + expect(slot).to.exist; + }); + + it('renders an icon slot', () => { + const slot = element.shadowRoot!.querySelector('slot[name=icon]')!; + expect(slot).to.exist; + }); + + it('renders an actions slot', () => { + const slot = element.shadowRoot!.querySelector('slot[name=actions]')!; + expect(slot).to.exist; + }); + + it('renders a button', () => { + const slot = element.shadowRoot!.querySelector('button')!; + expect(slot).to.exist; + }); + it('renders a anchor tag when href is defined', () => { + element.setAttribute('href', 'https://www.umbraco.com'); + const slot = element.shadowRoot!.querySelector('button')!; + expect(slot).to.exist; }); }); - describe('hide-children', async () => { - it('emits a hide-children event when collapsed', async () => { - element.hasChildren = true; - element.showChildren = true; + describe('expand', () => { + it('emits a show-children event when expand icon is clicked', async () => { + element.setAttribute('has-children', 'true'); await elementUpdated(element); - const labelElement = element.shadowRoot!.querySelector( - '#caret-button', - ) as HTMLElement; - const listener = oneEvent( - element, - UUIMenuItemEvent.HIDE_CHILDREN, - false, - ); - labelElement.click(); + const listener = oneEvent(element, 'show-children'); + const caretIconElement: HTMLElement | null = + element.shadowRoot!.querySelector('#caret-button'); + caretIconElement?.click(); const event = await listener; expect(event).to.exist; - expect(event.type).to.equal(UUIMenuItemEvent.HIDE_CHILDREN); - expect(element.showChildren).to.be.false; + expect(event.type).to.equal('show-children'); + expect(element.hasAttribute('show-children')).to.equal(true); }); - it('emits a cancelable hide-children event when collapsed', async () => { - element.hasChildren = true; - element.showChildren = true; + + it('emits a hide-children event when collapse icon is clicked', async () => { + element.setAttribute('has-children', 'true'); + element.setAttribute('show-children', 'true'); await elementUpdated(element); - const labelElement = element.shadowRoot!.querySelector( - '#caret-button', - ) as HTMLElement; - element.addEventListener(UUIMenuItemEvent.HIDE_CHILDREN, e => { - e.preventDefault(); - }); - const listener = oneEvent( - element, - UUIMenuItemEvent.HIDE_CHILDREN, - false, - ); - labelElement.click(); + const listener = oneEvent(element, 'hide-children'); + const caretIconElement: HTMLElement | null = + element.shadowRoot!.querySelector('#caret-button'); + caretIconElement?.click(); const event = await listener; expect(event).to.exist; - expect(event.type).to.equal(UUIMenuItemEvent.HIDE_CHILDREN); - expect(element.showChildren).to.be.true; + expect(event.type).to.equal('hide-children'); + expect(element.hasAttribute('show-children')).to.equal(false); }); }); - }); - describe('template', () => { - it('renders a default slot', () => { - const slot = element.shadowRoot!.querySelector('slot')!; - expect(slot).to.exist; - }); + describe('selectable', () => { + let labelElement: HTMLElement | null; - it('renders an icon slot', () => { - const slot = element.shadowRoot!.querySelector('slot[name=icon]')!; - expect(slot).to.exist; - }); + beforeEach(async () => { + labelElement = element.shadowRoot!.querySelector('#label-button'); + element.selectable = true; + }); - it('renders an actions slot', () => { - const slot = element.shadowRoot!.querySelector('slot[name=actions]')!; - expect(slot).to.exist; - }); + it('label element is defined', () => { + expect(labelElement).to.be.instanceOf(HTMLElement); + }); - it('renders a button', () => { - const slot = element.shadowRoot!.querySelector('button')!; - expect(slot).to.exist; - }); - it('renders a anchor tag when href is defined', () => { - element.setAttribute('href', 'https://www.umbraco.com'); - const slot = element.shadowRoot!.querySelector('button')!; - expect(slot).to.exist; - }); - }); + it('label is rendered as a button tag', async () => { + await elementUpdated(element); + expect(labelElement?.nodeName).to.be.equal('BUTTON'); + }); - describe('expand', () => { - it('emits a show-children event when expand icon is clicked', async () => { - element.setAttribute('has-children', 'true'); - await elementUpdated(element); - const listener = oneEvent(element, 'show-children', false); - const caretIconElement: HTMLElement | null = - element.shadowRoot!.querySelector('#caret-button'); - caretIconElement?.click(); - const event = await listener; - expect(event).to.exist; - expect(event.type).to.equal('show-children'); - expect(element.hasAttribute('show-children')).to.equal(true); - }); + it('can be selected when selectable', async () => { + await elementUpdated(element); + await sendMouse({ + type: 'click', + position: [75, 30], + button: 'left', + }); + expect(element.selected).to.be.true; + }); - it('emits a hide-children event when collapse icon is clicked', async () => { - element.setAttribute('has-children', 'true'); - element.setAttribute('show-children', 'true'); - await elementUpdated(element); - const listener = oneEvent(element, 'hide-children', false); - const caretIconElement: HTMLElement | null = - element.shadowRoot!.querySelector('#caret-button'); - caretIconElement?.click(); - const event = await listener; - expect(event).to.exist; - expect(event.type).to.equal('hide-children'); - expect(element.hasAttribute('show-children')).to.equal(false); - }); - }); + it('can not be selected when not selectable', async () => { + element.selectable = false; + await elementUpdated(element); + await sendMouse({ + type: 'click', + position: [75, 30], + button: 'left', + }); + expect(element.selected).to.be.false; + }); - describe('selectable', () => { - let labelElement: HTMLElement | null; + it('can not be selected when disabled', async () => { + element.disabled = true; + await elementUpdated(element); + await sendMouse({ + type: 'click', + position: [75, 30], + button: 'left', + }); + expect(element.selected).to.be.false; + }); - beforeEach(async () => { - labelElement = element.shadowRoot!.querySelector('#label-button'); - element.selectable = true; + it('can expand', async () => { + element.setAttribute('has-children', 'true'); + await elementUpdated(element); + const listener = oneEvent(element, 'show-children'); + const caretIconElement: HTMLElement | null = + element.shadowRoot!.querySelector('#caret-button'); + caretIconElement?.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal('show-children'); + expect(element.hasAttribute('show-children')).to.equal(true); + }); }); - it('label element is defined', () => { - expect(labelElement).to.be.instanceOf(HTMLElement); - }); + describe('selectable & selectOnly', () => { + let labelElement: HTMLElement | null; - it('label is rendered as a button tag', async () => { - await elementUpdated(element); - expect(labelElement?.nodeName).to.be.equal('BUTTON'); - }); + beforeEach(async () => { + labelElement = element.shadowRoot!.querySelector('#label-button'); + element.selectable = true; + element.selectOnly = true; + }); - it('can be selected when selectable', async () => { - await elementUpdated(element); - labelElement?.click(); - expect(element.selected).to.be.true; - }); + it('label element is defined', () => { + expect(labelElement).to.be.instanceOf(HTMLElement); + }); - it('can not be selected when not selectable', async () => { - element.selectable = false; - await elementUpdated(element); - labelElement?.click(); - expect(element.selected).to.be.false; - }); + it('label is rendered as a button tag', async () => { + await elementUpdated(element); + expect(labelElement?.nodeName).to.be.equal('BUTTON'); + }); - it('can be selected when selectable', async () => { - element.disabled = true; - await elementUpdated(element); - labelElement?.click(); - expect(element.selected).to.be.false; + it('can be selected when selectable', async () => { + await elementUpdated(element); + await sendMouse({ + type: 'click', + position: [75, 30], + button: 'left', + }); + expect(element.selected).to.be.true; + }); + + it('can not be selected when not selectable', async () => { + element.selectable = false; + await elementUpdated(element); + await sendMouse({ + type: 'click', + position: [75, 30], + button: 'left', + }); + expect(element.selected).to.be.false; + }); + + it('can not be selected when disabled', async () => { + element.disabled = true; + await elementUpdated(element); + await sendMouse({ + type: 'click', + position: [75, 30], + button: 'left', + }); + expect(element.selected).to.be.false; + }); + + it('can expand', async () => { + element.setAttribute('has-children', 'true'); + await elementUpdated(element); + const listener = oneEvent(element, 'show-children'); + const caretIconElement: HTMLElement | null = + element.shadowRoot!.querySelector('#caret-button'); + caretIconElement?.click(); + const event = await listener; + expect(event).to.exist; + expect(event.type).to.equal('show-children'); + expect(element.hasAttribute('show-children')).to.equal(true); + }); }); }); diff --git a/packages/uui-ref-node/lib/uui-ref-node.story.ts b/packages/uui-ref-node/lib/uui-ref-node.story.ts index ab9cf2d9c..fa0d7674a 100644 --- a/packages/uui-ref-node/lib/uui-ref-node.story.ts +++ b/packages/uui-ref-node/lib/uui-ref-node.story.ts @@ -16,7 +16,6 @@ const meta: Meta = { args: { name: 'Rabbit Suit Product Page', detail: 'path/to/nowhere', - href: 'umbraco.com', }, render: args => html`${renderSlots(args)}`, @@ -56,6 +55,13 @@ export const Standalone: Story = { }, }; +export const Href: Story = { + args: { + href: 'https://umbraco.com', + target: '_blank', + }, +}; + export const Selectable: Story = { args: { selectable: true, diff --git a/packages/uui-table/lib/uui-table-advanced-example.ts b/packages/uui-table/lib/uui-table-advanced-example.ts index b48336efb..a53442e68 100644 --- a/packages/uui-table/lib/uui-table-advanced-example.ts +++ b/packages/uui-table/lib/uui-table-advanced-example.ts @@ -2,6 +2,7 @@ import '.'; import '@umbraco-ui/uui-avatar/lib'; import '@umbraco-ui/uui-box/lib'; import '@umbraco-ui/uui-button/lib'; +import '@umbraco-ui/uui-checkbox/lib'; import '@umbraco-ui/uui-icon/lib'; import '@umbraco-ui/uui-progress-bar/lib'; import '@umbraco-ui/uui-tag/lib'; diff --git a/packages/uui-table/lib/uui-table-row.element.ts b/packages/uui-table/lib/uui-table-row.element.ts index 0c34a530f..40458296d 100644 --- a/packages/uui-table/lib/uui-table-row.element.ts +++ b/packages/uui-table/lib/uui-table-row.element.ts @@ -6,8 +6,6 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { css, html, LitElement } from 'lit'; import { queryAssignedElements } from 'lit/decorators.js'; -import { UUITableCellElement } from './uui-table-cell.element'; - /** * Table row element with option to set is as selectable. Parent for uui-table-cell. Must be a child of uui-table. * @element uui-table-row @@ -50,23 +48,21 @@ export class UUITableRowElement extends SelectOnlyMixin( private slotCellNodes?: unknown[]; protected updated(changedProperties: Map) { - if (changedProperties.has('selectOnly')) this.updateChildSelectOnly(); + if (changedProperties.has('selectOnly')) { + this.updateChildSelectOnly(); + } } private updateChildSelectOnly() { if (this.slotCellNodes) { - this.slotCellNodes.forEach(el => { - if (this.elementIsTableCell(el)) { + this.slotCellNodes.forEach((el: any) => { + if (el.disableChildInteraction !== undefined) { el.disableChildInteraction = this.selectOnly; } }); } } - private elementIsTableCell(element: unknown): element is UUITableCellElement { - return element instanceof UUITableCellElement; - } - render() { return html` `; } diff --git a/packages/uui-table/lib/uui-table-row.story.ts b/packages/uui-table/lib/uui-table-row.story.ts index 96b9b26bf..66456ace4 100644 --- a/packages/uui-table/lib/uui-table-row.story.ts +++ b/packages/uui-table/lib/uui-table-row.story.ts @@ -4,6 +4,10 @@ import { html } from 'lit'; import type { Meta, StoryObj } from '@storybook/web-components'; import { ArrayOfUmbracoWords } from '../../../storyhelpers/UmbracoWordGenerator'; +import '@umbraco-ui/uui-table'; +import '@umbraco-ui/uui-input'; +import '@umbraco-ui/uui-button'; + const meta: Meta = { id: 'uui-table-row', component: 'uui-table-row', @@ -12,18 +16,24 @@ const meta: Meta = { + ?select-only=${args.selectOnly}> ${ArrayOfUmbracoWords(5).map( el => html`${el}`, )} + ?select-only=${args.selectOnly}> - ${ArrayOfUmbracoWords(5).map( + + + + + Link + + ${ArrayOfUmbracoWords(3).map( el => html`${el}`, )} diff --git a/packages/uui-table/lib/uui-table-row.test.ts b/packages/uui-table/lib/uui-table-row.test.ts index 856dd8e13..55382f915 100644 --- a/packages/uui-table/lib/uui-table-row.test.ts +++ b/packages/uui-table/lib/uui-table-row.test.ts @@ -6,7 +6,7 @@ import { oneEvent, } from '@open-wc/testing'; -import './uui-table.element'; +import '.'; import { UUITableRowElement } from './uui-table-row.element'; describe('UuiTableRow', () => { @@ -51,7 +51,7 @@ describe('UuiTableRow', () => { it('emits a selected event when selectable', async () => { element.selectable = true; await elementUpdated(element); - const listener = oneEvent(element, 'selected', false); + const listener = oneEvent(element, 'selected'); element.click(); const event = await listener; expect(event).to.exist;