Compare commits

..

17 Commits

Author SHA1 Message Date
Cory LaViska
3217ce9f76 resolve feedback 2023-08-21 10:56:25 -04:00
Cory LaViska
c6d69fdbd6 update changelog 2023-08-18 14:47:58 -04:00
Cory LaViska
77b40fa7b4 Merge branch 'next' into submenus 2023-08-18 14:43:17 -04:00
Cory LaViska
bebf74a1fb stay highlighted when submenu is open 2023-08-18 14:40:21 -04:00
Cory LaViska
f524d20216 polish up submenu stuff 2023-08-18 14:18:38 -04:00
Cory LaViska
021c32ab32 Merge branch 'next-eycrb' of github.com:ecyrb/shoelace-ecyrb-fork into ecyrb-next-eycrb 2023-08-18 13:04:14 -04:00
Bryce Moore
dbf506d78f Submenu tweaks ...
- 100 ms delay when opening submenus on mouseover
- Shadows added
- Distance added to popup to have submenus overlap menu slightly.
2023-07-14 15:13:07 -04:00
Bryce Moore
1d826a9c53 fix: Prevent menu's extraneous Enter / space key propagation.
Menu's handleKeyDown calls item.click (to emit the selection).
Propagating the keyboard event on Enter / space would the cause re-entry
into a submenu, so prevent the needless propagation.
2023-07-13 09:55:45 -04:00
Bryce Moore
58d6cad39a fix: 2 changes to menu / submenu on-click behavior:
1. Close submenu on click explicitly, so this occurs even if the menu is
   not inside of an sl-dropdown.

2. In menu, ignore clicks that do not explicitly target a menu-item.
   Clicks that were on (e.g. a menu-border) were emitting select events.
