diff --git a/docs/pages/frameworks/react.md b/docs/pages/frameworks/react.md index b87cb9c7..65f37484 100644 --- a/docs/pages/frameworks/react.md +++ b/docs/pages/frameworks/react.md @@ -21,7 +21,7 @@ Next, [include a theme](/getting-started/themes) and set the [base path](/gettin ```jsx // App.jsx import '@shoelace-style/shoelace/%NPMDIR%/themes/light.css'; -import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path'; +import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path.js'; setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/%CDNDIR%/'); ``` @@ -43,7 +43,7 @@ Preact users facing type errors using components may benefit from setting "paths Every Shoelace component is available to import as a React component. Note that we're importing the `` _React component_ instead of the `` _custom element_ in the example below. ```jsx -import SlButton from '@shoelace-style/shoelace/%NPMDIR%/react/button'; +import SlButton from '@shoelace-style/shoelace/%NPMDIR%/react/button/index.js'; const MyComponent = () => Click me; @@ -62,7 +62,7 @@ However, tree-shaking extra Shoelace components proved to be a challenge. As a r ```diff - import { SlButton } from '@shoelace-style/shoelace/%NPMDIR%/react'; -+ import SlButton from '@shoelace-style/shoelace/%NPMDIR%/react/button'; ++ import SlButton from '@shoelace-style/shoelace/%NPMDIR%/react/button/index.js'; ``` You can find a copy + paste import for each component in the "importing" section of its documentation. @@ -75,7 +75,7 @@ Here's how you can bind the input's value to a state variable. ```jsx import { useState } from 'react'; -import SlInput from '@shoelace-style/shoelace/%NPMDIR%/react/input'; +import SlInput from '@shoelace-style/shoelace/%NPMDIR%/react/input/index.js'; function MyComponent() { const [value, setValue] = useState(''); @@ -90,8 +90,8 @@ If you're using TypeScript, it's important to note that `event.target` will be a ```tsx import { useState } from 'react'; -import SlInput from '@shoelace-style/shoelace/%NPMDIR%/react/input'; -import type SlInputElement from '@shoelace-style/shoelace/%NPMDIR%/components/input/input'; +import SlInput from '@shoelace-style/shoelace/%NPMDIR%/react/input/index.js'; +import type SlInputElement from '@shoelace-style/shoelace/%NPMDIR%/components/input/input.js'; function MyComponent() { const [value, setValue] = useState(''); @@ -106,8 +106,8 @@ You can also import the event type for use in your callbacks, shown below. ```tsx import { useCallback, useState } from 'react'; -import SlInput, { type SlInputEvent } from '@shoelace-style/shoelace/%NPMDIR%/react/input'; -import type SlInputElement from '@shoelace-style/shoelace/%NPMDIR%/components/input/input'; +import SlInput, { type SlInputEvent } from '@shoelace-style/shoelace/%NPMDIR%/react/input/index.js'; +import type SlInputElement from '@shoelace-style/shoelace/%NPMDIR%/components/input/input.js'; function MyComponent() { const [value, setValue] = useState(''); diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index e4dab556..8c0f3b93 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -12,6 +12,15 @@ Components with the Experimental bad New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style). +## 2.20.1 + +- Fixed a bug that prevented `` to be activated properly when rendered in another `` [#2367] +- Fixed a bug that in `` that prevented tab from working properly in some cases [#2371] +- Fixed the guard on popover to allow virtual elements [#2399] +- Fixed the close button in `` so clicking above/below it doesn't inadvertently close it [#2375] +- Fixed accessibility issues for elements that are closed while having slotted focused children. [#2383] +- Improved accessibility of `` [#2364] + ## 2.20.0 - Added the ability to set a custom snap function and use `repeat(n)` to `` [#2340] diff --git a/package-lock.json b/package-lock.json index 8a9c09bf..e0a215c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shoelace-style/shoelace", - "version": "2.20.0", + "version": "2.20.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shoelace-style/shoelace", - "version": "2.20.0", + "version": "2.20.1", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.1.0", diff --git a/package.json b/package.json index 3f424796..47f26e79 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@shoelace-style/shoelace", "description": "A forward-thinking library of web components.", - "version": "2.20.0", + "version": "2.20.1", "homepage": "https://github.com/shoelace-style/shoelace", "author": "Cory LaViska", "license": "MIT", diff --git a/src/components/alert/alert.component.ts b/src/components/alert/alert.component.ts index 2f2ffbd9..7b442cd6 100644 --- a/src/components/alert/alert.component.ts +++ b/src/components/alert/alert.component.ts @@ -1,4 +1,5 @@ import { animateTo, stopAnimations } from '../../internal/animate.js'; +import { blurActiveElement } from '../../internal/closeActiveElement.js'; import { classMap } from 'lit/directives/class-map.js'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js'; import { HasSlotController } from '../../internal/slot.js'; @@ -157,6 +158,7 @@ export default class SlAlert extends ShoelaceElement { this.emit('sl-after-show'); } else { // Hide + blurActiveElement(this); this.emit('sl-hide'); clearTimeout(this.autoHideTimeout); diff --git a/src/components/alert/alert.styles.ts b/src/components/alert/alert.styles.ts index 20a1729a..d7e0c756 100644 --- a/src/components/alert/alert.styles.ts +++ b/src/components/alert/alert.styles.ts @@ -94,7 +94,8 @@ export default css` display: flex; align-items: center; font-size: var(--sl-font-size-medium); - padding-inline-end: var(--sl-spacing-medium); + margin-inline-end: var(--sl-spacing-medium); + align-self: center; } .alert__countdown { diff --git a/src/components/alert/alert.test.ts b/src/components/alert/alert.test.ts index 84ba45cb..4fa7cc49 100644 --- a/src/components/alert/alert.test.ts +++ b/src/components/alert/alert.test.ts @@ -123,14 +123,14 @@ describe('', () => { }); describe('close button', () => { - it('shows a close button if the alert has the closable attribute', () => async () => { + it('shows a close button if the alert has the closable attribute', async () => { const alert = await fixture(html` I am an alert `); const closeButton = getCloseButton(alert); expect(closeButton).to.be.visible; }); - it('clicking the close button closes the alert', () => async () => { + it('clicking the close button closes the alert', async () => { const alert = await fixture(html` I am an alert `); const closeButton = getCloseButton(alert); @@ -138,6 +138,83 @@ describe('', () => { clickOnElement(closeButton!); }); }); + + it('clicking above close button does not close the alert', async () => { + const wrapper = await fixture( + html`
+ I am an alert +
` + ); + const alert = wrapper.querySelector('sl-alert')!; + + const clickTargetPromise = new Promise(resolve => { + const clickHandler = sinon.spy((event: MouseEvent) => { + resolve(event.target as HTMLElement); + }); + alert.shadowRoot!.addEventListener('click', clickHandler); + wrapper.addEventListener('click', clickHandler); + }); + + const closeButton = getCloseButton(alert); + await clickOnElement(closeButton!, 'top', 0, -4); + const clickTarget = await clickTargetPromise; + await expect(clickTarget.tagName.toLowerCase()).to.not.be.equal('sl-icon-button'); + expect(clickTarget.classList.contains('alert')).to.be.true; + expect(clickTarget.classList.contains('wrapper'), 'The click should happen in the alert and not outside of it').to + .be.false; + }); + + it('clicking under close button does not close the alert', async () => { + const wrapper = await fixture( + html`
+ I am an alert +
` + ); + const alert = wrapper.querySelector('sl-alert')!; + + const clickTargetPromise = new Promise(resolve => { + const clickHandler = sinon.spy((event: MouseEvent) => { + resolve(event.target as HTMLElement); + }); + alert.shadowRoot!.addEventListener('click', clickHandler); + wrapper.addEventListener('click', clickHandler); + }); + + const closeButton = getCloseButton(alert); + await clickOnElement(closeButton!, 'bottom', 0, 4); + const clickTarget = await clickTargetPromise; + + await expect(clickTarget.tagName.toLowerCase()).to.not.be.equal('sl-icon-button'); + expect(clickTarget.classList.contains('alert')).to.be.true; + expect(clickTarget.classList.contains('wrapper'), 'The click should happen in the alert and not outside of it').to + .be.false; + }); + + it('clicking on the right side of the close button does not close the alert', async () => { + const wrapper = await fixture( + html`
+ I am an alert +
` + ); + const alert = wrapper.querySelector('sl-alert')!; + + const clickTargetPromise = new Promise(resolve => { + const clickHandler = sinon.spy((event: MouseEvent) => { + resolve(event.target as HTMLElement); + }); + alert.shadowRoot!.addEventListener('click', clickHandler); + wrapper.addEventListener('click', clickHandler); + }); + + const closeButton = getCloseButton(alert); + await clickOnElement(closeButton!, 'right', 4, 0); + const clickTarget = await clickTargetPromise; + + await expect(clickTarget.tagName.toLowerCase()).to.not.be.equal('sl-icon-button'); + expect(clickTarget.classList.contains('alert')).to.be.true; + expect(clickTarget.classList.contains('wrapper'), 'The click should happen in the alert and not outside of it').to + .be.false; + }); }); describe('toast', () => { diff --git a/src/components/carousel-item/carousel-item.component.ts b/src/components/carousel-item/carousel-item.component.ts index 7ef2ba16..2eba116c 100644 --- a/src/components/carousel-item/carousel-item.component.ts +++ b/src/components/carousel-item/carousel-item.component.ts @@ -20,7 +20,6 @@ export default class SlCarouselItem extends ShoelaceElement { connectedCallback() { super.connectedCallback(); - this.setAttribute('role', 'group'); } render() { diff --git a/src/components/carousel/carousel.component.ts b/src/components/carousel/carousel.component.ts index 52024695..dd0c3e49 100644 --- a/src/components/carousel/carousel.component.ts +++ b/src/components/carousel/carousel.component.ts @@ -367,8 +367,16 @@ export default class SlCarousel extends ShoelaceElement { this.getSlides({ excludeClones: false }).forEach((slide, index) => { slide.classList.remove('--in-view'); slide.classList.remove('--is-active'); + slide.setAttribute('role', 'group'); slide.setAttribute('aria-label', this.localize.term('slideNum', index + 1)); + if (this.pagination) { + slide.setAttribute('id', `slide-${index + 1}`); + slide.setAttribute('role', 'tabpanel'); + slide.removeAttribute('aria-label'); + slide.setAttribute('aria-labelledby', `tab-${index + 1}`); + } + if (slide.hasAttribute('data-clone')) { slide.remove(); } @@ -611,7 +619,7 @@ export default class SlCarousel extends ShoelaceElement { : ''} ${this.pagination ? html` -