2023-07-13 08:49:10 -04:00
Bryce Moore
e0e96c8a0a style: Eslint warnings and errors fixed. npm run verify now passes. 2023-07-13 06:26:44 -04:00
Bryce Moore
25a146ffd1 Cleanup: Removed dead code and dead code comments. 2023-07-11 02:50:48 -04:00
Bryce Moore
c55ba0467e Merge branch 'next' into next-eycrb 2023-07-08 08:16:46 -04:00
Bryce Moore
126b0e34bd [WIP] Submenu WIP continues.
- Submenus now close on change-of-focus, not a timeout.
- Keyboard navigation support added.
- Skidding fix for better alignment.
- Submenu documentation moved to Menu page.
- Tests for accessibility, right and left arrow keys.
2023-07-07 09:43:40 -04:00
Bryce Moore
7aca8d7300 Revert "PoC working of ArrowRight to focus on submenu."
(Didn't mean to publish this.)

This reverts commit be04e9a221.
2023-06-30 09:52:54 -04:00
Bryce Moore
be04e9a221 PoC working of ArrowRight to focus on submenu. 2023-06-30 07:44:55 -04:00
Bryce
354404484e Update submenu-controller.ts
Removed extraneous `console.log()`.
2023-06-28 02:43:51 -04:00
Bryce Moore
b48d072bc8 [RFC] Proof-of-concept commit for submenu support
This is a Request For Comments to seek directional guidance towards
implementing the submenu slot of menu-item.

Includes:
- SubmenuController to manage event listeners on menu-item.
- Example usage in menu-item documentation.
- Trivial tests to check rendering.

Outstanding questions include:
- Accessibility concerns. E.g. where to handle 'ArrowRight',
  'ArrowLeft'?
- Should selection of menu-item denoting submenu be possible or
  customizable?
- How to parameterize contained popup?
- Implementation concerns:
  - Use of ref / id
  - delegation of some rendering to the controller
  - What to test

Related to [#620](https://github.com/shoelace-style/shoelace/issues/620).
2023-06-28 01:08:37 -04:00
63 changed files with 299 additions and 406 deletions

View File

@@ -137,17 +137,15 @@
<tr>
<td>
<code class="nowrap">{{ prop.name }}</code>
{% if prop.attribute | length > 0 %}
{% if prop.attribute != prop.name %}
<br>
<sl-tooltip content="This attribute is different from its property">
<small>
<code class="nowrap">
{{ prop.attribute }}
</code>
</small>
</sl-tooltip>
{% endif %}
{% if prop.attribute != prop.name %}
<br>
<sl-tooltip content="This attribute is different from its property">
<small>
<code class="nowrap">
{{ prop.attribute }}
</code>
</small>
</sl-tooltip>
{% endif %}
</td>
<td>
@@ -187,7 +185,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#properties') }}">attributes and properties</a>.</em></p>
{% endif %}
{# Events #}
@@ -307,7 +305,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#component-parts') }}">customizing CSS parts</a>.</em></p>
{% endif %}
{# Animations #}
@@ -331,7 +329,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#animations') }}">customizing animations</a>.</em></p>
{% endif %}
{# Dependencies #}

View File

@@ -1,5 +1,3 @@
let codeBlockId = 0;
/**
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
* document will be returned with the appropriate DOM manipulations.
@@ -7,14 +5,19 @@ let codeBlockId = 0;
module.exports = function (doc) {
doc.querySelectorAll('pre > code').forEach(code => {
const pre = code.closest('pre');
const button = doc.createElement('sl-copy-button');
if (!code.id) {
code.id = `code-block-${++codeBlockId}`;
}
const button = doc.createElement('button');
button.setAttribute('type', 'button');
button.classList.add('copy-code-button');
button.setAttribute('from', code.id);
button.setAttribute('aria-label', 'Copy');
button.innerHTML = `
<svg class="copy-code-button__copy-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16" part="svg">
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"></path>
</svg>
<svg class="copy-code-button__copied-icon" style="display: none;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16" part="svg">
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"></path>
</svg>
`;
pre.append(button);
});

View File

@@ -163,6 +163,32 @@
});
})();
//
// Copy code buttons
//
(() => {
document.addEventListener('click', event => {
const button = event.target.closest('.copy-code-button');
const pre = button?.closest('pre');
const code = pre?.querySelector('code');
const copyIcon = button?.querySelector('.copy-code-button__copy-icon');
const copiedIcon = button?.querySelector('.copy-code-button__copied-icon');
if (button && code) {
navigator.clipboard.writeText(code.innerText);
copyIcon.style.display = 'none';
copiedIcon.style.display = 'inline';
button.classList.add('copy-code-button--copied');
setTimeout(() => {
copyIcon.style.display = 'inline';
copiedIcon.style.display = 'none';
button.classList.remove('copy-code-button--copied');
}, 1000);
}
});
})();
//
// Smooth links
//

View File

@@ -373,12 +373,4 @@
hide();
}
});
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
// get trapped.
window.addEventListener('turbo:render', () => {
document.body.classList.remove('search-visible');
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
});
})();

View File

@@ -506,39 +506,46 @@ pre .token.italic {
/* Copy code button */
.copy-code-button {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
right: 0;
white-space: normal;
top: 0.5rem;
right: 0.5rem;
background: var(--sl-color-neutral-0);
border-radius: calc(var(--docs-border-radius) * 0.875);
border: solid 1px var(--sl-color-neutral-200);
color: var(--sl-color-neutral-800);
transition: 150ms opacity, 150ms scale;
text-transform: uppercase;
padding: 0.5rem;
margin: 0;
cursor: pointer;
transition: 100ms opacity, 100ms scale;
}
.copy-code-button::part(button) {
background-color: var(--sl-color-neutral-50);
border-radius: 0 var(--docs-border-radius) 0 var(--docs-border-radius);
padding: 0.75rem;
}
.copy-code-button::part(button):hover {
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 3%);
}
.copy-code-button::part(button):active {
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 6%);
.copy-code-button svg {
width: 1rem;
height: 1rem;
}
pre .copy-code-button {
opacity: 0;
scale: 0.75;
scale: 0.9;
}
pre:hover .copy-code-button,
.copy-code-button:focus-within {
.copy-code-button:focus-visible {
opacity: 1;
scale: 1;
}
pre:hover .copy-code-button:hover,
pre:hover .copy-code-button--copied {
background: var(--sl-color-neutral-200);
border-color: var(--sl-color-neutral-300);
color: var(--sl-color-neutral-900);
}
/* Callouts */
.callout {
position: relative;

View File

@@ -14,21 +14,12 @@ New versions of Shoelace are released as-needed and generally occur when a criti
## Next
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
- Updated `@ctrl/tinycolor` to 4.0.1 [#1542]
## 2.8.0
- Added `--isolatedModules` and `--verbatimModuleSyntax` to `tsconfig.json`. For anyone directly importing event types, they no longer provide a default export due to these options being enabled. For people using the `events/event.js` file directly, there is no change.
- Added support for submenus in `<sl-menu-item>` [#1410]
- Added the `--submenu-offset` custom property to `<sl-menu-item>` [#1410]
- Fixed an issue with focus trapping elements like `<sl-dialog>` when wrapped by other elements not checking the assigned elements of `<slot>`s. [#1537]
- Fixed type issues with the `ref` attribute in React Wrappers. [#1526]
- Fixed a regression that caused `<sl-radio-button>` to render incorrectly with gaps [#1523]
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
- Improved `<sl-menu-item>` so labels truncate properly instead of getting chopped and overflowing
- Removed the extra `React.Component` around `@lit-labs/react` wrapper. [#1531]
- Updated `@lit-labs/react` to v2.0.1. [#1531]
## 2.7.0

58
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "@shoelace-style/shoelace",
"version": "2.8.0",
"version": "2.7.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@shoelace-style/shoelace",
"version": "2.8.0",
"version": "2.7.0",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.0.1",
"@ctrl/tinycolor": "^3.5.0",
"@floating-ui/dom": "^1.2.1",
"@lit-labs/react": "^2.0.1",
"@lit-labs/react": "^1.2.1",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.1.1",
"composed-offset-position": "^0.0.4",
@@ -833,11 +833,11 @@
}
},
"node_modules/@ctrl/tinycolor": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.1.tgz",
"integrity": "sha512-dfimuE1mfaqL8P8jyQzdk9yFeFUWCyhjK5VyydXgDtQO0fezr6aWaGauHnlI07BZBIF45gahb0oxJjkUcylDwQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==",
"engines": {
"node": ">=14"
"node": ">=10"
}
},
"node_modules/@custom-elements-manifest/analyzer": {
@@ -1474,12 +1474,9 @@
}
},
"node_modules/@lit-labs/react": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.1.tgz",
"integrity": "sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==",
"peerDependencies": {
"@types/react": "17 || 18"
}
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz",
"integrity": "sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A=="
},
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.1.1",
@@ -2273,7 +2270,8 @@
"node_modules/@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"node_modules/@types/qs": {
"version": "6.9.7",
@@ -2291,6 +2289,7 @@
"version": "18.0.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -2318,7 +2317,8 @@
"node_modules/@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.0",
@@ -5685,7 +5685,8 @@
"node_modules/csstype": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
"dev": true
},
"node_modules/custom-element-jet-brains-integration": {
"version": "1.1.0",
@@ -17913,9 +17914,9 @@
"dev": true
},
"@ctrl/tinycolor": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.1.tgz",
"integrity": "sha512-dfimuE1mfaqL8P8jyQzdk9yFeFUWCyhjK5VyydXgDtQO0fezr6aWaGauHnlI07BZBIF45gahb0oxJjkUcylDwQ=="
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg=="
},
"@custom-elements-manifest/analyzer": {
"version": "0.8.3",
@@ -18290,10 +18291,9 @@
}
},
"@lit-labs/react": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.1.tgz",
"integrity": "sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==",
"requires": {}
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz",
"integrity": "sha512-DiZdJYFU0tBbdQkfwwRSwYyI/mcWkg3sWesKRsHUd4G+NekTmmeq9fzsurvcKTNVa0comNljwtg4Hvi1ds3V+A=="
},
"@lit-labs/ssr-dom-shim": {
"version": "1.1.1",
@@ -18998,7 +18998,8 @@
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/qs": {
"version": "6.9.7",
@@ -19016,6 +19017,7 @@
"version": "18.0.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -19043,7 +19045,8 @@
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/semver": {
"version": "7.5.0",
@@ -21511,7 +21514,8 @@
"csstype": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
"dev": true
},
"custom-element-jet-brains-integration": {
"version": "1.1.0",

View File

@@ -1,7 +1,7 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.8.0",
"version": "2.7.0",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
@@ -60,9 +60,9 @@
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^4.0.1",
"@ctrl/tinycolor": "^3.5.0",
"@floating-ui/dom": "^1.2.1",
"@lit-labs/react": "^2.0.1",
"@lit-labs/react": "^1.2.1",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.1.1",
"composed-offset-position": "^0.0.4",

View File

@@ -53,10 +53,6 @@ async function buildTheDocs(watch = false) {
output.push(data.toString());
});
child.stderr.on('data', data => {
output.push(data.toString());
});
if (watch) {
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
// tells us when the first build completes.

View File

@@ -51,11 +51,18 @@ components.map(component => {
${eventImports}
${eventExports}
const tagName = '${component.tagName}'
Component.define('${component.tagName}')
export type ForwardComponent<
Element extends HTMLElement,
ReactComponent extends React.ElementType
> = React.JSXElementConstructor<
React.ComponentPropsWithoutRef<ReactComponent> & {
ref?: React.ForwardedRef<Element>;
}
> & { displayName?: string }
${jsDoc}
const reactWrapper = createComponent({
const tagName = '${component.tagName}'
const component = createComponent({
tagName,
elementClass: Component,
react: React,
@@ -65,7 +72,20 @@ components.map(component => {
displayName: "${component.name}"
})
export default reactWrapper
${jsDoc}
class SlComponent extends React.Component<Parameters<typeof component>[0]> {
constructor (...args: Parameters<typeof component>) {
super(...args)
Component.define(tagName)
}
render () {
const { children, ...props } = this.props
return React.createElement(component, props, children)
}
}
export default SlComponent as ForwardComponent<Component, typeof SlComponent>;
`,
Object.assign(prettierConfig, {
parser: 'babel-ts'

View File

@@ -5,7 +5,7 @@ meta:
layout: component
---
```html:preview
```html preview
<{{ tag }}></{{ tag }}>
```

View File

@@ -20,8 +20,8 @@ import SlVisuallyHidden from '../visually-hidden/visually-hidden.component.js';
import styles from './color-picker.styles.js';
import type { CSSResultGroup } from 'lit';
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
import type { SlChangeEvent } from '../../events/sl-change.js';
import type { SlInputEvent } from '../../events/sl-input.js';
import type SlChangeEvent from '../../events/sl-change.js';
import type SlInputEvent from '../../events/sl-input.js';
const hasEyeDropper = 'EyeDropper' in window;

View File

@@ -2,9 +2,9 @@ import '../../../dist/shoelace.js';
// cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type { SlHideEvent } from '../../events/sl-hide';
import type { SlShowEvent } from '../../events/sl-show';
import type SlDetails from './details';
import type SlHideEvent from '../../events/sl-hide';
import type SlShowEvent from '../../events/sl-show';
describe('<sl-details>', () => {
describe('accessibility', () => {

View File

@@ -11,10 +11,10 @@ import ShoelaceElement from '../../internal/shoelace-element.js';
import SlPopup from '../popup/popup.component.js';
import styles from './dropdown.styles.js';
import type { CSSResultGroup } from 'lit';
import type { SlSelectEvent } from '../../events/sl-select.js';
import type SlButton from '../button/button.js';
import type SlIconButton from '../icon-button/icon-button.js';
import type SlMenu from '../menu/menu.js';
import type SlSelectEvent from '../../events/sl-select.js';
/**
* @summary Dropdowns expose additional content that "drops down" in a panel.

View File

@@ -1,8 +1,8 @@
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { registerIconLibrary } from '../../../dist/shoelace.js';
import type { SlErrorEvent } from '../../events/sl-error';
import type { SlLoadEvent } from '../../events/sl-load';
import type SlErrorEvent from '../../events/sl-error';
import type SlIcon from './icon';
import type SlLoadEvent from '../../events/sl-load';
const testLibraryIcons = {
'test-icon1': `

View File

@@ -2,8 +2,8 @@ import '../../../dist/shoelace.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type { SlSelectEvent } from '../../events/sl-select';
import type SlMenuItem from './menu-item';
import type SlSelectEvent from '../../events/sl-select';
describe('<sl-menu-item>', () => {
it('should pass accessibility tests', async () => {

View File

@@ -4,8 +4,8 @@ import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type { SlSelectEvent } from '../../events/sl-select';
import type SlMenu from './menu';
import type SlSelectEvent from '../../events/sl-select';
describe('<sl-menu>', () => {
it('emits sl-select with the correct event detail when clicking an item', async () => {

View File

@@ -4,7 +4,7 @@ import { clickOnElement } from '../../internal/test.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type { SlChangeEvent } from '../../events/sl-change.js';
import type SlChangeEvent from '../../events/sl-change.js';
import type SlRadio from '../radio/radio.js';
import type SlRadioGroup from './radio-group.js';

View File

@@ -18,8 +18,8 @@ import SlTag from '../tag/tag.component.js';
import styles from './select.styles.js';
import type { CSSResultGroup, TemplateResult } from 'lit';
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
import type { SlRemoveEvent } from '../../events/sl-remove.js';
import type SlOption from '../option/option.component.js';
import type SlRemoveEvent from '../../events/sl-remove.js';
/**
* @summary Selects allow you to choose items from a menu of predefined options.

View File

@@ -215,9 +215,7 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
<span part="thumb" class="switch__thumb"></span>
</span>
<div part="label" class="switch__label">
<slot></slot>
</div>
<slot part="label" class="switch__label"></slot>
</label>
`;
}

View File

@@ -7,10 +7,10 @@ import { queryByTestId } from '../../internal/test/data-testid-helpers.js';
import { sendKeys } from '@web/test-runner-commands';
import { waitForScrollingToEnd } from '../../internal/test/wait-for-scrolling.js';
import type { HTMLTemplateResult } from 'lit';
import type { SlTabShowEvent } from '../../events/sl-tab-show.js';
import type SlTab from '../tab/tab.js';
import type SlTabGroup from './tab-group.js';
import type SlTabPanel from '../tab-panel/tab-panel.js';
import type SlTabShowEvent from '../../events/sl-tab-show.js';
interface ClientRectangles {
body?: DOMRect;

View File

@@ -1,35 +1,35 @@
export type { SlAfterCollapseEvent } from './sl-after-collapse';
export type { SlAfterExpandEvent } from './sl-after-expand';
export type { SlAfterHideEvent } from './sl-after-hide';
export type { SlAfterShowEvent } from './sl-after-show';
export type { SlBlurEvent } from './sl-blur';
export type { SlCancelEvent } from './sl-cancel';
export type { SlChangeEvent } from './sl-change';
export type { SlClearEvent } from './sl-clear';
export type { SlCloseEvent } from './sl-close';
export type { SlCollapseEvent } from './sl-collapse';
export type { SlCopyEvent } from './sl-copy';
export type { SlErrorEvent } from './sl-error';
export type { SlExpandEvent } from './sl-expand';
export type { SlFinishEvent } from './sl-finish';
export type { SlFocusEvent } from './sl-focus';
export type { SlHideEvent } from './sl-hide';
export type { SlHoverEvent } from './sl-hover';
export type { SlInitialFocusEvent } from './sl-initial-focus';
export type { SlInputEvent } from './sl-input';
export type { SlInvalidEvent } from './sl-invalid';
export type { SlLazyChangeEvent } from './sl-lazy-change';
export type { SlLazyLoadEvent } from './sl-lazy-load';
export type { SlLoadEvent } from './sl-load';
export type { SlMutationEvent } from './sl-mutation';
export type { SlRemoveEvent } from './sl-remove';
export type { SlRepositionEvent } from './sl-reposition';
export type { SlRequestCloseEvent } from './sl-request-close';
export type { SlResizeEvent } from './sl-resize';
export type { SlSelectEvent } from './sl-select';
export type { SlSelectionChangeEvent } from './sl-selection-change';
export type { SlShowEvent } from './sl-show';
export type { SlSlideChangeEvent } from './sl-slide-change';
export type { SlStartEvent } from './sl-start';
export type { SlTabHideEvent } from './sl-tab-hide';
export type { SlTabShowEvent } from './sl-tab-show';
export type { default as SlAfterCollapseEvent } from './sl-after-collapse';
export type { default as SlAfterExpandEvent } from './sl-after-expand';
export type { default as SlAfterHideEvent } from './sl-after-hide';
export type { default as SlAfterShowEvent } from './sl-after-show';
export type { default as SlBlurEvent } from './sl-blur';
export type { default as SlCancelEvent } from './sl-cancel';
export type { default as SlChangeEvent } from './sl-change';
export type { default as SlClearEvent } from './sl-clear';
export type { default as SlCloseEvent } from './sl-close';
export type { default as SlCollapseEvent } from './sl-collapse';
export type { default as SlCopyEvent } from './sl-copy';
export type { default as SlErrorEvent } from './sl-error';
export type { default as SlExpandEvent } from './sl-expand';
export type { default as SlFinishEvent } from './sl-finish';
export type { default as SlFocusEvent } from './sl-focus';
export type { default as SlHideEvent } from './sl-hide';
export type { default as SlHoverEvent } from './sl-hover';
export type { default as SlInitialFocusEvent } from './sl-initial-focus';
export type { default as SlInputEvent } from './sl-input';
export type { default as SlInvalidEvent } from './sl-invalid';
export type { default as SlLazyChangeEvent } from './sl-lazy-change';
export type { default as SlLazyLoadEvent } from './sl-lazy-load';
export type { default as SlLoadEvent } from './sl-load';
export type { default as SlMutationEvent } from './sl-mutation';
export type { default as SlRemoveEvent } from './sl-remove';
export type { default as SlRepositionEvent } from './sl-reposition';
export type { default as SlRequestCloseEvent } from './sl-request-close';
export type { default as SlResizeEvent } from './sl-resize';
export type { default as SlSelectEvent } from './sl-select';
export type { default as SlSelectionChangeEvent } from './sl-selection-change';
export type { default as SlShowEvent } from './sl-show';
export type { default as SlSlideChangeEvent } from './sl-slide-change';
export type { default as SlStartEvent } from './sl-start';
export type { default as SlTabHideEvent } from './sl-tab-hide';
export type { default as SlTabShowEvent } from './sl-tab-show';

View File

@@ -1,7 +1,9 @@
export type SlAfterCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
type SlAfterCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-after-collapse': SlAfterCollapseEvent;
}
}
export default SlAfterCollapseEvent;

View File

@@ -1,7 +1,9 @@
export type SlAfterExpandEvent = CustomEvent<Record<PropertyKey, never>>;
type SlAfterExpandEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-after-expand': SlAfterExpandEvent;
}
}
export default SlAfterExpandEvent;

View File

@@ -1,7 +1,9 @@
export type SlAfterHideEvent = CustomEvent<Record<PropertyKey, never>>;
type SlAfterHideEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-after-hide': SlAfterHideEvent;
}
}
export default SlAfterHideEvent;

View File

@@ -1,7 +1,9 @@
export type SlAfterShowEvent = CustomEvent<Record<PropertyKey, never>>;
type SlAfterShowEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-after-show': SlAfterShowEvent;
}
}
export default SlAfterShowEvent;

View File

@@ -1,7 +1,9 @@
export type SlBlurEvent = CustomEvent<Record<PropertyKey, never>>;
type SlBlurEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-blur': SlBlurEvent;
}
}
export default SlBlurEvent;

View File

@@ -1,7 +1,9 @@
export type SlCancelEvent = CustomEvent<Record<PropertyKey, never>>;
type SlCancelEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-cancel': SlCancelEvent;
}
}
export default SlCancelEvent;

View File

@@ -1,7 +1,9 @@
export type SlChangeEvent = CustomEvent<Record<PropertyKey, never>>;
type SlChangeEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-change': SlChangeEvent;
}
}
export default SlChangeEvent;

View File

@@ -1,7 +1,9 @@
export type SlClearEvent = CustomEvent<Record<PropertyKey, never>>;
type SlClearEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-clear': SlClearEvent;
}
}
export default SlClearEvent;

View File

@@ -1,7 +1,9 @@
export type SlCloseEvent = CustomEvent<Record<PropertyKey, never>>;
type SlCloseEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-close': SlCloseEvent;
}
}
export default SlCloseEvent;

View File

@@ -1,7 +1,9 @@
export type SlCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
type SlCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-collapse': SlCollapseEvent;
}
}
export default SlCollapseEvent;

View File

@@ -1,7 +1,9 @@
export type SlCopyEvent = CustomEvent<{ value: string }>;
type SlCopyEvent = CustomEvent<{ value: string }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-copy': SlCopyEvent;
}
}
export default SlCopyEvent;

View File

@@ -1,7 +1,9 @@
export type SlErrorEvent = CustomEvent<{ status?: number }>;
type SlErrorEvent = CustomEvent<{ status?: number }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-error': SlErrorEvent;
}
}
export default SlErrorEvent;

View File

@@ -1,7 +1,9 @@
export type SlExpandEvent = CustomEvent<Record<PropertyKey, never>>;
type SlExpandEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-expand': SlExpandEvent;
}
}
export default SlExpandEvent;

View File

@@ -1,7 +1,9 @@
export type SlFinishEvent = CustomEvent<Record<PropertyKey, never>>;
type SlFinishEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-finish': SlFinishEvent;
}
}
export default SlFinishEvent;

View File

@@ -1,7 +1,9 @@
export type SlFocusEvent = CustomEvent<Record<PropertyKey, never>>;
type SlFocusEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-focus': SlFocusEvent;
}
}
export default SlFocusEvent;

View File

@@ -1,7 +1,9 @@
export type SlHideEvent = CustomEvent<Record<PropertyKey, never>>;
type SlHideEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-hide': SlHideEvent;
}
}
export default SlHideEvent;

View File

@@ -1,4 +1,4 @@
export type SlHoverEvent = CustomEvent<{
type SlHoverEvent = CustomEvent<{
phase: 'start' | 'move' | 'end';
value: number;
}>;
@@ -8,3 +8,5 @@ declare global {
'sl-hover': SlHoverEvent;
}
}
export default SlHoverEvent;

View File

@@ -1,7 +1,9 @@
export type SlInitialFocusEvent = CustomEvent<Record<PropertyKey, never>>;
type SlInitialFocusEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-initial-focus': SlInitialFocusEvent;
}
}
export default SlInitialFocusEvent;

View File

@@ -1,7 +1,9 @@
export type SlInputEvent = CustomEvent<Record<PropertyKey, never>>;
type SlInputEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-input': SlInputEvent;
}
}
export default SlInputEvent;

View File

@@ -1,7 +1,9 @@
export type SlInvalidEvent = CustomEvent<Record<PropertyKey, never>>;
type SlInvalidEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-invalid': SlInvalidEvent;
}
}
export default SlInvalidEvent;

View File

@@ -1,7 +1,9 @@
export type SlLazyChangeEvent = CustomEvent<Record<PropertyKey, never>>;
type SlLazyChangeEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-lazy-change': SlLazyChangeEvent;
}
}
export default SlLazyChangeEvent;

View File

@@ -1,7 +1,9 @@
export type SlLazyLoadEvent = CustomEvent<Record<PropertyKey, never>>;
type SlLazyLoadEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-lazy-load': SlLazyLoadEvent;
}
}
export default SlLazyLoadEvent;

View File

@@ -1,7 +1,9 @@
export type SlLoadEvent = CustomEvent<Record<PropertyKey, never>>;
type SlLoadEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-load': SlLoadEvent;
}
}
export default SlLoadEvent;

View File

@@ -1,7 +1,9 @@
export type SlMutationEvent = CustomEvent<{ mutationList: MutationRecord[] }>;
type SlMutationEvent = CustomEvent<{ mutationList: MutationRecord[] }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-mutation': SlMutationEvent;
}
}
export default SlMutationEvent;

View File

@@ -1,7 +1,9 @@
export type SlRemoveEvent = CustomEvent<Record<PropertyKey, never>>;
type SlRemoveEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-remove': SlRemoveEvent;
}
}
export default SlRemoveEvent;

View File

@@ -1,7 +1,9 @@
export type SlRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
type SlRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-reposition': SlRepositionEvent;
}
}
export default SlRepositionEvent;

View File

@@ -1,7 +1,9 @@
export type SlRequestCloseEvent = CustomEvent<{ source: 'close-button' | 'keyboard' | 'overlay' }>;
type SlRequestCloseEvent = CustomEvent<{ source: 'close-button' | 'keyboard' | 'overlay' }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-request-close': SlRequestCloseEvent;
}
}
export default SlRequestCloseEvent;

View File

@@ -1,7 +1,9 @@
export type SlResizeEvent = CustomEvent<{ entries: ResizeObserverEntry[] }>;
type SlResizeEvent = CustomEvent<{ entries: ResizeObserverEntry[] }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-resize': SlResizeEvent;
}
}
export default SlResizeEvent;

View File

@@ -1,9 +1,11 @@
import type SlMenuItem from '../components/menu-item/menu-item';
export type SlSelectEvent = CustomEvent<{ item: SlMenuItem }>;
type SlSelectEvent = CustomEvent<{ item: SlMenuItem }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-select': SlSelectEvent;
}
}
export default SlSelectEvent;

View File

@@ -1,9 +1,11 @@
import type SlTreeItem from '../components/tree-item/tree-item';
export type SlSelectionChangeEvent = CustomEvent<{ selection: SlTreeItem[] }>;
type SlSelectionChangeEvent = CustomEvent<{ selection: SlTreeItem[] }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-selection-change': SlSelectionChangeEvent;
}
}
export default SlSelectionChangeEvent;

View File

@@ -1,7 +1,9 @@
export type SlShowEvent = CustomEvent<Record<PropertyKey, never>>;
type SlShowEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-show': SlShowEvent;
}
}
export default SlShowEvent;

View File

@@ -1,9 +1,11 @@
import type SlCarouselItem from '../components/carousel-item/carousel-item';
export type SlSlideChangeEvent = CustomEvent<{ index: number; slide: SlCarouselItem }>;
type SlSlideChangeEvent = CustomEvent<{ index: number; slide: SlCarouselItem }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-slide-change': SlSlideChangeEvent;
}
}
export default SlSlideChangeEvent;

View File

@@ -1,7 +1,9 @@
export type SlStartEvent = CustomEvent<Record<PropertyKey, never>>;
type SlStartEvent = CustomEvent<Record<PropertyKey, never>>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-start': SlStartEvent;
}
}
export default SlStartEvent;

View File

@@ -1,7 +1,9 @@
export type SlTabHideEvent = CustomEvent<{ name: string }>;
type SlTabHideEvent = CustomEvent<{ name: string }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-tab-hide': SlTabHideEvent;
}
}
export default SlTabHideEvent;

View File

@@ -1,7 +1,9 @@
export type SlTabShowEvent = CustomEvent<{ name: string }>;
type SlTabShowEvent = CustomEvent<{ name: string }>;
declare global {
interface GlobalEventHandlersEventMap {
'sl-tab-show': SlTabShowEvent;
}
}
export default SlTabShowEvent;

View File

@@ -1,22 +0,0 @@
/**
* Use a generator so we can iterate and possibly break early.
* @example
* // to operate like a regular array. This kinda nullifies generator benefits, but worth knowing if you need the whole array.
* const allActiveElements = [...activeElements()]
*
* // Early return
* for (const activeElement of activeElements()) {
* if (<cond>) {
* break; // Break the loop, dont need to iterate over the whole array or store an array in memory!
* }
* }
*/
export function* activeElements(activeElement: Element | null = document.activeElement): Generator<Element> {
if (activeElement === null || activeElement === undefined) return;
yield activeElement;
if ('shadowRoot' in activeElement && activeElement.shadowRoot && activeElement.shadowRoot.mode !== 'closed') {
yield* activeElements(activeElement.shadowRoot.activeElement);
}
}

View File

@@ -1,4 +1,3 @@
import { activeElements } from './active-elements.js';
import { getTabbableElements } from './tabbable.js';
let activeModals: HTMLElement[] = [];
@@ -56,20 +55,6 @@ export default class Modal {
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
}
/**
* Checks if the `startElement` is already focused. This is important if the modal already
* has an existing focus prior to the first tab key.
*/
startElementAlreadyFocused(startElement: HTMLElement) {
for (const activeElement of activeElements()) {
if (startElement === activeElement) {
return true;
}
}
return false;
}
handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Tab') return;
@@ -83,10 +68,7 @@ export default class Modal {
const tabbableElements = getTabbableElements(this.element);
const start = tabbableElements[0];
// Sometimes we programmatically focus the first element in a modal.
// Lets make sure the start element isn't already focused.
let focusIndex = this.startElementAlreadyFocused(start) ? 0 : this.currentFocusIndex;
let focusIndex = this.currentFocusIndex;
if (focusIndex === -1) {
this.currentFocus = start;

View File

@@ -1,147 +0,0 @@
import { elementUpdated, expect, fixture } from '@open-wc/testing';
import '../../dist/shoelace.js';
import { activeElements } from './active-elements.js';
import { html } from 'lit';
import { sendKeys } from '@web/test-runner-commands';
async function holdShiftKey(callback: () => Promise<void>) {
await sendKeys({ down: 'Shift' });
await callback();
await sendKeys({ up: 'Shift' });
}
const tabKey =
navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('HeadlessChrome') ? 'Alt+Tab' : 'Tab';
// Simple helper to turn the activeElements generator into an array
function activeElementsArray() {
return [...activeElements()];
}
function getDeepestActiveElement() {
return activeElementsArray().pop();
}
window.customElements.define(
'tab-test-1',
class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot!.innerHTML = `
<sl-drawer>
<slot name="label" slot="label"></slot>
<slot></slot>
<slot name="footer" slot="footer"></slot>
</sl-drawer>
`;
}
}
);
it('Should allow tabbing to slotted elements', async () => {
const el = await fixture(html`
<tab-test-1>
<div slot="label">
<sl-button id="focus-1">Focus 1</sl-button>
</div>
<div>
<!-- Focus 2 lives as the close-button from <sl-drawer> -->
<sl-button id="focus-3">Focus 3</sl-button>
<button id="focus-4">Focus 4</sl-button>
<input id="focus-5" value="Focus 5">
</div>
<div slot="footer">
<div id="focus-6" tabindex="0">Focus 6</div>
<button tabindex="-1">No Focus</button>
</div>
</tab-test-1>
`);
const drawer = el.shadowRoot?.querySelector('sl-drawer');
if (drawer === null || drawer === undefined) throw Error('Could not find drawer inside of the test element');
await drawer.show();
await elementUpdated(drawer);
const focusZero = drawer.shadowRoot?.querySelector("[role='dialog']");
if (focusZero === null || focusZero === undefined) throw Error('Could not find dialog panel inside <sl-drawer>');
const focusOne = el.querySelector('#focus-1');
const focusTwo = drawer.shadowRoot?.querySelector("[part~='close-button']");
if (focusTwo === null || focusTwo === undefined) throw Error('Could not find close button inside <sl-drawer>');
const focusThree = el.querySelector('#focus-3');
const focusFour = el.querySelector('#focus-4');
const focusFive = el.querySelector('#focus-5');
const focusSix = el.querySelector('#focus-6');
// When we open drawer, we should be focused on the panel to start.
expect(getDeepestActiveElement()).to.equal(focusZero);
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusOne);
// When we hit the <Tab> key we should go to the "close button" on the drawer
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusTwo);
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusThree);
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusFour);
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusFive);
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusSix);
// Now we should loop back to #panel
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusZero);
// Now we should loop back to #panel
await sendKeys({ press: tabKey });
expect(activeElementsArray()).to.include(focusOne);
// Let's reset and try from starting point 0 and go backwards.
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusZero);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusSix);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusFive);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusFour);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusThree);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusTwo);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusOne);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusZero);
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
expect(activeElementsArray()).to.include(focusSix);
});

View File

@@ -69,32 +69,11 @@ export function getTabbableBoundary(root: HTMLElement | ShadowRoot) {
}
export function getTabbableElements(root: HTMLElement | ShadowRoot) {
const tabbableElements: HTMLElement[] = [];
const allElements: HTMLElement[] = [];
function walk(el: HTMLElement | ShadowRoot) {
if (el instanceof Element) {
// if the element has "inert" we can just no-op it.
if (el.hasAttribute('inert')) {
return;
}
if (!tabbableElements.includes(el) && isTabbable(el)) {
tabbableElements.push(el);
}
/**
* This looks funky. Basically a slot's children will always be picked up *if* they're within the `root` element.
* However, there is an edge case when, if the `root` is wrapped by another shadow DOM, it won't grab the children.
* This fixes that fun edge case.
*/
const slotChildrenOutsideRootElement = (slotElement: HTMLSlotElement) =>
(slotElement.getRootNode({ composed: true }) as ShadowRoot | null)?.host !== root;
if (el instanceof HTMLSlotElement && slotChildrenOutsideRootElement(el)) {
el.assignedElements({ flatten: true }).forEach((assignedEl: HTMLElement) => {
walk(assignedEl);
});
}
allElements.push(el);
if (el.shadowRoot !== null && el.shadowRoot.mode === 'open') {
walk(el.shadowRoot);
@@ -107,14 +86,10 @@ export function getTabbableElements(root: HTMLElement | ShadowRoot) {
// Collect all elements including the root
walk(root);
return tabbableElements;
// Is this worth having? Most sorts will always add increased overhead. And positive tabindexes shouldn't really be used.
// So is it worth being right? Or fast?
// return tabbableElements.filter(isTabbable).sort((a, b) => {
// // Make sure we sort by tabindex.
// const aTabindex = Number(a.getAttribute('tabindex')) || 0;
// const bTabindex = Number(b.getAttribute('tabindex')) || 0;
// return bTabindex - aTabindex;
// });
return allElements.filter(isTabbable).sort((a, b) => {
// Make sure we sort by tabindex.
const aTabindex = Number(a.getAttribute('tabindex')) || 0;
const bTabindex = Number(b.getAttribute('tabindex')) || 0;
return bTabindex - aTabindex;
});
}

View File

@@ -28,8 +28,7 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"types": [
"mocha",
"user-agent-data-types"

View File

@@ -1,6 +1,7 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "./src"
},
"include": ["src"],