feat: add ESLint, improve types, improve a11y

pull/647/head
Jason O'Neill 2022-01-15 21:47:14 -08:00
rodzic 2ad00deb38
commit 9fb3b5cfed
178 zmienionych plików z 17210 dodań i 1830 usunięć

9
.eslintignore 100644
Wyświetl plik

@ -0,0 +1,9 @@
.cache
docs/dist
docs/search.json
docs/**/*.min.js
dist
examples
node_modules
src/react
scripts

252
.eslintrc.cjs 100644
Wyświetl plik

@ -0,0 +1,252 @@
/* eslint-env node */
module.exports = {
plugins: ['@typescript-eslint', 'wc', 'lit', 'lit-a11y', 'chai-expect', 'chai-friendly', 'import'],
extends: [
'eslint:recommended',
'plugin:wc/recommended',
'plugin:wc/best-practice',
'plugin:lit/recommended',
'plugin:lit-a11y/recommended'
],
env: {
es2021: true,
browser: true
},
reportUnusedDisableDirectives: true,
parserOptions: {
sourceType: 'module'
},
overrides: [
{
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname
},
files: ['*.ts'],
rules: {
'default-param-last': 'off',
'@typescript-eslint/default-param-last': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'warn',
'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': 'error',
'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'no-throw-literal': 'off',
'@typescript-eslint/no-throw-literal': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/prefer-regexp-exec': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/consistent-type-assertions': [
'warn',
{
assertionStyle: 'as',
objectLiteralTypeAssertions: 'never'
}
],
'@typescript-eslint/consistent-type-imports': 'warn',
// "@typescript-eslint/explicit-function-return-type": [
// "error",
// {
// allowTypedFunctionExpressions: true,
// },
// ],
// "@typescript-eslint/explicit-member-accessibility": "warn",
// "@typescript-eslint/explicit-module-boundary-types": "error",
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-confusing-void-expression': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn',
'@typescript-eslint/no-unnecessary-condition': 'warn',
'@typescript-eslint/no-unnecessary-qualifier': 'warn',
'@typescript-eslint/non-nullable-type-assertion-style': 'warn',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-optional-chain': 'warn',
'@typescript-eslint/prefer-readonly': 'warn',
'@typescript-eslint/prefer-ts-expect-error': 'warn',
'@typescript-eslint/prefer-return-this-type': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'warn',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/unified-signatures': 'warn',
'@typescript-eslint/array-type': 'warn',
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/member-delimiter-style': 'warn',
'@typescript-eslint/method-signature-style': 'warn',
'@typescript-eslint/naming-convention': [
'warn',
{
selector: 'default',
format: ['camelCase']
},
{
selector: ['function', 'enumMember', 'property'],
format: ['camelCase', 'PascalCase']
},
{
selector: 'variable',
modifiers: ['const'],
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
},
{
selector: 'typeLike',
format: ['PascalCase']
},
{
selector: 'typeProperty',
format: ['camelCase', 'PascalCase', 'UPPER_CASE']
}
],
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-parameter-properties': 'error',
'@typescript-eslint/strict-boolean-expressions': [
'error',
{
allowString: false,
allowNumber: false,
allowNullableObject: false
}
]
}
},
{
extends: ['plugin:chai-expect/recommended', 'plugin:chai-friendly/recommended'],
files: ['*.test.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off'
}
}
],
rules: {
'no-template-curly-in-string': 'error',
'array-callback-return': 'error',
'consistent-return': 'error',
curly: 'warn',
'default-param-last': 'error',
eqeqeq: 'error',
'no-constructor-return': 'error',
'no-empty-function': 'warn',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-floating-decimal': 'error',
'no-implicit-coercion': 'error',
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-invalid-this': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-proto': 'error',
'no-return-assign': 'warn',
'no-script-url': 'error',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-throw-literal': 'error',
'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'warn',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-useless-return': 'warn',
'prefer-promise-reject-errors': 'error',
radix: 'error',
'require-await': 'error',
'wrap-iife': ['warn', 'inside'],
'no-shadow': 'error',
'no-array-constructor': 'error',
'no-bitwise': 'error',
'no-multi-assign': 'warn',
'no-new-object': 'error',
'no-useless-computed-key': 'warn',
'no-useless-rename': 'warn',
'no-var': 'error',
'prefer-const': 'warn',
'prefer-numeric-literals': 'warn',
'prefer-object-spread': 'warn',
'prefer-rest-params': 'warn',
'prefer-spread': 'warn',
'prefer-template': 'warn',
'no-else-return': 'warn',
'func-names': ['warn', 'never'],
'func-style': ['warn', 'declaration'],
'one-var': ['warn', 'never'],
'operator-assignment': 'warn',
'prefer-arrow-callback': 'warn',
'no-restricted-syntax': [
'warn',
{
selector: "CallExpression[callee.name='String']",
message: "Don't use the String function. Use .toString() instead."
},
{
selector: "CallExpression[callee.name='Number']",
message: "Don't use the Number function. Use parseInt or parseFloat instead."
},
{
selector: "CallExpression[callee.name='Boolean']",
message: "Don't use the Boolean function. Use a strict comparison instead."
}
],
'no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['../*'],
message: 'Usage of relative parent imports is not allowed.'
}
],
paths: [
{
name: '.',
message: 'Usage of local index imports is not allowed.'
},
{
name: './index',
message: 'Import from the source file instead.'
}
]
}
],
'import/no-duplicates': 'warn',
'import/order': [
'warn',
{
groups: ['builtin', 'external', ['parent', 'sibling', 'internal', 'index']],
pathGroups: [
{
pattern: '~/**',
group: 'internal'
},
{
pattern: 'dist/**',
group: 'external'
}
],
alphabetize: {
order: 'asc',
caseInsensitive: true
},
'newlines-between': 'never',
warnOnUnassignedImports: true
}
],
'wc/guard-super-call': 'off'
}
};

Wyświetl plik

@ -1,6 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"ms-vscode.vscode-typescript-tslint-plugin", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"bierner.lit-html", "bierner.lit-html",
"bashmish.es6-string-css", "bashmish.es6-string-css",

Wyświetl plik

@ -26,6 +26,7 @@
"csspart", "csspart",
"cssproperty", "cssproperty",
"datetime", "datetime",
"describedby",
"Docsify", "Docsify",
"dropdowns", "dropdowns",
"easings", "easings",
@ -79,6 +80,7 @@
"rgba", "rgba",
"roadmap", "roadmap",
"Roboto", "Roboto",
"saturationl",
"Schilp", "Schilp",
"Segoe", "Segoe",
"semibold", "semibold",

Wyświetl plik

@ -4,7 +4,10 @@ import { pascalCase } from 'pascal-case';
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const { name, description, version, author, homepage, license } = packageData; const { name, description, version, author, homepage, license } = packageData;
const noDash = string => string.replace(/^\s?-/, '').trim();
function noDash(string) {
return string.replace(/^\s?-/, '').trim();
}
export default { export default {
globs: ['src/components/**/*.ts'], globs: ['src/components/**/*.ts'],
@ -13,7 +16,7 @@ export default {
// Append package data // Append package data
{ {
name: 'shoelace-package-data', name: 'shoelace-package-data',
packageLinkPhase({ customElementsManifest, context }) { packageLinkPhase({ customElementsManifest }) {
customElementsManifest.package = { name, description, version, author, homepage, license }; customElementsManifest.package = { name, description, version, author, homepage, license };
} }
}, },
@ -21,9 +24,9 @@ export default {
// Parse custom jsDoc tags // Parse custom jsDoc tags
{ {
name: 'shoelace-custom-tags', name: 'shoelace-custom-tags',
analyzePhase({ ts, node, moduleDoc, context }) { analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) { switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: case ts.SyntaxKind.ClassDeclaration: {
const className = node.name.getText(); const className = node.name.getText();
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
const customTags = ['animation', 'dependency', 'since', 'status']; const customTags = ['animation', 'dependency', 'since', 'status'];
@ -39,8 +42,8 @@ export default {
}); });
}); });
const parsed = parse(customComments + '\n */'); const parsed = parse(`${customComments}\n */`);
parsed[0].tags?.map(t => { parsed[0].tags?.forEach(t => {
switch (t.tag) { switch (t.tag) {
// Animations // Animations
case 'animation': case 'animation':
@ -80,23 +83,25 @@ export default {
}); });
} }
}); });
}
} }
} }
}, },
{ {
name: 'shoelace-react-event-names', name: 'shoelace-react-event-names',
analyzePhase({ ts, node, moduleDoc, context }) { analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) { switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: case ts.SyntaxKind.ClassDeclaration: {
const className = node.name.getText(); const className = node.name.getText();
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
if (classDoc?.events) { if (classDoc?.events) {
classDoc.events.map(event => { classDoc.events.forEach(event => {
event.reactName = `on${pascalCase(event.name)}`; event.reactName = `on${pascalCase(event.name)}`;
}); });
} }
}
} }
} }
} }

Wyświetl plik

@ -1,3 +1,5 @@
/* global Prism */
(() => { (() => {
const reactVersion = '17.0.2'; const reactVersion = '17.0.2';
let flavor = getFlavor(); let flavor = getFlavor();
@ -61,14 +63,9 @@
document.body.classList.toggle('flavor-react', flavor === 'react'); document.body.classList.toggle('flavor-react', flavor === 'react');
} }
function wrap(el, wrapper) { window.$docsify.plugins.push(hook => {
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
}
window.$docsify.plugins.push((hook, vm) => {
// Convert code blocks to previews // Convert code blocks to previews
hook.afterEach(function (html, next) { hook.afterEach((html, next) => {
const domParser = new DOMParser(); const domParser = new DOMParser();
const doc = domParser.parseFromString(html, 'text/html'); const doc = domParser.parseFromString(html, 'text/html');
@ -111,7 +108,7 @@
</button> </button>
`; `;
[...doc.querySelectorAll('code[class^="lang-"]')].map(code => { [...doc.querySelectorAll('code[class^="lang-"]')].forEach(code => {
if (code.classList.contains('preview')) { if (code.classList.contains('preview')) {
const isExpanded = code.classList.contains('expanded'); const isExpanded = code.classList.contains('expanded');
const pre = code.closest('pre'); const pre = code.closest('pre');
@ -119,12 +116,6 @@
const toggleId = `code-block-toggle-${count}`; const toggleId = `code-block-toggle-${count}`;
const reactPre = getAdjacentExample('react', pre); const reactPre = getAdjacentExample('react', pre);
const hasReact = reactPre !== null; const hasReact = reactPre !== null;
const examples = [
{
name: 'HTML',
codeBlock: pre
}
];
pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, '')); pre.setAttribute('data-lang', pre.getAttribute('data-lang').replace(/ preview$/, ''));
pre.setAttribute('aria-labelledby', toggleId); pre.setAttribute('aria-labelledby', toggleId);
@ -182,7 +173,7 @@
`; `;
pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body); pre.replaceWith(domParser.parseFromString(codeBlock, 'text/html').body);
if (reactPre) reactPre.remove(); reactPre?.remove();
count++; count++;
} }
@ -201,12 +192,12 @@
// Horizontal resizing // Horizontal resizing
hook.doneEach(() => { hook.doneEach(() => {
[...document.querySelectorAll('.code-block__preview')].map(preview => { [...document.querySelectorAll('.code-block__preview')].forEach(preview => {
const resizer = preview.querySelector('.code-block__resizer'); const resizer = preview.querySelector('.code-block__resizer');
let startX; let startX;
let startWidth; let startWidth;
const dragStart = event => { function dragStart(event) {
startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX; startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10); startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10);
preview.classList.add('code-block__preview--dragging'); preview.classList.add('code-block__preview--dragging');
@ -215,21 +206,23 @@
document.documentElement.addEventListener('touchmove', dragMove, false); document.documentElement.addEventListener('touchmove', dragMove, false);
document.documentElement.addEventListener('mouseup', dragStop, false); document.documentElement.addEventListener('mouseup', dragStop, false);
document.documentElement.addEventListener('touchend', dragStop, false); document.documentElement.addEventListener('touchend', dragStop, false);
}; }
const dragMove = event => { function dragMove(event) {
setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX); setWidth(startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX);
}; }
const dragStop = event => { function dragStop() {
preview.classList.remove('code-block__preview--dragging'); preview.classList.remove('code-block__preview--dragging');
document.documentElement.removeEventListener('mousemove', dragMove, false); document.documentElement.removeEventListener('mousemove', dragMove, false);
document.documentElement.removeEventListener('touchmove', dragMove, false); document.documentElement.removeEventListener('touchmove', dragMove, false);
document.documentElement.removeEventListener('mouseup', dragStop, false); document.documentElement.removeEventListener('mouseup', dragStop, false);
document.documentElement.removeEventListener('touchend', dragStop, false); document.documentElement.removeEventListener('touchend', dragStop, false);
}; }
const setWidth = width => (preview.style.width = width + 'px'); function setWidth(width) {
preview.style.width = `${width}px`;
}
resizer.addEventListener('mousedown', dragStart); resizer.addEventListener('mousedown', dragStart);
resizer.addEventListener('touchstart', dragStart); resizer.addEventListener('touchstart', dragStart);
@ -240,7 +233,6 @@
// Toggle source mode // Toggle source mode
document.addEventListener('click', event => { document.addEventListener('click', event => {
const button = event.target.closest('button'); const button = event.target.closest('button');
const codeBlock = button?.closest('.code-block');
if (button?.classList.contains('code-block__button--html')) { if (button?.classList.contains('code-block__button--html')) {
setFlavor('html'); setFlavor('html');
@ -251,7 +243,7 @@
} }
// Update flavor buttons // Update flavor buttons
[...document.querySelectorAll('.code-block')].map(codeBlock => { [...document.querySelectorAll('.code-block')].forEach(codeBlock => {
codeBlock codeBlock
.querySelector('.code-block__button--html') .querySelector('.code-block__button--html')
?.classList.toggle('code-block__button--selected', flavor === 'html'); ?.classList.toggle('code-block__button--selected', flavor === 'html');
@ -310,8 +302,7 @@
if (!isReact) { if (!isReact) {
htmlTemplate = htmlTemplate =
`<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` + `<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${version}/dist/shoelace.js"></script>\n` +
'\n' + `\n${htmlExample}`;
htmlExample;
jsTemplate = ''; jsTemplate = '';
} }
@ -322,13 +313,11 @@
`import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` + `import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` +
`import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` + `import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` +
`import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/utilities/base-path';\n` + `import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/utilities/base-path';\n` +
'\n' + `\n` +
`// Set the base path for Shoelace assets\n` + `// Set the base path for Shoelace assets\n` +
`setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/')\n` + `setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${version}/dist/')\n` +
'\n' + `\n${convertModuleLinks(reactExample)}\n` +
convertModuleLinks(reactExample) + `\n` +
'\n' +
'\n' +
`ReactDOM.render(<App />, document.getElementById('root'));`; `ReactDOM.render(<App />, document.getElementById('root'));`;
} }

Wyświetl plik

@ -20,7 +20,7 @@
<tbody> <tbody>
${props ${props
.map(prop => { .map(prop => {
const hasAttribute = !!prop.attribute; const hasAttribute = typeof prop.attribute !== 'undefined';
const isAttributeDifferent = prop.attribute !== prop.name; const isAttributeDifferent = prop.attribute !== prop.name;
let attributeInfo = ''; let attributeInfo = '';
@ -244,19 +244,19 @@
function getDependencies(tag) { function getDependencies(tag) {
const component = allComponents.find(c => c.tagName === tag); const component = allComponents.find(c => c.tagName === tag);
if (!component || !Array.isArray(component.dependencies)) { if (!component || !Array.isArray(component.dependencies)) {
return []; return;
} }
component.dependencies?.map(tag => { component.dependencies?.forEach(dependentTag => {
if (!dependencies.includes(tag)) { if (!dependencies.includes(dependentTag)) {
dependencies.push(tag); dependencies.push(dependentTag);
} }
getDependencies(tag); getDependencies(dependentTag);
}); });
} }
getDependencies(targetComponent); getDependencies(targetComponent);
dependencies.sort().map(tag => { dependencies.sort().forEach(tag => {
const li = document.createElement('li'); const li = document.createElement('li');
li.innerHTML = `<code>&lt;${tag}&gt;</code>`; li.innerHTML = `<code>&lt;${tag}&gt;</code>`;
ul.appendChild(li); ul.appendChild(li);
@ -266,7 +266,11 @@
} }
function escapeHtml(html) { function escapeHtml(html) {
return (html + '') if (typeof html === 'undefined') {
return '';
}
return html
.toString()
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
@ -277,8 +281,8 @@
function getAllComponents(metadata) { function getAllComponents(metadata) {
const allComponents = []; const allComponents = [];
metadata.modules?.map(module => { metadata.modules?.forEach(module => {
module.declarations?.map(declaration => { module.declarations?.forEach(declaration => {
if (declaration.customElement) { if (declaration.customElement) {
// Generate the dist path based on the src path and attach it to the component // Generate the dist path based on the src path and attach it to the component
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js'); declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
@ -299,8 +303,8 @@
throw new Error('Docsify must be loaded before installing this plugin.'); throw new Error('Docsify must be loaded before installing this plugin.');
} }
window.$docsify.plugins.push((hook, vm) => { window.$docsify.plugins.push(hook => {
hook.mounted(async function () { hook.mounted(async () => {
const metadata = await customElements; const metadata = await customElements;
const target = document.querySelector('.app-name'); const target = document.querySelector('.app-name');
@ -330,7 +334,7 @@
target.appendChild(buttons); target.appendChild(buttons);
}); });
hook.beforeEach(async function (content, next) { hook.beforeEach(async (content, next) => {
const metadata = await customElements; const metadata = await customElements;
// Replace %VERSION% placeholders // Replace %VERSION% placeholders
@ -342,15 +346,23 @@
let result = ''; let result = '';
if (!component) { if (!component) {
console.error('Component not found in metadata: ' + tag); console.error(`Component not found in metadata: ${tag}`);
return next(content); return next(content);
} }
let badgeType = 'neutral'; let badgeType = 'neutral';
if (component.status === 'stable') badgeType = 'primary'; if (component.status === 'stable') {
if (component.status === 'experimental') badgeType = 'warning'; badgeType = 'primary';
if (component.status === 'planned') badgeType = 'neutral'; }
if (component.status === 'deprecated') badgeType = 'danger'; if (component.status === 'experimental') {
badgeType = 'warning';
}
if (component.status === 'planned') {
badgeType = 'neutral';
}
if (component.status === 'deprecated') {
badgeType = 'danger';
}
result += ` result += `
<div class="component-header"> <div class="component-header">
@ -379,7 +391,7 @@
let result = ''; let result = '';
if (!component) { if (!component) {
console.error('Component not found in metadata: ' + tag); console.error(`Component not found in metadata: ${tag}`);
return next(content); return next(content);
} }
@ -521,11 +533,11 @@
}); });
// Wrap tables so we can scroll them horizontally when needed // Wrap tables so we can scroll them horizontally when needed
hook.doneEach(function () { hook.doneEach(() => {
const content = document.querySelector('.content'); const content = document.querySelector('.content');
const tables = [...content.querySelectorAll('table')]; const tables = [...content.querySelectorAll('table')];
tables.map(table => { tables.forEach(table => {
table.outerHTML = ` table.outerHTML = `
<div class="table-wrapper"> <div class="table-wrapper">
${table.outerHTML} ${table.outerHTML}

Wyświetl plik

@ -7,7 +7,7 @@
// Docsify generates pages dynamically and asynchronously, so when a reload happens, the scroll position can't be // Docsify generates pages dynamically and asynchronously, so when a reload happens, the scroll position can't be
// be restored immediately. This plugin waits until Docsify loads the page and then restores it. // be restored immediately. This plugin waits until Docsify loads the page and then restores it.
// //
window.$docsify.plugins.push((hook, vm) => { window.$docsify.plugins.push(hook => {
hook.ready(() => { hook.ready(() => {
// Restore // Restore
const scrollTop = sessionStorage.getItem('bs-scroll'); const scrollTop = sessionStorage.getItem('bs-scroll');
@ -16,7 +16,7 @@
} }
// Remember // Remember
document.addEventListener('scroll', event => { document.addEventListener('scroll', () => {
sessionStorage.setItem('bs-scroll', document.documentElement.scrollTop); sessionStorage.setItem('bs-scroll', document.documentElement.scrollTop);
}); });
}); });

Wyświetl plik

@ -1,11 +1,12 @@
/* global lunr */
(() => { (() => {
if (!window.$docsify) { if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.'); throw new Error('Docsify must be loaded before installing this plugin.');
} }
window.$docsify.plugins.push((hook, vm) => { window.$docsify.plugins.push(hook => {
// Append the search box to the sidebar // Append the search box to the sidebar
hook.mounted(function () { hook.mounted(() => {
const appName = document.querySelector('.sidebar .app-name'); const appName = document.querySelector('.sidebar .app-name');
const searchBox = document.createElement('div'); const searchBox = document.createElement('div');
searchBox.classList.add('search-box'); searchBox.classList.add('search-box');
@ -76,7 +77,6 @@
`; `;
document.body.append(siteSearch); document.body.append(siteSearch);
const searchButtons = [...document.querySelectorAll('[data-site-search]')];
const overlay = siteSearch.querySelector('.site-search__overlay'); const overlay = siteSearch.querySelector('.site-search__overlay');
const panel = siteSearch.querySelector('.site-search__panel'); const panel = siteSearch.querySelector('.site-search__panel');
const input = siteSearch.querySelector('.site-search__input'); const input = siteSearch.querySelector('.site-search__input');
@ -89,7 +89,7 @@
let map; let map;
// Load search data // Load search data
const searchData = fetch('../../../search.json') fetch('../../../search.json')
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
searchIndex = lunr.Index.load(data.searchIndex); searchIndex = lunr.Index.load(data.searchIndex);
@ -203,7 +203,7 @@
} }
// Update the selected item // Update the selected item
items.map(item => { items.forEach(item => {
if (item === nextEl) { if (item === nextEl) {
item.setAttribute('aria-selected', 'true'); item.setAttribute('aria-selected', 'true');
nextEl.scrollIntoView({ block: 'nearest' }); nextEl.scrollIntoView({ block: 'nearest' });
@ -211,8 +211,6 @@
item.setAttribute('aria-selected', 'false'); item.setAttribute('aria-selected', 'false');
} }
}); });
return;
} }
} }
@ -228,27 +226,39 @@
matches = searchIndex.search(`${query}~2`); matches = searchIndex.search(`${query}~2`);
} }
let hasResults = hasQuery && matches.length > 0; const hasResults = hasQuery && matches.length > 0;
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults); siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults); siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false'); panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
results.innerHTML = ''; results.innerHTML = '';
matches.map((match, index) => { matches.forEach((match, index) => {
const page = map[match.ref]; const page = map[match.ref];
const li = document.createElement('li'); const li = document.createElement('li');
const a = document.createElement('a'); const a = document.createElement('a');
let icon = 'file-text'; let icon = 'file-text';
if (page.url.includes('getting-started/')) icon = 'lightbulb'; if (page.url.includes('getting-started/')) {
if (page.url.includes('resources/')) icon = 'book'; icon = 'lightbulb';
if (page.url.includes('components/')) icon = 'puzzle'; }
if (page.url.includes('tokens/')) icon = 'palette2'; if (page.url.includes('resources/')) {
if (page.url.includes('utilities/')) icon = 'wrench'; icon = 'book';
if (page.url.includes('tutorials/')) icon = 'joystick'; }
if (page.url.includes('components/')) {
icon = 'puzzle';
}
if (page.url.includes('tokens/')) {
icon = 'palette2';
}
if (page.url.includes('utilities/')) {
icon = 'wrench';
}
if (page.url.includes('tutorials/')) {
icon = 'joystick';
}
a.href = $docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`; a.href = window.$docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
a.innerHTML = ` a.innerHTML = `
<div class="site-search__result-icon"> <div class="site-search__result-icon">
<sl-icon name="${icon}" aria-hidden="true"></sl-icon> <sl-icon name="${icon}" aria-hidden="true"></sl-icon>

Wyświetl plik

@ -3,8 +3,8 @@
throw new Error('Docsify must be loaded before installing this plugin.'); throw new Error('Docsify must be loaded before installing this plugin.');
} }
window.$docsify.plugins.push((hook, vm) => { window.$docsify.plugins.push(hook => {
hook.mounted(function () { hook.mounted(() => {
function getTheme() { function getTheme() {
return localStorage.getItem('theme') || 'auto'; return localStorage.getItem('theme') || 'auto';
} }
@ -12,9 +12,8 @@
function isDark() { function isDark() {
if (theme === 'auto') { if (theme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches; return window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
return theme === 'dark';
} }
return theme === 'dark';
} }
function setTheme(newTheme) { function setTheme(newTheme) {
@ -62,7 +61,7 @@
menu.addEventListener('sl-select', event => setTheme(event.detail.item.value)); menu.addEventListener('sl-select', event => setTheme(event.detail.item.value));
// Update the theme when the preference changes // Update the theme when the preference changes
window.matchMedia('(prefers-color-scheme: dark)').addListener(event => setTheme(theme)); window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setTheme(theme));
// Toggle themes when pressing backslash // Toggle themes when pressing backslash
document.addEventListener('keydown', event => { document.addEventListener('keydown', event => {

Wyświetl plik

@ -85,7 +85,7 @@ npm install
Once you've cloned the repo, run the following command to spin up the Shoelace dev server. Once you've cloned the repo, run the following command to spin up the Shoelace dev server.
```bash ```bash
npm start npm run start
``` ```
After the initial build, a browser will open automatically to a local version of the docs. The documentation is powered by Docsify, which uses raw markdown files to generate pages on the fly. After the initial build, a browser will open automatically to a local version of the docs. The documentation is powered by Docsify, which uses raw markdown files to generate pages on the fly.
@ -113,7 +113,7 @@ For more information about running and building the project locally, refer to `R
Shoelace uses [Web Test Runner](https://modern-web.dev/guides/test-runner/getting-started/) for testing. To launch the test runner during development, open a terminal and launch the dev server. Shoelace uses [Web Test Runner](https://modern-web.dev/guides/test-runner/getting-started/) for testing. To launch the test runner during development, open a terminal and launch the dev server.
```bash ```bash
npm start npm run start
``` ```
In a second terminal window, launch the test runner. In a second terminal window, launch the test runner.
@ -124,13 +124,20 @@ npm run test:watch
Follow the on-screen instructions to work with the test runner. Tests will automatically re-run as you make changes. Follow the on-screen instructions to work with the test runner. Tests will automatically re-run as you make changes.
To run tests only once, make sure to build the project first. To run all tests only once:
```bash ```bash
npm run build
npm run test npm run test
``` ```
To run just one test file:
If the test file is `src/components/breadcrumb-item.test.ts`, then `<nameOfTestFile>` would be `breadcrumb-item`.
```bash
npm run test -- --group <nameOfTestFile>
```
## Best Practices ## Best Practices
The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the project. The following is a non-exhaustive list of conventions, patterns, and best practices we try to follow. As a contributor, we ask that you make a good faith effort to follow them as well. This ensures consistency and maintainability throughout the project.

Wyświetl plik

@ -1,4 +1,5 @@
export default { export default {
'*.{js,ts,jsx,tsx,json,html,xml,css,scss,sass,md}': 'cspell --no-must-find-files', '*.{js,ts,json,html,xml,css,scss,sass,md}': 'cspell --no-must-find-files',
'src/**/*.{js,ts}': 'eslint --max-warnings 0 --fix',
'*': 'prettier --write --ignore-unknown' '*': 'prettier --write --ignore-unknown'
}; };

15384
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -32,17 +32,19 @@
"scripts": { "scripts": {
"start": "node scripts/build.js --bundle --serve", "start": "node scripts/build.js --bundle --serve",
"build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"", "build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"",
"verify": "npm run spellcheck && npm run prettier:check && npm run ts-check && npm run build && npm run test", "verify": "npm run spellcheck && npm run prettier:check && npm run lint && npm run ts-check && npm run test && npm run build",
"prepublishOnly": "npm run verify", "prepublishOnly": "npm run verify",
"prettier": "prettier --write --loglevel warn .", "prettier": "prettier --write --loglevel warn .",
"prettier:check": "prettier --check --loglevel warn .", "prettier:check": "prettier --check --loglevel warn .",
"ts-check": "tsc --noEmit", "lint": "eslint src --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"ts-check": "tsc --noEmit --project ./tsconfig.json",
"create": "plop --plopfile scripts/plop/plopfile.js", "create": "plop --plopfile scripts/plop/plopfile.js",
"test": "web-test-runner \"src/**/*.test.ts\" --node-resolve --playwright --browsers chromium firefox webkit", "test": "node scripts/build.js --bundle && web-test-runner",
"test:watch": "web-test-runner \"src/**/*.test.ts\" --node-resolve --playwright --browsers chromium firefox webkit --watch", "test:watch": "web-test-runner --watch",
"spellcheck": "cspell \"**/*.{js,ts,jsx,tsx,json,html,xml,css,scss,sass,md}\" --no-progress", "spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress",
"list-outdated-dependencies": "npm-check-updates --format repo --peer", "list-outdated-dependencies": "npm-check-updates --format repo --peer",
"update-dependencies": "npm-check-updates --peer -u && npm install && npm run prettier && npm run verify" "update-dependencies": "npm-check-updates --peer -u && npm install && npm run lint:fix && npm run prettier && npm run verify"
}, },
"engines": { "engines": {
"node": ">=14.15.0" "node": ">=14.15.0"
@ -54,13 +56,17 @@
"@shoelace-style/localize": "^2.1.3", "@shoelace-style/localize": "^2.1.3",
"color": "4.1", "color": "4.1",
"lit": "^2.1.0", "lit": "^2.1.0",
"lit-html": "^2.1.1",
"qr-creator": "^1.0.0" "qr-creator": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@custom-elements-manifest/analyzer": "^0.5.7", "@custom-elements-manifest/analyzer": "^0.5.7",
"@open-wc/testing": "^3.0.3", "@open-wc/testing": "^3.0.3",
"@types/color": "^3.0.2", "@types/color": "^3.0.2",
"@types/mocha": "^9.0.0",
"@types/react": "^17.0.38", "@types/react": "^17.0.38",
"@typescript-eslint/eslint-plugin": "^5.9.0",
"@typescript-eslint/parser": "^5.9.0",
"@web/dev-server-esbuild": "^0.2.16", "@web/dev-server-esbuild": "^0.2.16",
"@web/test-runner": "^0.13.23", "@web/test-runner": "^0.13.23",
"@web/test-runner-playwright": "^0.8.8", "@web/test-runner-playwright": "^0.8.8",
@ -73,6 +79,13 @@
"del": "^6.0.0", "del": "^6.0.0",
"download": "^8.0.0", "download": "^8.0.0",
"esbuild": "^0.14.10", "esbuild": "^0.14.10",
"eslint": "^8.6.0",
"eslint-plugin-chai-expect": "^3.0.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-lit-a11y": "^2.2.0",
"eslint-plugin-wc": "^1.3.2",
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"get-port": "^6.0.0", "get-port": "^6.0.0",
"globby": "^12.0.2", "globby": "^12.0.2",
@ -88,6 +101,7 @@
"sinon": "^12.0.1", "sinon": "^12.0.1",
"strip-css-comments": "^5.0.0", "strip-css-comments": "^5.0.0",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.5.4" "typescript": "^4.5.4",
"utility-types": "^3.10.0"
} }
} }

Wyświetl plik

@ -32,7 +32,7 @@ fs.mkdirSync(outdir, { recursive: true });
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' }); execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' }); execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' }); execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' }); if (types) execSync(`tsc --project ./tsconfig.prod.json --outdir "${outdir}"`, { stdio: 'inherit' });
} catch (err) { } catch (err) {
console.error(chalk.red(err)); console.error(chalk.red(err));
process.exit(1); process.exit(1);

Wyświetl plik

@ -1,8 +1,8 @@
// //
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json // This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
// //
import commandLineArgs from 'command-line-args';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import commandLineArgs from 'command-line-args';
const { outdir } = commandLineArgs({ name: 'outdir', type: String }); const { outdir } = commandLineArgs({ name: 'outdir', type: String });

Wyświetl plik

@ -1,7 +1,7 @@
import chalk from 'chalk';
import fs from 'fs'; import fs from 'fs';
import del from 'del';
import path from 'path'; import path from 'path';
import chalk from 'chalk';
import del from 'del';
import { pascalCase } from 'pascal-case'; import { pascalCase } from 'pascal-case';
import prettier from 'prettier'; import prettier from 'prettier';
import prettierConfig from '../prettier.config.cjs'; import prettierConfig from '../prettier.config.cjs';

Wyświetl plik

@ -1,8 +1,8 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import styles from './{{ tagWithoutPrefix tag }}.styles'; import styles from './{{ tagWithoutPrefix tag }}.styles';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,8 +1,4 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type {{ properCase tag }} from './{{ tagWithoutPrefix tag }}';
describe('<{{ tag }}>', () => { describe('<{{ tag }}>', () => {
it('should render a component', async () => { it('should render a component', async () => {

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,33 +1,31 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlAlert from './alert'; import type SlAlert from './alert';
describe('<sl-alert>', () => { describe('<sl-alert>', () => {
it('should be visible with the open attribute', async () => { it('should be visible with the open attribute', async () => {
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `); const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.false; expect(base.hidden).to.be.false;
}); });
it('should not be visible without the open attribute', async () => { it('should not be visible without the open attribute', async () => {
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `); const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.true; expect(base.hidden).to.be.true;
}); });
it('should emit sl-show and sl-after-show when calling show()', async () => { it('should emit sl-show and sl-after-show when calling show()', async () => {
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `); const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler); el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler); el.addEventListener('sl-after-show', afterShowHandler);
el.show(); void el.show();
await waitUntil(() => showHandler.calledOnce); await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce); await waitUntil(() => afterShowHandler.calledOnce);
@ -39,13 +37,13 @@ describe('<sl-alert>', () => {
it('should emit sl-hide and sl-after-hide when calling hide()', async () => { it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `); const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler); el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler); el.addEventListener('sl-after-hide', afterHideHandler);
el.hide(); void el.hide();
await waitUntil(() => hideHandler.calledOnce); await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce); await waitUntil(() => afterHideHandler.calledOnce);
@ -57,7 +55,7 @@ describe('<sl-alert>', () => {
it('should emit sl-show and sl-after-show when setting open = true', async () => { it('should emit sl-show and sl-after-show when setting open = true', async () => {
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `); const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
@ -75,7 +73,7 @@ describe('<sl-alert>', () => {
it('should emit sl-hide and sl-after-hide when setting open = false', async () => { it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `); const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();

Wyświetl plik

@ -1,14 +1,12 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { animateTo, stopAnimations } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import styles from './alert.styles'; import styles from './alert.styles';
import '~/components/icon-button/icon-button';
import '../icon-button/icon-button'; import { animateTo, stopAnimations } from '~/internal/animate';
import { emit, waitForEvent } from '~/internal/event';
import { watch } from '~/internal/watch';
import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry';
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' }); const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
@ -41,7 +39,7 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
export default class SlAlert extends LitElement { export default class SlAlert extends LitElement {
static styles = styles; static styles = styles;
private autoHideTimeout: any; private autoHideTimeout: NodeJS.Timeout;
@query('[part="base"]') base: HTMLElement; @query('[part="base"]') base: HTMLElement;
@ -58,7 +56,7 @@ export default class SlAlert extends LitElement {
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with * The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`. * the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`.
*/ */
@property({ type: Number }) duration: number = Infinity; @property({ type: Number }) duration = Infinity;
firstUpdated() { firstUpdated() {
this.base.hidden = !this.open; this.base.hidden = !this.open;
@ -67,7 +65,7 @@ export default class SlAlert extends LitElement {
/** Shows the alert. */ /** Shows the alert. */
async show() { async show() {
if (this.open) { if (this.open) {
return; return undefined;
} }
this.open = true; this.open = true;
@ -77,7 +75,7 @@ export default class SlAlert extends LitElement {
/** Hides the alert */ /** Hides the alert */
async hide() { async hide() {
if (!this.open) { if (!this.open) {
return; return undefined;
} }
this.open = false; this.open = false;
@ -91,7 +89,7 @@ export default class SlAlert extends LitElement {
*/ */
async toast() { async toast() {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
if (!toastStack.parentElement) { if (toastStack.parentElement === null) {
document.body.append(toastStack); document.body.append(toastStack);
} }
@ -99,8 +97,9 @@ export default class SlAlert extends LitElement {
// Wait for the toast stack to render // Wait for the toast stack to render
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.clientWidth; // force a reflow for the initial transition // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
this.show(); this.clientWidth;
void this.show();
}); });
this.addEventListener( this.addEventListener(
@ -110,7 +109,7 @@ export default class SlAlert extends LitElement {
resolve(); resolve();
// Remove the toast stack from the DOM when there are no more alerts // Remove the toast stack from the DOM when there are no more alerts
if (!toastStack.querySelector('sl-alert')) { if (toastStack.querySelector('sl-alert') === null) {
toastStack.remove(); toastStack.remove();
} }
}, },
@ -122,12 +121,14 @@ export default class SlAlert extends LitElement {
restartAutoHide() { restartAutoHide() {
clearTimeout(this.autoHideTimeout); clearTimeout(this.autoHideTimeout);
if (this.open && this.duration < Infinity) { if (this.open && this.duration < Infinity) {
this.autoHideTimeout = setTimeout(() => this.hide(), this.duration); this.autoHideTimeout = setTimeout(() => {
void this.hide();
}, this.duration);
} }
} }
handleCloseClick() { handleCloseClick() {
this.hide(); void this.hide();
} }
handleMouseMove() { handleMouseMove() {

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,8 +1,4 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlAnimatedImage from './animated-image';
describe('<sl-animated-image>', () => { describe('<sl-animated-image>', () => {
it('should render a component', async () => { it('should render a component', async () => {

Wyświetl plik

@ -1,10 +1,9 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js'; import { customElement, property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch';
import { emit } from '../../internal/event';
import styles from './animated-image.styles'; import styles from './animated-image.styles';
import '~/components/icon/icon';
import '../icon/icon'; import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -63,7 +62,7 @@ export default class SlAnimatedImage extends LitElement {
} }
@watch('play') @watch('play')
async handlePlayChange() { handlePlayChange() {
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this // When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
// won't trigger another request. // won't trigger another request.
if (this.play) { if (this.play) {

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,9 +1,9 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, queryAsync } from 'lit/decorators.js'; import { customElement, property, queryAsync } from 'lit/decorators.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { animations } from './animations';
import styles from './animation.styles'; import styles from './animation.styles';
import { animations } from './animations';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -20,7 +20,7 @@ import styles from './animation.styles';
export default class SlAnimation extends LitElement { export default class SlAnimation extends LitElement {
static styles = styles; static styles = styles;
private animation: Animation; private animation?: Animation;
private hasStarted = false; private hasStarted = false;
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>; @queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
@ -56,13 +56,13 @@ export default class SlAnimation extends LitElement {
@property() fill: FillMode = 'auto'; @property() fill: FillMode = 'auto';
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */ /** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
@property({ type: Number }) iterations: number = Infinity; @property({ type: Number }) iterations = Infinity;
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */ /** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0; @property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */ /** The keyframes to use for the animation. If this is set, `name` will be ignored. */
@property({ attribute: false }) keyframes: Keyframe[]; @property({ attribute: false }) keyframes?: Keyframe[];
/** /**
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this * Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
@ -73,18 +73,18 @@ export default class SlAnimation extends LitElement {
/** Gets and sets the current animation time. */ /** Gets and sets the current animation time. */
get currentTime(): number { get currentTime(): number {
return this.animation?.currentTime || 0; return this.animation?.currentTime ?? 0;
} }
set currentTime(time: number) { set currentTime(time: number) {
if (this.animation) { if (typeof this.animation !== 'undefined') {
this.animation.currentTime = time; this.animation.currentTime = time;
} }
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.createAnimation(); void this.createAnimation();
this.handleAnimationCancel = this.handleAnimationCancel.bind(this); this.handleAnimationCancel = this.handleAnimationCancel.bind(this);
this.handleAnimationFinish = this.handleAnimationFinish.bind(this); this.handleAnimationFinish = this.handleAnimationFinish.bind(this);
} }
@ -104,12 +104,12 @@ export default class SlAnimation extends LitElement {
@watch('iterations') @watch('iterations')
@watch('iterationsStart') @watch('iterationsStart')
@watch('keyframes') @watch('keyframes')
async handleAnimationChange() { handleAnimationChange() {
if (!this.hasUpdated) { if (!this.hasUpdated) {
return; return;
} }
this.createAnimation(); void this.createAnimation();
} }
handleAnimationFinish() { handleAnimationFinish() {
@ -126,39 +126,43 @@ export default class SlAnimation extends LitElement {
@watch('play') @watch('play')
handlePlayChange() { handlePlayChange() {
if (this.animation) { if (typeof this.animation !== 'undefined') {
if (this.play && !this.hasStarted) { if (this.play && !this.hasStarted) {
this.hasStarted = true; this.hasStarted = true;
emit(this, 'sl-start'); emit(this, 'sl-start');
} }
this.play ? this.animation.play() : this.animation.pause(); if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
return true; return true;
} else {
return false;
} }
return false;
} }
@watch('playbackRate') @watch('playbackRate')
handlePlaybackRateChange() { handlePlaybackRateChange() {
if (this.animation) { if (typeof this.animation !== 'undefined') {
this.animation.playbackRate = this.playbackRate; this.animation.playbackRate = this.playbackRate;
} }
} }
handleSlotChange() { handleSlotChange() {
this.destroyAnimation(); this.destroyAnimation();
this.createAnimation(); void this.createAnimation();
} }
async createAnimation() { async createAnimation() {
const easing = animations.easings[this.easing] || this.easing; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- The specified easing may not exist
const keyframes: Keyframe[] = this.keyframes ? this.keyframes : (animations as any)[this.name]; const easing = animations.easings[this.easing] ?? this.easing;
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
const slot = await this.defaultSlot; const slot = await this.defaultSlot;
const element = slot.assignedElements()[0] as HTMLElement; const element = slot.assignedElements()[0] as HTMLElement | undefined;
if (!element) { if (typeof element === 'undefined' || typeof keyframes === 'undefined') {
return false; return false;
} }
@ -188,7 +192,7 @@ export default class SlAnimation extends LitElement {
} }
destroyAnimation() { destroyAnimation() {
if (this.animation) { if (typeof this.animation !== 'undefined') {
this.animation.cancel(); this.animation.cancel();
this.animation.removeEventListener('cancel', this.handleAnimationCancel); this.animation.removeEventListener('cancel', this.handleAnimationCancel);
this.animation.removeEventListener('finish', this.handleAnimationFinish); this.animation.removeEventListener('finish', this.handleAnimationFinish);
@ -198,16 +202,12 @@ export default class SlAnimation extends LitElement {
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */ /** Clears all KeyframeEffects caused by this animation and aborts its playback. */
cancel() { cancel() {
try { this.animation?.cancel();
this.animation.cancel();
} catch {}
} }
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */ /** Sets the playback time to the end of the animation corresponding to the current playback direction. */
finish() { finish() {
try { this.animation?.finish();
this.animation.finish();
} catch {}
} }
render() { render() {

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlAvatar from './avatar'; import type SlAvatar from './avatar';
describe('<sl-avatar>', () => { describe('<sl-avatar>', () => {
let el: SlAvatar; let el: SlAvatar;
describe('when provided no parameters', async () => { describe('when provided no parameters', () => {
before(async () => { before(async () => {
el = await fixture<SlAvatar>(html` <sl-avatar label="Avatar"></sl-avatar> `); el = await fixture<SlAvatar>(html` <sl-avatar label="Avatar"></sl-avatar> `);
}); });
@ -15,14 +13,14 @@ describe('<sl-avatar>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should default to circle styling', async () => { it('should default to circle styling', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(el.getAttribute('shape')).to.eq('circle'); expect(el.getAttribute('shape')).to.eq('circle');
expect(part.classList.value.trim()).to.eq('avatar avatar--circle'); expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
}); });
}); });
describe('when provided an image and label parameter', async () => { describe('when provided an image and label parameter', () => {
const image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const label = 'Small transparent square'; const label = 'Small transparent square';
before(async () => { before(async () => {
@ -40,20 +38,20 @@ describe('<sl-avatar>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('renders "image" part, with src and a role of presentation', async () => { it('renders "image" part, with src and a role of presentation', () => {
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement; const part = el.shadowRoot!.querySelector('[part="image"]')!;
expect(part.getAttribute('src')).to.eq(image); expect(part.getAttribute('src')).to.eq(image);
}); });
it('renders the label attribute in the "base" part', async () => { it('renders the label attribute in the "base" part', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.getAttribute('aria-label')).to.eq(label); expect(part.getAttribute('aria-label')).to.eq(label);
}); });
}); });
describe('when provided initials parameter', async () => { describe('when provided initials parameter', () => {
const initials = 'SL'; const initials = 'SL';
before(async () => { before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}" label="Avatar"></sl-avatar>`); el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}" label="Avatar"></sl-avatar>`);
@ -63,8 +61,8 @@ describe('<sl-avatar>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('renders "initials" part, with initials as the text node', async () => { it('renders "initials" part, with initials as the text node', () => {
const part = el.shadowRoot?.querySelector('[part="initials"]') as HTMLImageElement; const part = el.shadowRoot!.querySelector<HTMLElement>('[part="initials"]')!;
expect(part.innerText).to.eq(initials); expect(part.innerText).to.eq(initials);
}); });
@ -73,15 +71,15 @@ describe('<sl-avatar>', () => {
['square', 'rounded', 'circle'].forEach(shape => { ['square', 'rounded', 'circle'].forEach(shape => {
describe(`when passed a shape attribute ${shape}`, () => { describe(`when passed a shape attribute ${shape}`, () => {
before(async () => { before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape as any}" label="Shaped avatar"></sl-avatar>`); el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape}" label="Shaped avatar"></sl-avatar>`);
}); });
it('passes accessibility test', async () => { it('passes accessibility test', async () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('appends the appropriate class on the "base" part', async () => { it('appends the appropriate class on the "base" part', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(el.getAttribute('shape')).to.eq(shape); expect(el.getAttribute('shape')).to.eq(shape);
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`); expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
@ -89,7 +87,7 @@ describe('<sl-avatar>', () => {
}); });
}); });
describe('when passed a <span>, on slot "icon"', async () => { describe('when passed a <span>, on slot "icon"', () => {
before(async () => { before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar label="Avatar"><span slot="icon">random content</span></sl-avatar>`); el = await fixture<SlAvatar>(html`<sl-avatar label="Avatar"><span slot="icon">random content</span></sl-avatar>`);
}); });
@ -98,13 +96,13 @@ describe('<sl-avatar>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should accept as an assigned child in the shadow root', async () => { it('should accept as an assigned child in the shadow root', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=icon]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=icon]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true }) as HTMLElement[];
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
const span = <HTMLElement>childNodes[0]; const span = childNodes[0];
expect(span.innerHTML).to.eq('random content'); expect(span.innerHTML).to.eq('random content');
}); });
}); });

Wyświetl plik

@ -2,8 +2,7 @@ import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import styles from './avatar.styles'; import styles from './avatar.styles';
import '~/components/icon/icon';
import '../icon/icon';
/** /**
* @since 2.0 * @since 2.0
@ -27,13 +26,13 @@ export default class SlAvatar extends LitElement {
@state() private hasError = false; @state() private hasError = false;
/** The image source to use for the avatar. */ /** The image source to use for the avatar. */
@property() image: string; @property() image?: string;
/** A label to use to describe the avatar to assistive devices. */ /** A label to use to describe the avatar to assistive devices. */
@property() label: string; @property() label = '';
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */ /** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
@property() initials: string; @property() initials?: string;
/** The shape of the avatar. */ /** The shape of the avatar. */
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle'; @property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
@ -51,7 +50,7 @@ export default class SlAvatar extends LitElement {
role="img" role="img"
aria-label=${this.label} aria-label=${this.label}
> >
${this.initials ${typeof this.initials !== 'undefined'
? html` <div part="initials" class="avatar__initials">${this.initials}</div> ` ? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
: html` : html`
<div part="icon" class="avatar__icon" aria-hidden="true"> <div part="icon" class="avatar__icon" aria-hidden="true">
@ -60,7 +59,7 @@ export default class SlAvatar extends LitElement {
</slot> </slot>
</div> </div>
`} `}
${this.image && !this.hasError ${typeof this.image !== 'undefined' && !this.hasError
? html` ? html`
<img <img
part="image" part="image"

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBadge from './badge'; import type SlBadge from './badge';
describe('<sl-badge>', () => { describe('<sl-badge>', () => {
let el: SlBadge; let el: SlBadge;
describe('when provided no parameters', async () => { describe('when provided no parameters', () => {
before(async () => { before(async () => {
el = await fixture<SlBadge>(html` <sl-badge>Badge</sl-badge> `); el = await fixture<SlBadge>(html` <sl-badge>Badge</sl-badge> `);
}); });
@ -14,21 +12,21 @@ describe('<sl-badge>', () => {
it('should render a component that passes accessibility test, with a role of status on the base part.', async () => { it('should render a component that passes accessibility test, with a role of status on the base part.', async () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.getAttribute('role')).to.eq('status'); expect(part.getAttribute('role')).to.eq('status');
}); });
it('should render the child content provided', async () => { it('should render the child content provided', () => {
expect(el.innerText).to.eq('Badge'); expect(el.innerText).to.eq('Badge');
}); });
it('should default to square styling, with the primary color', async () => { it('should default to square styling, with the primary color', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.classList.value.trim()).to.eq('badge badge--primary'); expect(part.classList.value.trim()).to.eq('badge badge--primary');
}); });
}); });
describe('when provided a pill parameter', async () => { describe('when provided a pill parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `); el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `);
}); });
@ -37,13 +35,13 @@ describe('<sl-badge>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should append the pill class to the classlist to render a pill', async () => { it('should append the pill class to the classlist to render a pill', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill'); expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill');
}); });
}); });
describe('when provided a pulse parameter', async () => { describe('when provided a pulse parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `); el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `);
}); });
@ -52,8 +50,8 @@ describe('<sl-badge>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should append the pulse class to the classlist to render a pulse', async () => { it('should append the pulse class to the classlist to render a pulse', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse'); expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse');
}); });
}); });
@ -61,15 +59,15 @@ describe('<sl-badge>', () => {
['primary', 'success', 'neutral', 'warning', 'danger'].forEach(variant => { ['primary', 'success', 'neutral', 'warning', 'danger'].forEach(variant => {
describe(`when passed a variant attribute ${variant}`, () => { describe(`when passed a variant attribute ${variant}`, () => {
before(async () => { before(async () => {
el = await fixture<SlBadge>(html`<sl-badge variant="${variant as any}">Badge</sl-badge>`); el = await fixture<SlBadge>(html`<sl-badge variant="${variant}">Badge</sl-badge>`);
}); });
it('should render a component that passes accessibility test', async () => { it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should default to square styling, with the primary color', async () => { it('should default to square styling, with the primary color', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`); expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`);
}); });
}); });

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBreadcrumbItem from './breadcrumb-item'; import type SlBreadcrumbItem from './breadcrumb-item';
describe('<sl-breadcrumb-item>', () => { describe('<sl-breadcrumb-item>', () => {
let el: SlBreadcrumbItem; let el: SlBreadcrumbItem;
describe('when not provided a href attribute', async () => { describe('when not provided a href attribute', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `); el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `);
}); });
@ -15,19 +13,19 @@ describe('<sl-breadcrumb-item>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should hide the separator from screen readers', async () => { it('should hide the separator from screen readers', () => {
const separator: HTMLSpanElement = el.shadowRoot.querySelector('[part="separator"]'); const separator = el.shadowRoot!.querySelector<HTMLSpanElement>('[part="separator"]');
expect(separator).attribute('aria-hidden', 'true'); expect(separator).attribute('aria-hidden', 'true');
}); });
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => { it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
const button: HTMLButtonElement = el.shadowRoot.querySelector('[part="label"]'); const button = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="label"]');
expect(button).to.exist; expect(button).to.exist;
expect(button).attribute('type', 'button'); expect(button).attribute('type', 'button');
}); });
}); });
describe('when provided a href attribute', async () => { describe('when provided a href attribute', () => {
describe('and no target', () => { describe('and no target', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumbItem>(html` el = await fixture<SlBreadcrumbItem>(html`
@ -40,7 +38,7 @@ describe('<sl-breadcrumb-item>', () => {
}); });
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => { it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
const hyperlink: HTMLAnchorElement = el.shadowRoot.querySelector('[part="label"]'); const hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/'); expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
}); });
}); });
@ -57,10 +55,10 @@ describe('<sl-breadcrumb-item>', () => {
}); });
describe('should render a HTMLAnchorElement as the part "label"', () => { describe('should render a HTMLAnchorElement as the part "label"', () => {
let hyperlink: HTMLAnchorElement; let hyperlink: HTMLAnchorElement | null;
before(() => { before(() => {
hyperlink = el.shadowRoot.querySelector('[part="label"]'); hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
}); });
it('should use the supplied href value, as the href attribute value', () => { it('should use the supplied href value, as the href attribute value', () => {
@ -87,10 +85,10 @@ describe('<sl-breadcrumb-item>', () => {
}); });
describe('should render a HTMLAnchorElement', () => { describe('should render a HTMLAnchorElement', () => {
let hyperlink: HTMLAnchorElement; let hyperlink: HTMLAnchorElement | null;
before(() => { before(() => {
hyperlink = el.shadowRoot.querySelector('a'); hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('a');
}); });
it('should use the supplied href value, as the href attribute value', () => { it('should use the supplied href value, as the href attribute value', () => {
@ -104,7 +102,7 @@ describe('<sl-breadcrumb-item>', () => {
}); });
}); });
describe('when provided an element in the slot "prefix" to support prefix icons', async () => { describe('when provided an element in the slot "prefix" to support prefix icons', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumbItem>(html` el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item> <sl-breadcrumb-item>
@ -119,19 +117,19 @@ describe('<sl-breadcrumb-item>', () => {
}); });
it('should accept as an assigned child in the shadow root', () => { it('should accept as an assigned child in the shadow root', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=prefix]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=prefix]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
}); });
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => { it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-prefix'); expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
}); });
}); });
describe('when provided an element in the slot "suffix" to support suffix icons', async () => { describe('when provided an element in the slot "suffix" to support suffix icons', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumbItem>(html` el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item> <sl-breadcrumb-item>
@ -146,14 +144,14 @@ describe('<sl-breadcrumb-item>', () => {
}); });
it('should accept as an assigned child in the shadow root', () => { it('should accept as an assigned child in the shadow root', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=suffix]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=suffix]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
}); });
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => { it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const part = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-suffix'); expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
}); });
}); });

Wyświetl plik

@ -2,8 +2,8 @@ import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { HasSlotController } from '../../internal/slot';
import styles from './breadcrumb-item.styles'; import styles from './breadcrumb-item.styles';
import { HasSlotController } from '~/internal/slot';
/** /**
* @since 2.0 * @since 2.0
@ -25,22 +25,22 @@ import styles from './breadcrumb-item.styles';
export default class SlBreadcrumbItem extends LitElement { export default class SlBreadcrumbItem extends LitElement {
static styles = styles; static styles = styles;
private hasSlotController = new HasSlotController(this, 'prefix', 'suffix'); private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
/** /**
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered * Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
* internally. When unset, a button will be rendered instead. * internally. When unset, a button will be rendered instead.
*/ */
@property() href: string; @property() href?: string;
/** Tells the browser where to open the link. Only used when `href` is set. */ /** Tells the browser where to open the link. Only used when `href` is set. */
@property() target: '_blank' | '_parent' | '_self' | '_top'; @property() target?: '_blank' | '_parent' | '_self' | '_top';
/** The `rel` attribute to use on the link. Only used when `href` is set. */ /** The `rel` attribute to use on the link. Only used when `href` is set. */
@property() rel: string = 'noreferrer noopener'; @property() rel = 'noreferrer noopener';
render() { render() {
const isLink = this.href ? true : false; const isLink = typeof this.href !== 'undefined';
return html` return html`
<div <div
@ -62,7 +62,7 @@ export default class SlBreadcrumbItem extends LitElement {
class="breadcrumb-item__label breadcrumb-item__label--link" class="breadcrumb-item__label breadcrumb-item__label--link"
href="${this.href}" href="${this.href}"
target="${this.target}" target="${this.target}"
rel=${ifDefined(this.target ? this.rel : undefined)} rel=${ifDefined(typeof this.target !== 'undefined' ? this.rel : undefined)}
> >
<slot></slot> <slot></slot>
</a> </a>

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBreadcrumb from './breadcrumb'; import type SlBreadcrumb from './breadcrumb';
describe('<sl-breadcrumb>', () => { describe('<sl-breadcrumb>', () => {
let el: SlBreadcrumb; let el: SlBreadcrumb;
describe('when provided a standard list of el-breadcrumb-item children and no parameters', async () => { describe('when provided a standard list of el-breadcrumb-item children and no parameters', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumb>(html` el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb> <sl-breadcrumb>
@ -22,18 +20,18 @@ describe('<sl-breadcrumb>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should render sl-icon as separator', async () => { it('should render sl-icon as separator', () => {
expect(el.querySelectorAll('sl-icon').length).to.eq(4); expect(el.querySelectorAll('sl-icon').length).to.eq(4);
}); });
it('should attach aria-current "page" on the last breadcrumb item.', async () => { it('should attach aria-current "page" on the last breadcrumb item.', () => {
const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item'); const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item');
const lastNode = breadcrumbItems[3]; const lastNode = breadcrumbItems[3];
expect(lastNode).attribute('aria-current', 'page'); expect(lastNode).attribute('aria-current', 'page');
}); });
}); });
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "separator" to support Custom Separators', async () => { describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "separator" to support Custom Separators', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumb>(html` el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb> <sl-breadcrumb>
@ -49,20 +47,20 @@ describe('<sl-breadcrumb>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should accept "separator" as an assigned child in the shadow root', async () => { it('should accept "separator" as an assigned child in the shadow root', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=separator]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=separator]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
}); });
it('should replace the sl-icon separator with the provided separator', async () => { it('should replace the sl-icon separator with the provided separator', () => {
expect(el.querySelectorAll('.replacement-separator').length).to.eq(4); expect(el.querySelectorAll('.replacement-separator').length).to.eq(4);
expect(el.querySelectorAll('sl-icon').length).to.eq(0); expect(el.querySelectorAll('sl-icon').length).to.eq(0);
}); });
}); });
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', async () => { describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumb>(html` el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb> <sl-breadcrumb>
@ -82,7 +80,7 @@ describe('<sl-breadcrumb>', () => {
}); });
}); });
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', async () => { describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', () => {
before(async () => { before(async () => {
el = await fixture<SlBreadcrumb>(html` el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb> <sl-breadcrumb>

Wyświetl plik

@ -1,9 +1,8 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import styles from './breadcrumb.styles'; import styles from './breadcrumb.styles';
import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item'; import type SlBreadcrumbItem from '~/components/breadcrumb-item/breadcrumb-item';
import '~/components/icon/icon';
import '../icon/icon';
/** /**
* @since 2.0 * @since 2.0
@ -35,7 +34,9 @@ export default class SlBreadcrumb extends LitElement {
// Clone it, remove ids, and slot it // Clone it, remove ids, and slot it
const clone = separator.cloneNode(true) as HTMLElement; const clone = separator.cloneNode(true) as HTMLElement;
[clone, ...clone.querySelectorAll('[id]')].map(el => el.removeAttribute('id')); [clone, ...clone.querySelectorAll('[id]')].forEach(el => {
el.removeAttribute('id');
});
clone.slot = 'separator'; clone.slot = 'separator';
return clone; return clone;
@ -46,10 +47,10 @@ export default class SlBreadcrumb extends LitElement {
item => item.tagName.toLowerCase() === 'sl-breadcrumb-item' item => item.tagName.toLowerCase() === 'sl-breadcrumb-item'
) as SlBreadcrumbItem[]; ) as SlBreadcrumbItem[];
items.map((item, index) => { items.forEach((item, index) => {
// Append separators to each item if they don't already have one // Append separators to each item if they don't already have one
const separator = item.querySelector('[slot="separator"]') as HTMLElement; const separator = item.querySelector('[slot="separator"]');
if (!separator) { if (separator === null) {
item.append(this.getSeparator()); item.append(this.getSeparator());
} }

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -42,11 +42,11 @@ export default class SlButtonGroup extends LitElement {
handleSlotChange() { handleSlotChange() {
const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[]; const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[];
slottedElements.map(el => { slottedElements.forEach(el => {
const index = slottedElements.indexOf(el); const index = slottedElements.indexOf(el);
const button = findButton(el); const button = findButton(el);
if (button) { if (button !== null) {
button.classList.add('sl-button-group__button'); button.classList.add('sl-button-group__button');
button.classList.toggle('sl-button-group__button--first', index === 0); button.classList.toggle('sl-button-group__button--first', index === 0);
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1); button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
@ -56,6 +56,7 @@ export default class SlButtonGroup extends LitElement {
} }
render() { render() {
// eslint-disable-next-line lit-a11y/mouse-events-have-key-events -- focusout & focusin support bubbling where as focus & blur do not which is necessary here
return html` return html`
<div <div
part="base" part="base"

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,14 +1,13 @@
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js'; import { customElement, property, query, state } from 'lit/decorators.js';
import { html, literal } from 'lit/static-html.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { emit } from '../../internal/event'; import { html, literal } from 'lit/static-html.js';
import { FormSubmitController } from '../../internal/form-control';
import { HasSlotController } from '../../internal/slot';
import styles from './button.styles'; import styles from './button.styles';
import '~/components/spinner/spinner';
import '../spinner/spinner'; import { emit } from '~/internal/event';
import { FormSubmitController } from '~/internal/form-control';
import { HasSlotController } from '~/internal/slot';
/** /**
* @since 2.0 * @since 2.0
@ -35,8 +34,8 @@ export default class SlButton extends LitElement {
@query('.button') button: HTMLButtonElement | HTMLLinkElement; @query('.button') button: HTMLButtonElement | HTMLLinkElement;
private formSubmitController = new FormSubmitController(this); private readonly formSubmitController = new FormSubmitController(this);
private hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix'); private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
@state() private hasFocus = false; @state() private hasFocus = false;
@ -72,19 +71,19 @@ export default class SlButton extends LitElement {
@property() type: 'button' | 'submit' = 'button'; @property() type: 'button' | 'submit' = 'button';
/** An optional name for the button. Ignored when `href` is set. */ /** An optional name for the button. Ignored when `href` is set. */
@property() name: string; @property() name?: string;
/** An optional value for the button. Ignored when `href` is set. */ /** An optional value for the button. Ignored when `href` is set. */
@property() value: string; @property() value?: string;
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */ /** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
@property() href: string; @property() href?: string;
/** Tells the browser where to open the link. Only used when `href` is set. */ /** Tells the browser where to open the link. Only used when `href` is set. */
@property() target: '_blank' | '_parent' | '_self' | '_top'; @property() target?: '_blank' | '_parent' | '_self' | '_top';
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */ /** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
@property() download: string; @property() download?: string;
/** Simulates a click on the button. */ /** Simulates a click on the button. */
click() { click() {
@ -124,9 +123,10 @@ export default class SlButton extends LitElement {
} }
render() { render() {
const isLink = this.href ? true : false; const isLink = typeof this.href !== 'undefined';
const tag = isLink ? literal`a` : literal`button`; const tag = isLink ? literal`a` : literal`button`;
/* eslint-disable lit/binding-positions, lit/no-invalid-html */
return html` return html`
<${tag} <${tag}
part="base" part="base"
@ -161,7 +161,7 @@ export default class SlButton extends LitElement {
href=${ifDefined(this.href)} href=${ifDefined(this.href)}
target=${ifDefined(this.target)} target=${ifDefined(this.target)}
download=${ifDefined(this.download)} download=${ifDefined(this.download)}
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)} rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
role="button" role="button"
aria-disabled=${this.disabled ? 'true' : 'false'} aria-disabled=${this.disabled ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'} tabindex=${this.disabled ? '-1' : '0'}
@ -199,6 +199,7 @@ export default class SlButton extends LitElement {
${this.loading ? html`<sl-spinner></sl-spinner>` : ''} ${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
</${tag}> </${tag}>
`; `;
/* eslint-enable lit/binding-positions, lit/no-invalid-html */
} }
} }

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlCard from './card'; import type SlCard from './card';
describe('<sl-card>', () => { describe('<sl-card>', () => {
let el: SlCard; let el: SlCard;
describe('when provided no parameters', async () => { describe('when provided no parameters', () => {
before(async () => { before(async () => {
el = await fixture<SlCard>( el = await fixture<SlCard>(
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> ` html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
@ -17,17 +15,17 @@ describe('<sl-card>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should render the child content provided.', async () => { it('should render the child content provided.', () => {
expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.'); expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.');
}); });
it('should contain the class card.', async () => { it('should contain the class card.', () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement; const card = el.shadowRoot!.querySelector('.card')!;
expect(card.classList.value.trim()).to.eq('card'); expect(card.classList.value.trim()).to.eq('card');
}); });
}); });
describe('when provided an element in the slot "header" to render a header', async () => { describe('when provided an element in the slot "header" to render a header', () => {
before(async () => { before(async () => {
el = await fixture<SlCard>( el = await fixture<SlCard>(
html`<sl-card> html`<sl-card>
@ -41,29 +39,29 @@ describe('<sl-card>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should render the child content provided.', async () => { it('should render the child content provided.', () => {
expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!'); expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!');
}); });
it('render the header content provided.', async () => { it('render the header content provided.', () => {
const header = <HTMLDivElement>el.querySelector('div[slot=header]'); const header = el.querySelector<HTMLElement>('div[slot=header]')!;
expect(header.innerText).eq('Header Title'); expect(header.innerText).eq('Header Title');
}); });
it('accept "header" as an assigned child in the shadow root.', async () => { it('accept "header" as an assigned child in the shadow root.', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=header]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=header]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
}); });
it('should contain the class card--has-header.', async () => { it('should contain the class card--has-header.', () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement; const card = el.shadowRoot!.querySelector('.card')!;
expect(card.classList.value.trim()).to.eq('card card--has-header'); expect(card.classList.value.trim()).to.eq('card card--has-header');
}); });
}); });
describe('when provided an element in the slot "footer" to render a footer', async () => { describe('when provided an element in the slot "footer" to render a footer', () => {
before(async () => { before(async () => {
el = await fixture<SlCard>( el = await fixture<SlCard>(
html`<sl-card> html`<sl-card>
@ -78,35 +76,35 @@ describe('<sl-card>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should render the child content provided.', async () => { it('should render the child content provided.', () => {
expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!'); expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!');
}); });
it('render the footer content provided.', async () => { it('render the footer content provided.', () => {
const footer = <HTMLDivElement>el.querySelector('div[slot=footer]'); const footer = el.querySelector<HTMLElement>('div[slot=footer]')!;
expect(footer.innerText).eq('Footer Content'); expect(footer.innerText).eq('Footer Content');
}); });
it('accept "footer" as an assigned child in the shadow root.', async () => { it('accept "footer" as an assigned child in the shadow root.', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=footer]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=footer]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
}); });
it('should contain the class card--has-footer.', async () => { it('should contain the class card--has-footer.', () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement; const card = el.shadowRoot!.querySelector('.card')!;
expect(card.classList.value.trim()).to.eq('card card--has-footer'); expect(card.classList.value.trim()).to.eq('card card--has-footer');
}); });
}); });
describe('when provided an element in the slot "image" to render a image', async () => { describe('when provided an element in the slot "image" to render a image', () => {
before(async () => { before(async () => {
el = await fixture<SlCard>( el = await fixture<SlCard>(
html`<sl-card> html`<sl-card>
<img <img
slot="image" slot="image"
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
alt="A kitten walks towards camera on top of pallet." alt="A kitten walks towards camera on top of pallet."
/> />
This is a kitten, but not just any kitten. This kitten likes walking along pallets. This is a kitten, but not just any kitten. This kitten likes walking along pallets.
@ -118,21 +116,21 @@ describe('<sl-card>', () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should render the child content provided.', async () => { it('should render the child content provided.', () => {
expect(el.innerText).to.contain( expect(el.innerText).to.contain(
'This is a kitten, but not just any kitten. This kitten likes walking along pallets.' 'This is a kitten, but not just any kitten. This kitten likes walking along pallets.'
); );
}); });
it('accept "image" as an assigned child in the shadow root.', async () => { it('accept "image" as an assigned child in the shadow root.', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=image]'); const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=image]')!;
const childNodes = slot.assignedNodes({ flatten: true }); const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1); expect(childNodes.length).to.eq(1);
}); });
it('should contain the class card--has-image.', async () => { it('should contain the class card--has-image.', () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement; const card = el.shadowRoot!.querySelector('.card')!;
expect(card.classList.value.trim()).to.eq('card card--has-image'); expect(card.classList.value.trim()).to.eq('card card--has-image');
}); });
}); });

Wyświetl plik

@ -1,8 +1,8 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js'; import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { HasSlotController } from '../../internal/slot';
import styles from './card.styles'; import styles from './card.styles';
import { HasSlotController } from '~/internal/slot';
/** /**
* @since 2.0 * @since 2.0
@ -28,7 +28,7 @@ import styles from './card.styles';
export default class SlCard extends LitElement { export default class SlCard extends LitElement {
static styles = styles; static styles = styles;
private hasSlotController = new HasSlotController(this, 'footer', 'header', 'image'); private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
render() { render() {
return html` return html`

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,7 +1,5 @@
import { expect, fixture, html, oneEvent } from '@open-wc/testing'; import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands'; import { sendKeys } from '@web/test-runner-commands';
import '../../../dist/shoelace.js';
import type SlCheckbox from './checkbox'; import type SlCheckbox from './checkbox';
describe('<sl-checkbox>', () => { describe('<sl-checkbox>', () => {
@ -9,7 +7,7 @@ describe('<sl-checkbox>', () => {
const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `); const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `);
const checkbox = el.shadowRoot?.querySelector('input'); const checkbox = el.shadowRoot?.querySelector('input');
expect(checkbox.disabled).to.be.true; expect(checkbox!.disabled).to.be.true;
}); });
it('should be valid by default', async () => { it('should be valid by default', async () => {
@ -20,7 +18,9 @@ describe('<sl-checkbox>', () => {
it('should fire sl-change when clicked', async () => { it('should fire sl-change when clicked', async () => {
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `); const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
setTimeout(() => el.shadowRoot?.querySelector('input').click()); setTimeout(() => {
el.shadowRoot!.querySelector('input')!.click();
});
const event = await oneEvent(el, 'sl-change'); const event = await oneEvent(el, 'sl-change');
expect(event.target).to.equal(el); expect(event.target).to.equal(el);
expect(el.checked).to.be.true; expect(el.checked).to.be.true;
@ -28,9 +28,11 @@ describe('<sl-checkbox>', () => {
it('should fire sl-change when toggled via keyboard', async () => { it('should fire sl-change when toggled via keyboard', async () => {
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `); const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
const input = el.shadowRoot?.querySelector('input'); const input = el.shadowRoot!.querySelector('input')!;
input.focus(); input.focus();
setTimeout(() => sendKeys({ press: ' ' })); setTimeout(() => {
void sendKeys({ press: ' ' });
});
const event = await oneEvent(el, 'sl-change'); const event = await oneEvent(el, 'sl-change');
expect(event.target).to.equal(el); expect(event.target).to.equal(el);
expect(el.checked).to.be.true; expect(el.checked).to.be.true;

Wyświetl plik

@ -3,12 +3,11 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js'; import { live } from 'lit/directives/live.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { FormSubmitController } from '../../internal/form-control';
import styles from './checkbox.styles'; import styles from './checkbox.styles';
import { autoIncrement } from '~/internal/autoIncrement';
let id = 0; import { emit } from '~/internal/event';
import { FormSubmitController } from '~/internal/form-control';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -32,12 +31,13 @@ export default class SlCheckbox extends LitElement {
@query('input[type="checkbox"]') input: HTMLInputElement; @query('input[type="checkbox"]') input: HTMLInputElement;
// @ts-ignore // @ts-expect-error -- Controller is currently unused
private formSubmitController = new FormSubmitController(this, { private readonly formSubmitController = new FormSubmitController(this, {
value: (control: SlCheckbox) => (control.checked ? control.value : undefined) value: (control: SlCheckbox) => (control.checked ? control.value : undefined)
}); });
private inputId = `checkbox-${++id}`; private readonly attrId = autoIncrement();
private labelId = `checkbox-label-${id}`; private readonly inputId = `checkbox-${this.attrId}`;
private readonly labelId = `checkbox-label-${this.attrId}`;
@state() private hasFocus = false; @state() private hasFocus = false;
@ -103,13 +103,11 @@ export default class SlCheckbox extends LitElement {
emit(this, 'sl-blur'); emit(this, 'sl-blur');
} }
@watch('disabled') @watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() { handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes // Disabled form controls are always valid, so we need to recheck validity when the state changes
if (this.input) { this.input.disabled = this.disabled;
this.input.disabled = this.disabled; this.invalid = !this.input.checkValidity();
this.invalid = !this.input.checkValidity();
}
} }
handleFocus() { handleFocus() {
@ -146,7 +144,6 @@ export default class SlCheckbox extends LitElement {
.checked=${live(this.checked)} .checked=${live(this.checked)}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
role="checkbox"
aria-checked=${this.checked ? 'true' : 'false'} aria-checked=${this.checked ? 'true' : 'false'}
aria-labelledby=${this.labelId} aria-labelledby=${this.labelId}
@click=${this.handleClick} @click=${this.handleClick}

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,13 +1,11 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlColorPicker from './color-picker'; import type SlColorPicker from './color-picker';
describe('<sl-color-picker>', () => { describe('<sl-color-picker>', () => {
it('should emit change and show correct color when the value changes', async () => { it('should emit change and show correct color when the value changes', async () => {
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `); const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
const trigger = el.shadowRoot.querySelector('[part="trigger"]') as HTMLElement; const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part="trigger"]')!;
const changeHandler = sinon.spy(); const changeHandler = sinon.spy();
const color = 'rgb(255, 204, 0)'; const color = 'rgb(255, 204, 0)';
@ -22,21 +20,21 @@ describe('<sl-color-picker>', () => {
it('should render in a dropdown', async () => { it('should render in a dropdown', async () => {
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `); const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
const dropdown = el.shadowRoot.querySelector('sl-dropdown'); const dropdown = el.shadowRoot!.querySelector('sl-dropdown');
expect(dropdown).to.exist; expect(dropdown).to.exist;
}); });
it('should not render in a dropdown when inline is enabled', async () => { it('should not render in a dropdown when inline is enabled', async () => {
const el = await fixture<SlColorPicker>(html` <sl-color-picker inline></sl-color-picker> `); const el = await fixture<SlColorPicker>(html` <sl-color-picker inline></sl-color-picker> `);
const dropdown = el.shadowRoot.querySelector('sl-dropdown'); const dropdown = el.shadowRoot!.querySelector('sl-dropdown');
expect(dropdown).to.not.exist; expect(dropdown).to.not.exist;
}); });
it('should show opacity slider when opacity is enabled', async () => { it('should show opacity slider when opacity is enabled', async () => {
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `); const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
const opacitySlider = el.shadowRoot.querySelector('[part*="opacity-slider"]') as HTMLElement; const opacitySlider = el.shadowRoot!.querySelector('[part*="opacity-slider"]')!;
expect(opacitySlider).to.exist; expect(opacitySlider).to.exist;
}); });

Wyświetl plik

@ -1,27 +1,37 @@
import Color from 'color';
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js'; import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js'; import { live } from 'lit/directives/live.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { clamp } from '../../internal/math';
import { FormSubmitController } from '../../internal/form-control';
import { LocalizeController } from '../../utilities/localize';
import type SlDropdown from '../dropdown/dropdown';
import type SlInput from '../input/input';
import color from 'color';
import styles from './color-picker.styles'; import styles from './color-picker.styles';
import '~/components/button-group/button-group';
import '../button/button'; import '~/components/button/button';
import '../button-group/button-group'; import type SlDropdown from '~/components/dropdown/dropdown';
import '../dropdown/dropdown'; import '~/components/dropdown/dropdown';
import '../icon/icon'; import '~/components/icon/icon';
import '../input/input'; import type SlInput from '~/components/input/input';
import '~/components/input/input';
import { drag } from '~/internal/drag';
import { emit } from '~/internal/event';
import { FormSubmitController } from '~/internal/form-control';
import { clamp } from '~/internal/math';
import { watch } from '~/internal/watch';
import { LocalizeController } from '~/utilities/localize';
const hasEyeDropper = 'EyeDropper' in window; const hasEyeDropper = 'EyeDropper' in window;
interface EyeDropperConstructor {
new (): EyeDropperInterface;
}
interface EyeDropperInterface {
open: () => Promise<{ sRGBHex: string }>;
}
declare const EyeDropper: EyeDropperConstructor;
/** /**
* @since 2.0 * @since 2.0
* @status stable * @status stable
@ -63,11 +73,11 @@ export default class SlColorPicker extends LitElement {
@query('[part="preview"]') previewButton: HTMLButtonElement; @query('[part="preview"]') previewButton: HTMLButtonElement;
@query('.color-dropdown') dropdown: SlDropdown; @query('.color-dropdown') dropdown: SlDropdown;
// @ts-ignore // @ts-expect-error -- Controller is currently unused
private formSubmitController = new FormSubmitController(this); private readonly formSubmitController = new FormSubmitController(this);
private isSafeValue = false; private isSafeValue = false;
private lastValueEmitted: string; private lastValueEmitted: string;
private localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
@state() private inputValue = ''; @state() private inputValue = '';
@state() private hue = 0; @state() private hue = 0;
@ -151,7 +161,7 @@ export default class SlColorPicker extends LitElement {
this.inputValue = this.value; this.inputValue = this.value;
this.lastValueEmitted = this.value; this.lastValueEmitted = this.value;
this.syncValues(); void this.syncValues();
} }
/** Returns the current value as a string in the specified format. */ /** Returns the current value as a string in the specified format. */
@ -160,7 +170,7 @@ export default class SlColorPicker extends LitElement {
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})` `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
); );
if (!currentColor) { if (currentColor === null) {
return ''; return '';
} }
@ -195,11 +205,10 @@ export default class SlColorPicker extends LitElement {
}, },
{ once: true } { once: true }
); );
this.dropdown.show(); void this.dropdown.show();
}); });
} else {
return this.input.reportValidity();
} }
return this.input.reportValidity();
} }
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */ /** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
@ -215,9 +224,9 @@ export default class SlColorPicker extends LitElement {
// Show copied animation // Show copied animation
this.previewButton.classList.add('color-picker__preview-color--copied'); this.previewButton.classList.add('color-picker__preview-color--copied');
this.previewButton.addEventListener('animationend', () => this.previewButton.addEventListener('animationend', () => {
this.previewButton.classList.remove('color-picker__preview-color--copied') this.previewButton.classList.remove('color-picker__preview-color--copied');
); });
} }
handleFormatToggle() { handleFormatToggle() {
@ -226,106 +235,74 @@ export default class SlColorPicker extends LitElement {
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl'; this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
} }
handleAlphaDrag(event: any) { handleAlphaDrag(event: Event) {
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__alpha') as HTMLElement; const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement; const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
const { width } = container.getBoundingClientRect(); const { width } = container.getBoundingClientRect();
handle.focus(); handle.focus();
event.preventDefault(); event.preventDefault();
this.handleDrag(event, container, x => { drag(container, x => {
this.alpha = clamp((x / width) * 100, 0, 100); this.alpha = clamp((x / width) * 100, 0, 100);
this.syncValues(); void this.syncValues();
}); });
} }
handleHueDrag(event: any) { handleHueDrag(event: Event) {
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__hue') as HTMLElement; const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement; const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
const { width } = container.getBoundingClientRect(); const { width } = container.getBoundingClientRect();
handle.focus(); handle.focus();
event.preventDefault(); event.preventDefault();
this.handleDrag(event, container, x => { drag(container, x => {
this.hue = clamp((x / width) * 360, 0, 360); this.hue = clamp((x / width) * 360, 0, 360);
this.syncValues(); void this.syncValues();
}); });
} }
handleGridDrag(event: any) { handleGridDrag(event: Event) {
const grid = this.shadowRoot!.querySelector('.color-picker__grid') as HTMLElement; const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
const handle = grid.querySelector('.color-picker__grid-handle') as HTMLElement; const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
const { width, height } = grid.getBoundingClientRect(); const { width, height } = grid.getBoundingClientRect();
handle.focus(); handle.focus();
event.preventDefault(); event.preventDefault();
this.handleDrag(event, grid, (x, y) => { drag(grid, (x, y) => {
this.saturation = clamp((x / width) * 100, 0, 100); this.saturation = clamp((x / width) * 100, 0, 100);
this.lightness = clamp(100 - (y / height) * 100, 0, 100); this.lightness = clamp(100 - (y / height) * 100, 0, 100);
this.syncValues(); void this.syncValues();
}); });
} }
handleDrag(event: any, container: HTMLElement, onMove: (x: number, y: number) => void) {
if (this.disabled) {
return;
}
const move = (event: any) => {
const dims = container.getBoundingClientRect();
const defaultView = container.ownerDocument.defaultView!;
const offsetX = dims.left + defaultView.pageXOffset;
const offsetY = dims.top + defaultView.pageYOffset;
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
const y = (event.changedTouches ? event.changedTouches[0].pageY : event.pageY) - offsetY;
onMove(x, y);
};
// Move on init
move(event);
const stop = () => {
document.removeEventListener('mousemove', move);
document.removeEventListener('touchmove', move);
document.removeEventListener('mouseup', stop);
document.removeEventListener('touchend', stop);
};
document.addEventListener('mousemove', move);
document.addEventListener('touchmove', move);
document.addEventListener('mouseup', stop);
document.addEventListener('touchend', stop);
}
handleAlphaKeyDown(event: KeyboardEvent) { handleAlphaKeyDown(event: KeyboardEvent) {
const increment = event.shiftKey ? 10 : 1; const increment = event.shiftKey ? 10 : 1;
if (event.key === 'ArrowLeft') { if (event.key === 'ArrowLeft') {
event.preventDefault(); event.preventDefault();
this.alpha = clamp(this.alpha - increment, 0, 100); this.alpha = clamp(this.alpha - increment, 0, 100);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'ArrowRight') { if (event.key === 'ArrowRight') {
event.preventDefault(); event.preventDefault();
this.alpha = clamp(this.alpha + increment, 0, 100); this.alpha = clamp(this.alpha + increment, 0, 100);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'Home') { if (event.key === 'Home') {
event.preventDefault(); event.preventDefault();
this.alpha = 0; this.alpha = 0;
this.syncValues(); void this.syncValues();
} }
if (event.key === 'End') { if (event.key === 'End') {
event.preventDefault(); event.preventDefault();
this.alpha = 100; this.alpha = 100;
this.syncValues(); void this.syncValues();
} }
} }
@ -335,25 +312,25 @@ export default class SlColorPicker extends LitElement {
if (event.key === 'ArrowLeft') { if (event.key === 'ArrowLeft') {
event.preventDefault(); event.preventDefault();
this.hue = clamp(this.hue - increment, 0, 360); this.hue = clamp(this.hue - increment, 0, 360);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'ArrowRight') { if (event.key === 'ArrowRight') {
event.preventDefault(); event.preventDefault();
this.hue = clamp(this.hue + increment, 0, 360); this.hue = clamp(this.hue + increment, 0, 360);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'Home') { if (event.key === 'Home') {
event.preventDefault(); event.preventDefault();
this.hue = 0; this.hue = 0;
this.syncValues(); void this.syncValues();
} }
if (event.key === 'End') { if (event.key === 'End') {
event.preventDefault(); event.preventDefault();
this.hue = 360; this.hue = 360;
this.syncValues(); void this.syncValues();
} }
} }
@ -363,25 +340,25 @@ export default class SlColorPicker extends LitElement {
if (event.key === 'ArrowLeft') { if (event.key === 'ArrowLeft') {
event.preventDefault(); event.preventDefault();
this.saturation = clamp(this.saturation - increment, 0, 100); this.saturation = clamp(this.saturation - increment, 0, 100);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'ArrowRight') { if (event.key === 'ArrowRight') {
event.preventDefault(); event.preventDefault();
this.saturation = clamp(this.saturation + increment, 0, 100); this.saturation = clamp(this.saturation + increment, 0, 100);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
event.preventDefault(); event.preventDefault();
this.lightness = clamp(this.lightness + increment, 0, 100); this.lightness = clamp(this.lightness + increment, 0, 100);
this.syncValues(); void this.syncValues();
} }
if (event.key === 'ArrowDown') { if (event.key === 'ArrowDown') {
event.preventDefault(); event.preventDefault();
this.lightness = clamp(this.lightness - increment, 0, 100); this.lightness = clamp(this.lightness - increment, 0, 100);
this.syncValues(); void this.syncValues();
} }
} }
@ -397,7 +374,9 @@ export default class SlColorPicker extends LitElement {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.setColor(this.input.value); this.setColor(this.input.value);
this.input.value = this.value; this.input.value = this.value;
setTimeout(() => this.input.select()); setTimeout(() => {
this.input.select();
});
} }
} }
@ -419,7 +398,7 @@ export default class SlColorPicker extends LitElement {
} }
if (rgba[3].indexOf('%') > -1) { if (rgba[3].indexOf('%') > -1) {
rgba[3] = (Number(rgba[3].replace(/%/g, '')) / 100).toString(); rgba[3] = (parseFloat(rgba[3].replace(/%/g, '')) / 100).toString();
} }
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`; return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
@ -437,7 +416,7 @@ export default class SlColorPicker extends LitElement {
} }
if (hsla[3].indexOf('%') > -1) { if (hsla[3].indexOf('%') > -1) {
hsla[3] = (Number(hsla[3].replace(/%/g, '')) / 100).toString(); hsla[3] = (parseFloat(hsla[3].replace(/%/g, '')) / 100).toString();
} }
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`; return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
@ -451,41 +430,40 @@ export default class SlColorPicker extends LitElement {
} }
parseColor(colorString: string) { parseColor(colorString: string) {
function toHex(value: number) { let parsed: Color;
const hex = Math.round(value).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
}
let parsed: any;
// The color module has a weak parser, so we normalize certain things to make the user experience better // The color module has a weak parser, so we normalize certain things to make the user experience better
colorString = this.normalizeColorString(colorString); colorString = this.normalizeColorString(colorString);
try { try {
parsed = color(colorString); parsed = Color(colorString);
} catch { } catch {
return false; return null;
} }
const hslColor = parsed.hsl();
const hsl = { const hsl = {
h: parsed.hsl().color[0], h: hslColor.hue(),
s: parsed.hsl().color[1], s: hslColor.saturationl(),
l: parsed.hsl().color[2], l: hslColor.lightness(),
a: parsed.hsl().valpha a: hslColor.alpha()
}; };
const rgbColor = parsed.rgb();
const rgb = { const rgb = {
r: parsed.rgb().color[0], r: rgbColor.red(),
g: parsed.rgb().color[1], g: rgbColor.green(),
b: parsed.rgb().color[2], b: rgbColor.blue(),
a: parsed.rgb().valpha a: rgbColor.alpha()
}; };
const hex = { const hex = {
r: toHex(parsed.rgb().color[0]), r: toHex(rgb.r),
g: toHex(parsed.rgb().color[1]), g: toHex(rgb.g),
b: toHex(parsed.rgb().color[2]), b: toHex(rgb.b),
a: toHex(parsed.rgb().valpha * 255) a: toHex(rgb.a * 255)
}; };
return { return {
@ -501,9 +479,7 @@ export default class SlColorPicker extends LitElement {
l: hsl.l, l: hsl.l,
a: hsl.a, a: hsl.a,
string: this.setLetterCase( string: this.setLetterCase(
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${Number( `hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${hsl.a.toFixed(2).toString()})`
hsl.a.toFixed(2).toString()
)})`
) )
}, },
rgb: { rgb: {
@ -518,9 +494,7 @@ export default class SlColorPicker extends LitElement {
b: rgb.b, b: rgb.b,
a: rgb.a, a: rgb.a,
string: this.setLetterCase( string: this.setLetterCase(
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${Number( `rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${rgb.a.toFixed(2).toString()})`
rgb.a.toFixed(2).toString()
)})`
) )
}, },
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`), hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
@ -531,7 +505,7 @@ export default class SlColorPicker extends LitElement {
setColor(colorString: string) { setColor(colorString: string) {
const newColor = this.parseColor(colorString); const newColor = this.parseColor(colorString);
if (!newColor) { if (newColor === null) {
return false; return false;
} }
@ -540,13 +514,15 @@ export default class SlColorPicker extends LitElement {
this.lightness = newColor.hsla.l; this.lightness = newColor.hsla.l;
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100; this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
this.syncValues(); void this.syncValues();
return true; return true;
} }
setLetterCase(string: string) { setLetterCase(string: string) {
if (typeof string !== 'string') return ''; if (typeof string !== 'string') {
return '';
}
return this.uppercase ? string.toUpperCase() : string.toLowerCase(); return this.uppercase ? string.toUpperCase() : string.toLowerCase();
} }
@ -555,7 +531,7 @@ export default class SlColorPicker extends LitElement {
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})` `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
); );
if (!currentColor) { if (currentColor === null) {
return; return;
} }
@ -586,12 +562,11 @@ export default class SlColorPicker extends LitElement {
return; return;
} }
// @ts-ignore
const eyeDropper = new EyeDropper(); const eyeDropper = new EyeDropper();
eyeDropper eyeDropper
.open() .open()
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex)) .then(colorSelectionResult => this.setColor(colorSelectionResult.sRGBHex))
.catch(() => { .catch(() => {
// The user canceled, do nothing // The user canceled, do nothing
}); });
@ -599,7 +574,7 @@ export default class SlColorPicker extends LitElement {
@watch('format') @watch('format')
handleFormatChange() { handleFormatChange() {
this.syncValues(); void this.syncValues();
} }
@watch('opacity') @watch('opacity')
@ -608,11 +583,11 @@ export default class SlColorPicker extends LitElement {
} }
@watch('value') @watch('value')
handleValueChange(oldValue: string, newValue: string) { handleValueChange(oldValue: string | undefined, newValue: string) {
if (!this.isSafeValue) { if (!this.isSafeValue && oldValue !== undefined) {
const newColor = this.parseColor(newValue); const newColor = this.parseColor(newValue);
if (newColor) { if (newColor !== null) {
this.inputValue = this.value; this.inputValue = this.value;
this.hue = newColor.hsla.h; this.hue = newColor.hsla.h;
this.saturation = newColor.hsla.s; this.saturation = newColor.hsla.s;
@ -658,11 +633,8 @@ export default class SlColorPicker extends LitElement {
left: `${x}%`, left: `${x}%`,
backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)` backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)`
})} })}
role="slider" role="application"
aria-label="HSL" aria-label="HSL"
aria-valuetext=${`hsl(${Math.round(this.hue)}, ${Math.round(this.saturation)}%, ${Math.round(
this.lightness
)}%)`}
tabindex=${ifDefined(this.disabled ? undefined : '0')} tabindex=${ifDefined(this.disabled ? undefined : '0')}
@keydown=${this.handleGridKeyDown} @keydown=${this.handleGridKeyDown}
></span> ></span>
@ -743,7 +715,7 @@ export default class SlColorPicker extends LitElement {
></button> ></button>
</div> </div>
<div class="color-picker__user-input"> <div class="color-picker__user-input" aria-live="polite">
<sl-input <sl-input
part="input" part="input"
type="text" type="text"
@ -762,7 +734,7 @@ export default class SlColorPicker extends LitElement {
${!this.noFormatToggle ${!this.noFormatToggle
? html` ? html`
<sl-button <sl-button
aria-label=${this.localize.term('toggle_color_format')} aria-label=${this.localize.term('toggleColorFormat')}
exportparts="base:format-button" exportparts="base:format-button"
@click=${this.handleFormatToggle} @click=${this.handleFormatToggle}
> >
@ -776,7 +748,7 @@ export default class SlColorPicker extends LitElement {
<sl-icon <sl-icon
library="system" library="system"
name="eyedropper" name="eyedropper"
label=${this.localize.term('select_a_color_from_the_screen')} label=${this.localize.term('selectAColorFromTheScreen')}
></sl-icon> ></sl-icon>
</sl-button> </sl-button>
` `
@ -784,7 +756,7 @@ export default class SlColorPicker extends LitElement {
</sl-button-group> </sl-button-group>
</div> </div>
${this.swatches ${this.swatches.length > 0
? html` ? html`
<div part="swatches" class="color-picker__swatches"> <div part="swatches" class="color-picker__swatches">
${this.swatches.map(swatch => { ${this.swatches.map(swatch => {
@ -828,12 +800,14 @@ export default class SlColorPicker extends LitElement {
part="trigger" part="trigger"
slot="trigger" slot="trigger"
class=${classMap({ class=${classMap({
/* eslint-disable @typescript-eslint/naming-convention */
'color-dropdown__trigger': true, 'color-dropdown__trigger': true,
'color-dropdown__trigger--disabled': this.disabled, 'color-dropdown__trigger--disabled': this.disabled,
'color-dropdown__trigger--small': this.size === 'small', 'color-dropdown__trigger--small': this.size === 'small',
'color-dropdown__trigger--medium': this.size === 'medium', 'color-dropdown__trigger--medium': this.size === 'medium',
'color-dropdown__trigger--large': this.size === 'large', 'color-dropdown__trigger--large': this.size === 'large',
'color-picker__transparent-bg': true 'color-picker__transparent-bg': true
/* eslint-enable @typescript-eslint/naming-convention */
})} })}
style=${styleMap({ style=${styleMap({
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})` color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
@ -846,6 +820,11 @@ export default class SlColorPicker extends LitElement {
} }
} }
function toHex(value: number) {
const hex = Math.round(value).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'sl-color-picker': SlColorPicker; 'sl-color-picker': SlColorPicker;

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,8 +1,6 @@
// cspell:dictionaries lorem-ipsum // cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDetails from './details'; import type SlDetails from './details';
describe('<sl-details>', () => { describe('<sl-details>', () => {
@ -14,7 +12,7 @@ describe('<sl-details>', () => {
consequat. consequat.
</sl-details> </sl-details>
`); `);
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement; const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
expect(body.hidden).to.be.false; expect(body.hidden).to.be.false;
}); });
@ -27,7 +25,7 @@ describe('<sl-details>', () => {
consequat. consequat.
</sl-details> </sl-details>
`); `);
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement; const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
expect(body.hidden).to.be.true; expect(body.hidden).to.be.true;
}); });
@ -40,13 +38,13 @@ describe('<sl-details>', () => {
consequat. consequat.
</sl-details> </sl-details>
`); `);
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement; const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler); el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler); el.addEventListener('sl-after-show', afterShowHandler);
el.show(); void el.show();
await waitUntil(() => showHandler.calledOnce); await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce); await waitUntil(() => afterShowHandler.calledOnce);
@ -64,13 +62,13 @@ describe('<sl-details>', () => {
consequat. consequat.
</sl-details> </sl-details>
`); `);
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement; const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler); el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler); el.addEventListener('sl-after-hide', afterHideHandler);
el.hide(); void el.hide();
await waitUntil(() => hideHandler.calledOnce); await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce); await waitUntil(() => afterHideHandler.calledOnce);
@ -88,7 +86,7 @@ describe('<sl-details>', () => {
consequat. consequat.
</sl-details> </sl-details>
`); `);
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement; const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
@ -112,7 +110,7 @@ describe('<sl-details>', () => {
consequat. consequat.
</sl-details> </sl-details>
`); `);
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement; const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();

Wyświetl plik

@ -1,14 +1,12 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import styles from './details.styles'; import styles from './details.styles';
import '~/components/icon/icon';
import '../icon/icon'; import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '~/internal/animate';
import { emit, waitForEvent } from '~/internal/event';
import { watch } from '~/internal/watch';
import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry';
/** /**
* @since 2.0 * @since 2.0
@ -58,7 +56,7 @@ export default class SlDetails extends LitElement {
/** Shows the details. */ /** Shows the details. */
async show() { async show() {
if (this.open) { if (this.open) {
return; return undefined;
} }
this.open = true; this.open = true;
@ -68,16 +66,24 @@ export default class SlDetails extends LitElement {
/** Hides the details */ /** Hides the details */
async hide() { async hide() {
if (!this.open) { if (!this.open) {
return; return undefined;
} }
this.open = false; this.open = false;
return waitForEvent(this, 'sl-after-hide'); return waitForEvent(this, 'sl-after-hide');
} }
toggleOpen() {
if (this.open) {
void this.hide();
} else {
void this.show();
}
}
handleSummaryClick() { handleSummaryClick() {
if (!this.disabled) { if (!this.disabled) {
this.open ? this.hide() : this.show(); this.toggleOpen();
this.header.focus(); this.header.focus();
} }
} }
@ -85,17 +91,17 @@ export default class SlDetails extends LitElement {
handleSummaryKeyDown(event: KeyboardEvent) { handleSummaryKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault(); event.preventDefault();
this.open ? this.hide() : this.show(); this.toggleOpen();
} }
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
event.preventDefault(); event.preventDefault();
this.hide(); void this.hide();
} }
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.preventDefault(); event.preventDefault();
this.show(); void this.show();
} }
} }

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,8 +1,6 @@
// cspell:dictionaries lorem-ipsum // cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDialog from './dialog'; import type SlDialog from './dialog';
describe('<sl-dialog>', () => { describe('<sl-dialog>', () => {
@ -10,7 +8,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html` const el = await fixture<SlDialog>(html`
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> <sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.false; expect(base.hidden).to.be.false;
}); });
@ -19,7 +17,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>( const el = await fixture<SlDialog>(
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> ` html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
); );
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.true; expect(base.hidden).to.be.true;
}); });
@ -28,13 +26,13 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html` const el = await fixture<SlDialog>(html`
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler); el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler); el.addEventListener('sl-after-show', afterShowHandler);
el.show(); void el.show();
await waitUntil(() => showHandler.calledOnce); await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce); await waitUntil(() => afterShowHandler.calledOnce);
@ -48,13 +46,13 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html` const el = await fixture<SlDialog>(html`
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> <sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler); el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler); el.addEventListener('sl-after-hide', afterHideHandler);
el.hide(); void el.hide();
await waitUntil(() => hideHandler.calledOnce); await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce); await waitUntil(() => afterHideHandler.calledOnce);
@ -68,7 +66,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html` const el = await fixture<SlDialog>(html`
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
@ -88,7 +86,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html` const el = await fixture<SlDialog>(html`
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> <sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
@ -108,9 +106,11 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html` const el = await fixture<SlDialog>(html`
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> <sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`); `);
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement; const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
el.addEventListener('sl-request-close', event => event.preventDefault()); el.addEventListener('sl-request-close', event => {
event.preventDefault();
});
overlay.click(); overlay.click();
expect(el.open).to.be.true; expect(el.open).to.be.true;
@ -118,14 +118,14 @@ describe('<sl-dialog>', () => {
it('should allow initial focus to be set', async () => { it('should allow initial focus to be set', async () => {
const el = await fixture<SlDialog>(html` <sl-dialog><input /></sl-dialog> `); const el = await fixture<SlDialog>(html` <sl-dialog><input /></sl-dialog> `);
const input = el.querySelector('input'); const input = el.querySelector('input')!;
const initialFocusHandler = sinon.spy(event => { const initialFocusHandler = sinon.spy((event: Event) => {
event.preventDefault(); event.preventDefault();
input.focus(); input.focus();
}); });
el.addEventListener('sl-initial-focus', initialFocusHandler); el.addEventListener('sl-initial-focus', initialFocusHandler);
el.show(); void el.show();
await waitUntil(() => initialFocusHandler.calledOnce); await waitUntil(() => initialFocusHandler.calledOnce);

Wyświetl plik

@ -2,18 +2,16 @@ import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { animateTo, stopAnimations } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { HasSlotController } from '../../internal/slot';
import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import styles from './dialog.styles'; import styles from './dialog.styles';
import '~/components/icon-button/icon-button';
import '../icon-button/icon-button'; import { animateTo, stopAnimations } from '~/internal/animate';
import { emit, waitForEvent } from '~/internal/event';
import Modal from '~/internal/modal';
import { lockBodyScrolling, unlockBodyScrolling } from '~/internal/scroll';
import { HasSlotController } from '~/internal/slot';
import { isPreventScrollSupported } from '~/internal/support';
import { watch } from '~/internal/watch';
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
const hasPreventScroll = isPreventScrollSupported(); const hasPreventScroll = isPreventScrollSupported();
@ -65,7 +63,7 @@ export default class SlDialog extends LitElement {
@query('.dialog__panel') panel: HTMLElement; @query('.dialog__panel') panel: HTMLElement;
@query('.dialog__overlay') overlay: HTMLElement; @query('.dialog__overlay') overlay: HTMLElement;
private hasSlotController = new HasSlotController(this, 'footer'); private readonly hasSlotController = new HasSlotController(this, 'footer');
private modal: Modal; private modal: Modal;
private originalTrigger: HTMLElement | null; private originalTrigger: HTMLElement | null;
@ -106,7 +104,7 @@ export default class SlDialog extends LitElement {
/** Shows the dialog. */ /** Shows the dialog. */
async show() { async show() {
if (this.open) { if (this.open) {
return; return undefined;
} }
this.open = true; this.open = true;
@ -116,7 +114,7 @@ export default class SlDialog extends LitElement {
/** Hides the dialog */ /** Hides the dialog */
async hide() { async hide() {
if (!this.open) { if (!this.open) {
return; return undefined;
} }
this.open = false; this.open = false;
@ -127,11 +125,11 @@ export default class SlDialog extends LitElement {
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true }); const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
if (slRequestClose.defaultPrevented) { if (slRequestClose.defaultPrevented) {
const animation = getAnimation(this, 'dialog.denyClose'); const animation = getAnimation(this, 'dialog.denyClose');
animateTo(this.panel, animation.keyframes, animation.options); void animateTo(this.panel, animation.keyframes, animation.options);
return; return;
} }
this.hide(); void this.hide();
} }
handleKeyDown(event: KeyboardEvent) { handleKeyDown(event: KeyboardEvent) {
@ -197,8 +195,10 @@ export default class SlDialog extends LitElement {
// Restore focus to the original trigger // Restore focus to the original trigger
const trigger = this.originalTrigger; const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') { if (typeof trigger?.focus === 'function') {
setTimeout(() => trigger.focus()); setTimeout(() => {
trigger.focus();
});
} }
emit(this, 'sl-after-hide'); emit(this, 'sl-after-hide');
@ -216,7 +216,13 @@ export default class SlDialog extends LitElement {
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
> >
<div part="overlay" class="dialog__overlay" @click=${this.requestClose} tabindex="-1"></div> <div
part="overlay"
class="dialog__overlay"
@click=${this.requestClose}
@keydown=${this.handleKeyDown}
tabindex="-1"
></div>
<div <div
part="panel" part="panel"
@ -232,7 +238,7 @@ export default class SlDialog extends LitElement {
? html` ? html`
<header part="header" class="dialog__header"> <header part="header" class="dialog__header">
<span part="title" class="dialog__title" id="title"> <span part="title" class="dialog__title" id="title">
<slot name="label"> ${this.label || String.fromCharCode(65279)} </slot> <slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
</span> </span>
<sl-icon-button <sl-icon-button
exportparts="base:close-button" exportparts="base:close-button"

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,7 +1,7 @@
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { watch } from '../../internal/watch';
import styles from './divider.styles'; import styles from './divider.styles';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,8 +1,6 @@
// cspell:dictionaries lorem-ipsum // cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDrawer from './drawer'; import type SlDrawer from './drawer';
describe('<sl-drawer>', () => { describe('<sl-drawer>', () => {
@ -10,7 +8,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html` const el = await fixture<SlDrawer>(html`
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> <sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.false; expect(base.hidden).to.be.false;
}); });
@ -19,7 +17,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>( const el = await fixture<SlDrawer>(
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> ` html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
); );
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.true; expect(base.hidden).to.be.true;
}); });
@ -28,13 +26,13 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html` const el = await fixture<SlDrawer>(html`
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler); el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler); el.addEventListener('sl-after-show', afterShowHandler);
el.show(); void el.show();
await waitUntil(() => showHandler.calledOnce); await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce); await waitUntil(() => afterShowHandler.calledOnce);
@ -48,13 +46,13 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html` const el = await fixture<SlDrawer>(html`
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> <sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler); el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler); el.addEventListener('sl-after-hide', afterHideHandler);
el.hide(); void el.hide();
await waitUntil(() => hideHandler.calledOnce); await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce); await waitUntil(() => afterHideHandler.calledOnce);
@ -68,7 +66,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html` const el = await fixture<SlDrawer>(html`
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
@ -88,7 +86,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html` const el = await fixture<SlDrawer>(html`
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> <sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`); `);
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement; const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
@ -108,9 +106,11 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html` const el = await fixture<SlDrawer>(html`
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> <sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`); `);
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement; const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
el.addEventListener('sl-request-close', event => event.preventDefault()); el.addEventListener('sl-request-close', event => {
event.preventDefault();
});
overlay.click(); overlay.click();
expect(el.open).to.be.true; expect(el.open).to.be.true;
@ -118,14 +118,14 @@ describe('<sl-drawer>', () => {
it('should allow initial focus to be set', async () => { it('should allow initial focus to be set', async () => {
const el = await fixture<SlDrawer>(html` <sl-drawer><input /></sl-drawer> `); const el = await fixture<SlDrawer>(html` <sl-drawer><input /></sl-drawer> `);
const input = el.querySelector('input'); const input = el.querySelector<HTMLInputElement>('input')!;
const initialFocusHandler = sinon.spy(event => { const initialFocusHandler = sinon.spy((event: InputEvent) => {
event.preventDefault(); event.preventDefault();
input.focus(); input.focus();
}); });
el.addEventListener('sl-initial-focus', initialFocusHandler); el.addEventListener('sl-initial-focus', initialFocusHandler);
el.show(); void el.show();
await waitUntil(() => initialFocusHandler.calledOnce); await waitUntil(() => initialFocusHandler.calledOnce);

Wyświetl plik

@ -2,19 +2,17 @@ import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { animateTo, stopAnimations } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { HasSlotController } from '../../internal/slot';
import { uppercaseFirstLetter } from '../../internal/string';
import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import styles from './drawer.styles'; import styles from './drawer.styles';
import '~/components/icon-button/icon-button';
import '../icon-button/icon-button'; import { animateTo, stopAnimations } from '~/internal/animate';
import { emit, waitForEvent } from '~/internal/event';
import Modal from '~/internal/modal';
import { lockBodyScrolling, unlockBodyScrolling } from '~/internal/scroll';
import { HasSlotController } from '~/internal/slot';
import { uppercaseFirstLetter } from '~/internal/string';
import { isPreventScrollSupported } from '~/internal/support';
import { watch } from '~/internal/watch';
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
const hasPreventScroll = isPreventScrollSupported(); const hasPreventScroll = isPreventScrollSupported();
@ -73,7 +71,7 @@ export default class SlDrawer extends LitElement {
@query('.drawer__panel') panel: HTMLElement; @query('.drawer__panel') panel: HTMLElement;
@query('.drawer__overlay') overlay: HTMLElement; @query('.drawer__overlay') overlay: HTMLElement;
private hasSlotController = new HasSlotController(this, 'footer'); private readonly hasSlotController = new HasSlotController(this, 'footer');
private modal: Modal; private modal: Modal;
private originalTrigger: HTMLElement | null; private originalTrigger: HTMLElement | null;
@ -123,7 +121,7 @@ export default class SlDrawer extends LitElement {
/** Shows the drawer. */ /** Shows the drawer. */
async show() { async show() {
if (this.open) { if (this.open) {
return; return undefined;
} }
this.open = true; this.open = true;
@ -133,7 +131,7 @@ export default class SlDrawer extends LitElement {
/** Hides the drawer */ /** Hides the drawer */
async hide() { async hide() {
if (!this.open) { if (!this.open) {
return; return undefined;
} }
this.open = false; this.open = false;
@ -144,11 +142,11 @@ export default class SlDrawer extends LitElement {
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true }); const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
if (slRequestClose.defaultPrevented) { if (slRequestClose.defaultPrevented) {
const animation = getAnimation(this, 'drawer.denyClose'); const animation = getAnimation(this, 'drawer.denyClose');
animateTo(this.panel, animation.keyframes, animation.options); void animateTo(this.panel, animation.keyframes, animation.options);
return; return;
} }
this.hide(); void this.hide();
} }
handleKeyDown(event: KeyboardEvent) { handleKeyDown(event: KeyboardEvent) {
@ -217,8 +215,10 @@ export default class SlDrawer extends LitElement {
// Restore focus to the original trigger // Restore focus to the original trigger
const trigger = this.originalTrigger; const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') { if (typeof trigger?.focus === 'function') {
setTimeout(() => trigger.focus()); setTimeout(() => {
trigger.focus();
});
} }
emit(this, 'sl-after-hide'); emit(this, 'sl-after-hide');
@ -242,7 +242,13 @@ export default class SlDrawer extends LitElement {
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
> >
<div part="overlay" class="drawer__overlay" @click=${this.requestClose} tabindex="-1"></div> <div
part="overlay"
class="drawer__overlay"
@click=${this.requestClose}
@keydown=${this.handleKeyDown}
tabindex="-1"
></div>
<div <div
part="panel" part="panel"
@ -259,7 +265,7 @@ export default class SlDrawer extends LitElement {
<header part="header" class="drawer__header"> <header part="header" class="drawer__header">
<span part="title" class="drawer__title" id="title"> <span part="title" class="drawer__title" id="title">
<!-- If there's no label, use an invisible character to prevent the header from collapsing --> <!-- If there's no label, use an invisible character to prevent the header from collapsing -->
<slot name="label"> ${this.label || String.fromCharCode(65279)} </slot> <slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
</span> </span>
<sl-icon-button <sl-icon-button
exportparts="base:close-button" exportparts="base:close-button"

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,7 +1,5 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDropdown from './dropdown'; import type SlDropdown from './dropdown';
describe('<sl-dropdown>', () => { describe('<sl-dropdown>', () => {
@ -16,7 +14,7 @@ describe('<sl-dropdown>', () => {
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
`); `);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement; const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
expect(panel.hidden).to.be.false; expect(panel.hidden).to.be.false;
}); });
@ -32,7 +30,7 @@ describe('<sl-dropdown>', () => {
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
`); `);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement; const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
expect(panel.hidden).to.be.true; expect(panel.hidden).to.be.true;
}); });
@ -48,13 +46,13 @@ describe('<sl-dropdown>', () => {
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
`); `);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement; const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler); el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler); el.addEventListener('sl-after-show', afterShowHandler);
el.show(); void el.show();
await waitUntil(() => showHandler.calledOnce); await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce); await waitUntil(() => afterShowHandler.calledOnce);
@ -75,13 +73,13 @@ describe('<sl-dropdown>', () => {
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
`); `);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement; const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler); el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler); el.addEventListener('sl-after-hide', afterHideHandler);
el.hide(); void el.hide();
await waitUntil(() => hideHandler.calledOnce); await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce); await waitUntil(() => afterHideHandler.calledOnce);
@ -102,7 +100,7 @@ describe('<sl-dropdown>', () => {
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
`); `);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement; const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const showHandler = sinon.spy(); const showHandler = sinon.spy();
const afterShowHandler = sinon.spy(); const afterShowHandler = sinon.spy();
@ -129,7 +127,7 @@ describe('<sl-dropdown>', () => {
</sl-menu> </sl-menu>
</sl-dropdown> </sl-dropdown>
`); `);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement; const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const hideHandler = sinon.spy(); const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy(); const afterHideHandler = sinon.spy();

Wyświetl plik

@ -1,17 +1,17 @@
import type { Instance as PopperInstance } from '@popperjs/core/dist/esm';
import { createPopper } from '@popperjs/core/dist/esm';
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
import { animateTo, stopAnimations } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event';
import { scrollIntoView } from '../../internal/scroll';
import { getTabbableBoundary } from '../../internal/tabbable';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import type SlMenu from '../menu/menu';
import type SlMenuItem from '../menu-item/menu-item';
import styles from './dropdown.styles'; import styles from './dropdown.styles';
import type SlMenuItem from '~/components/menu-item/menu-item';
import type SlMenu from '~/components/menu/menu';
import { animateTo, stopAnimations } from '~/internal/animate';
import { emit, waitForEvent } from '~/internal/event';
import { scrollIntoView } from '~/internal/scroll';
import { getTabbableBoundary } from '~/internal/tabbable';
import { watch } from '~/internal/watch';
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
/** /**
* @since 2.0 * @since 2.0
@ -40,7 +40,7 @@ export default class SlDropdown extends LitElement {
@query('.dropdown__panel') panel: HTMLElement; @query('.dropdown__panel') panel: HTMLElement;
@query('.dropdown__positioner') positioner: HTMLElement; @query('.dropdown__positioner') positioner: HTMLElement;
private popover: PopperInstance; private popover?: PopperInstance;
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */ /** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false; @property({ type: Boolean, reflect: true }) open = false;
@ -73,7 +73,7 @@ export default class SlDropdown extends LitElement {
@property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false; @property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false;
/** The dropdown will close when the user interacts outside of this element (e.g. clicking). */ /** The dropdown will close when the user interacts outside of this element (e.g. clicking). */
@property({ attribute: false }) containingElement: HTMLElement; @property({ attribute: false }) containingElement?: HTMLElement;
/** The distance in pixels from which to offset the panel away from its trigger. */ /** The distance in pixels from which to offset the panel away from its trigger. */
@property({ type: Number }) distance = 0; @property({ type: Number }) distance = 0;
@ -94,12 +94,12 @@ export default class SlDropdown extends LitElement {
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this); this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this); this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
if (!this.containingElement) { if (typeof this.containingElement === 'undefined') {
this.containingElement = this; this.containingElement = this;
} }
// Create the popover after render // Create the popover after render
this.updateComplete.then(() => { void this.updateComplete.then(() => {
this.popover = createPopper(this.trigger, this.positioner, { this.popover = createPopper(this.trigger, this.positioner, {
placement: this.placement, placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute', strategy: this.hoist ? 'fixed' : 'absolute',
@ -127,30 +127,30 @@ export default class SlDropdown extends LitElement {
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
this.hide(); void void this.hide();
if (this.popover) { this.popover?.destroy();
this.popover.destroy();
}
} }
focusOnTrigger() { focusOnTrigger() {
const slot = this.trigger.querySelector('slot')!; const slot = this.trigger.querySelector('slot')!;
const trigger = slot.assignedElements({ flatten: true })[0] as any; const trigger = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
if (trigger && typeof trigger.focus === 'function') { if (typeof trigger?.focus === 'function') {
trigger.focus(); trigger.focus();
} }
} }
getMenu() { getMenu() {
const slot = this.panel.querySelector('slot')!; const slot = this.panel.querySelector('slot')!;
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu; return slot.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'sl-menu') as
| SlMenu
| undefined;
} }
handleDocumentKeyDown(event: KeyboardEvent) { handleDocumentKeyDown(event: KeyboardEvent) {
// Close when escape is pressed // Close when escape is pressed
if (event.key === 'Escape') { if (event.key === 'Escape') {
this.hide(); void this.hide();
this.focusOnTrigger(); this.focusOnTrigger();
return; return;
} }
@ -160,7 +160,7 @@ export default class SlDropdown extends LitElement {
// Tabbing within an open menu should close the dropdown and refocus the trigger // Tabbing within an open menu should close the dropdown and refocus the trigger
if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') { if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') {
event.preventDefault(); event.preventDefault();
this.hide(); void this.hide();
this.focusOnTrigger(); this.focusOnTrigger();
return; return;
} }
@ -171,13 +171,15 @@ export default class SlDropdown extends LitElement {
// otherwise `document.activeElement` will only return the name of the parent shadow DOM element. // otherwise `document.activeElement` will only return the name of the parent shadow DOM element.
setTimeout(() => { setTimeout(() => {
const activeElement = const activeElement =
this.containingElement.getRootNode() instanceof ShadowRoot this.containingElement?.getRootNode() instanceof ShadowRoot
? document.activeElement?.shadowRoot?.activeElement ? document.activeElement?.shadowRoot?.activeElement
: document.activeElement; : document.activeElement;
if (activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement) { if (
this.hide(); typeof this.containingElement === 'undefined' ||
return; activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
) {
void this.hide();
} }
}); });
} }
@ -185,10 +187,9 @@ export default class SlDropdown extends LitElement {
handleDocumentMouseDown(event: MouseEvent) { handleDocumentMouseDown(event: MouseEvent) {
// Close when clicking outside of the containing element // Close when clicking outside of the containing element
const path = event.composedPath() as Array<EventTarget>; const path = event.composedPath();
if (!path.includes(this.containingElement)) { if (typeof this.containingElement !== 'undefined' && !path.includes(this.containingElement)) {
this.hide(); void this.hide();
return;
} }
} }
@ -202,7 +203,7 @@ export default class SlDropdown extends LitElement {
// Hide the dropdown when a menu item is selected // Hide the dropdown when a menu item is selected
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') { if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') {
this.hide(); void this.hide();
this.focusOnTrigger(); this.focusOnTrigger();
} }
} }
@ -212,42 +213,44 @@ export default class SlDropdown extends LitElement {
@watch('placement') @watch('placement')
@watch('skidding') @watch('skidding')
handlePopoverOptionsChange() { handlePopoverOptionsChange() {
if (this.popover) { void this.popover?.setOptions({
this.popover.setOptions({ placement: this.placement,
placement: this.placement, strategy: this.hoist ? 'fixed' : 'absolute',
strategy: this.hoist ? 'fixed' : 'absolute', modifiers: [
modifiers: [ {
{ name: 'flip',
name: 'flip', options: {
options: { boundary: 'viewport'
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
} }
] },
}); {
} name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
} }
handleTriggerClick() { handleTriggerClick() {
this.open ? this.hide() : this.show(); if (this.open) {
void this.hide();
} else {
void this.show();
}
} }
handleTriggerKeyDown(event: KeyboardEvent) { handleTriggerKeyDown(event: KeyboardEvent) {
const menu = this.getMenu(); const menu = this.getMenu();
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : []; const menuItems = typeof menu !== 'undefined' ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
const firstMenuItem = menuItems[0]; const firstMenuItem = menuItems[0];
const lastMenuItem = menuItems[menuItems.length - 1]; const lastMenuItem = menuItems[menuItems.length - 1];
// Close when escape or tab is pressed // Close when escape or tab is pressed
if (event.key === 'Escape') { if (event.key === 'Escape') {
this.focusOnTrigger(); this.focusOnTrigger();
this.hide(); void this.hide();
return; return;
} }
@ -255,7 +258,7 @@ export default class SlDropdown extends LitElement {
// key again to hide the menu in case they don't want to make a selection. // key again to hide the menu in case they don't want to make a selection.
if ([' ', 'Enter'].includes(event.key)) { if ([' ', 'Enter'].includes(event.key)) {
event.preventDefault(); event.preventDefault();
this.open ? this.hide() : this.show(); this.handleTriggerClick();
return; return;
} }
@ -267,19 +270,18 @@ export default class SlDropdown extends LitElement {
// Show the menu if it's not already open // Show the menu if it's not already open
if (!this.open) { if (!this.open) {
this.show(); void this.show();
} }
// Focus on a menu item // Focus on a menu item
if (event.key === 'ArrowDown' && firstMenuItem) { if (event.key === 'ArrowDown' && typeof firstMenuItem !== 'undefined') {
const menu = this.getMenu(); menu!.setCurrentItem(firstMenuItem);
menu.setCurrentItem(firstMenuItem);
firstMenuItem.focus(); firstMenuItem.focus();
return; return;
} }
if (event.key === 'ArrowUp' && lastMenuItem) { if (event.key === 'ArrowUp' && typeof lastMenuItem !== 'undefined') {
menu.setCurrentItem(lastMenuItem); menu!.setCurrentItem(lastMenuItem);
lastMenuItem.focus(); lastMenuItem.focus();
return; return;
} }
@ -287,9 +289,8 @@ export default class SlDropdown extends LitElement {
// Other keys bring focus to the menu and initiate type-to-select behavior // Other keys bring focus to the menu and initiate type-to-select behavior
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt']; const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
if (this.open && menu && !ignoredKeys.includes(event.key)) { if (this.open && !ignoredKeys.includes(event.key)) {
menu.typeToSelect(event.key); menu?.typeToSelect(event.key);
return;
} }
} }
@ -315,22 +316,20 @@ export default class SlDropdown extends LitElement {
// To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger." // To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger."
// //
updateAccessibleTrigger() { updateAccessibleTrigger() {
if (this.trigger) { const slot = this.trigger.querySelector('slot')!;
const slot = this.trigger.querySelector('slot') as HTMLSlotElement; const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[]; const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
if (accessibleTrigger) { if (typeof accessibleTrigger !== 'undefined') {
accessibleTrigger.setAttribute('aria-haspopup', 'true'); accessibleTrigger.setAttribute('aria-haspopup', 'true');
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false'); accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
}
} }
} }
/** Shows the dropdown panel. */ /** Shows the dropdown panel. */
async show() { async show() {
if (this.open) { if (this.open) {
return; return undefined;
} }
this.open = true; this.open = true;
@ -340,7 +339,7 @@ export default class SlDropdown extends LitElement {
/** Hides the dropdown panel */ /** Hides the dropdown panel */
async hide() { async hide() {
if (!this.open) { if (!this.open) {
return; return undefined;
} }
this.open = false; this.open = false;
@ -356,7 +355,7 @@ export default class SlDropdown extends LitElement {
return; return;
} }
this.popover.update(); void this.popover?.update();
} }
@watch('open', { waitUntilFirstUpdate: true }) @watch('open', { waitUntilFirstUpdate: true })
@ -376,7 +375,7 @@ export default class SlDropdown extends LitElement {
document.addEventListener('mousedown', this.handleDocumentMouseDown); document.addEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this); await stopAnimations(this);
this.popover.update(); void this.popover?.update();
this.panel.hidden = false; this.panel.hidden = false;
const { keyframes, options } = getAnimation(this, 'dropdown.show'); const { keyframes, options } = getAnimation(this, 'dropdown.show');
await animateTo(this.panel, keyframes, options); await animateTo(this.panel, keyframes, options);

Wyświetl plik

@ -1,7 +1,7 @@
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { formatBytes } from '../../internal/number'; import { formatBytes } from '~/internal/number';
import { LocalizeController } from '../../utilities/localize'; import { LocalizeController } from '~/utilities/localize';
/** /**
* @since 2.0 * @since 2.0
@ -9,7 +9,7 @@ import { LocalizeController } from '../../utilities/localize';
*/ */
@customElement('sl-format-bytes') @customElement('sl-format-bytes')
export default class SlFormatBytes extends LitElement { export default class SlFormatBytes extends LitElement {
private localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
/** The number to format in bytes. */ /** The number to format in bytes. */
@property({ type: Number }) value = 0; @property({ type: Number }) value = 0;

Wyświetl plik

@ -1,6 +1,6 @@
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { LocalizeController } from '../../utilities/localize'; import { LocalizeController } from '~/utilities/localize';
/** /**
* @since 2.0 * @since 2.0
@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
*/ */
@customElement('sl-format-date') @customElement('sl-format-date')
export default class SlFormatDate extends LitElement { export default class SlFormatDate extends LitElement {
private localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
/** The date/time to format. If not set, the current date and time will be used. */ /** The date/time to format. If not set, the current date and time will be used. */
@property() date: Date | string = new Date(); @property() date: Date | string = new Date();
@ -55,7 +55,7 @@ export default class SlFormatDate extends LitElement {
// Check for an invalid date // Check for an invalid date
if (isNaN(date.getMilliseconds())) { if (isNaN(date.getMilliseconds())) {
return; return undefined;
} }
return this.localize.date(date, { return this.localize.date(date, {

Wyświetl plik

@ -1,6 +1,6 @@
import { LitElement } from 'lit'; import { LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { LocalizeController } from '../../utilities/localize'; import { LocalizeController } from '~/utilities/localize';
/** /**
* @since 2.0 * @since 2.0
@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
*/ */
@customElement('sl-format-number') @customElement('sl-format-number')
export default class SlFormatNumber extends LitElement { export default class SlFormatNumber extends LitElement {
private localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
/** The number to format. */ /** The number to format. */
@property({ type: Number }) value = 0; @property({ type: Number }) value = 0;

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -3,8 +3,7 @@ import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import styles from './icon-button.styles'; import styles from './icon-button.styles';
import '~/components/icon/icon';
import '../icon/icon';
/** /**
* @since 2.0 * @since 2.0
@ -21,22 +20,22 @@ export default class SlIconButton extends LitElement {
@query('button') button: HTMLButtonElement | HTMLLinkElement; @query('button') button: HTMLButtonElement | HTMLLinkElement;
/** The name of the icon to draw. */ /** The name of the icon to draw. */
@property() name: string; @property() name?: string;
/** The name of a registered custom icon library. */ /** The name of a registered custom icon library. */
@property() library: string; @property() library?: string;
/** An external URL of an SVG file. */ /** An external URL of an SVG file. */
@property() src: string; @property() src?: string;
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */ /** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
@property() href: string; @property() href?: string;
/** Tells the browser where to open the link. Only used when `href` is set. */ /** Tells the browser where to open the link. Only used when `href` is set. */
@property() target: '_blank' | '_parent' | '_self' | '_top'; @property() target?: '_blank' | '_parent' | '_self' | '_top';
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */ /** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
@property() download: string; @property() download?: string;
/** /**
* A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should * A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should
@ -48,7 +47,7 @@ export default class SlIconButton extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false; @property({ type: Boolean, reflect: true }) disabled = false;
render() { render() {
const isLink = this.href ? true : false; const isLink = typeof this.href !== 'undefined';
const interior = html` const interior = html`
<sl-icon <sl-icon
@ -67,7 +66,7 @@ export default class SlIconButton extends LitElement {
href=${ifDefined(this.href)} href=${ifDefined(this.href)}
target=${ifDefined(this.target)} target=${ifDefined(this.target)}
download=${ifDefined(this.download)} download=${ifDefined(this.download)}
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)} rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
role="button" role="button"
aria-disabled=${this.disabled ? 'true' : 'false'} aria-disabled=${this.disabled ? 'true' : 'false'}
aria-label="${this.label}" aria-label="${this.label}"

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -2,11 +2,11 @@ import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { emit } from '../../internal/event'; import styles from './icon.styles';
import { watch } from '../../internal/watch';
import { getIconLibrary, watchIcon, unwatchIcon } from './library'; import { getIconLibrary, watchIcon, unwatchIcon } from './library';
import { requestIcon } from './request'; import { requestIcon } from './request';
import styles from './icon.styles'; import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
const parser = new DOMParser(); const parser = new DOMParser();
@ -26,13 +26,17 @@ export default class SlIcon extends LitElement {
@state() private svg = ''; @state() private svg = '';
/** The name of the icon to draw. */ /** The name of the icon to draw. */
@property() name: string; @property() name?: string;
/** An external URL of an SVG file. */ /**
@property() src: string; * An external URL of an SVG file.
*
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
*/
@property() src?: string;
/** An alternate description to use for accessibility. If omitted, the icon will be ignored by assistive devices. */ /** An alternate description to use for accessibility. If omitted, the icon will be ignored by assistive devices. */
@property() label: string; @property() label = '';
/** The name of a registered custom icon library. */ /** The name of a registered custom icon library. */
@property() library = 'default'; @property() library = 'default';
@ -43,7 +47,7 @@ export default class SlIcon extends LitElement {
} }
firstUpdated() { firstUpdated() {
this.setIcon(); void this.setIcon();
} }
disconnectedCallback() { disconnectedCallback() {
@ -51,18 +55,17 @@ export default class SlIcon extends LitElement {
unwatchIcon(this); unwatchIcon(this);
} }
private getUrl(): string { private getUrl() {
const library = getIconLibrary(this.library); const library = getIconLibrary(this.library);
if (this.name && library) { if (typeof this.name !== 'undefined' && typeof library !== 'undefined') {
return library.resolver(this.name); return library.resolver(this.name);
} else {
return this.src;
} }
return this.src;
} }
/** @internal Fetches the icon and redraws it. Used to handle library registrations. */ /** @internal Fetches the icon and redraws it. Used to handle library registrations. */
redraw() { redraw() {
this.setIcon(); void this.setIcon();
} }
@watch('name') @watch('name')
@ -71,7 +74,7 @@ export default class SlIcon extends LitElement {
async setIcon() { async setIcon() {
const library = getIconLibrary(this.library); const library = getIconLibrary(this.library);
const url = this.getUrl(); const url = this.getUrl();
if (url) { if (typeof url !== 'undefined' && url.length > 0) {
try { try {
const file = await requestIcon(url)!; const file = await requestIcon(url)!;
if (url !== this.getUrl()) { if (url !== this.getUrl()) {
@ -81,10 +84,8 @@ export default class SlIcon extends LitElement {
const doc = parser.parseFromString(file.svg, 'text/html'); const doc = parser.parseFromString(file.svg, 'text/html');
const svgEl = doc.body.querySelector('svg'); const svgEl = doc.body.querySelector('svg');
if (svgEl) { if (svgEl !== null) {
if (library && library.mutator) { library?.mutator?.(svgEl);
library.mutator(svgEl);
}
this.svg = svgEl.outerHTML; this.svg = svgEl.outerHTML;
emit(this, 'sl-load'); emit(this, 'sl-load');
@ -99,14 +100,14 @@ export default class SlIcon extends LitElement {
} catch { } catch {
emit(this, 'sl-error', { detail: { status: -1 } }); emit(this, 'sl-error', { detail: { status: -1 } });
} }
} else if (this.svg) { } else if (this.svg.length > 0) {
// If we can't resolve a URL and an icon was previously set, remove it // If we can't resolve a URL and an icon was previously set, remove it
this.svg = ''; this.svg = '';
} }
} }
handleChange() { handleChange() {
this.setIcon(); void this.setIcon();
} }
render() { render() {

Wyświetl plik

@ -1,5 +1,5 @@
import { getBasePath } from '../../utilities/base-path';
import type { IconLibrary } from './library'; import type { IconLibrary } from './library';
import { getBasePath } from '~/utilities/base-path';
const library: IconLibrary = { const library: IconLibrary = {
name: 'default', name: 'default',

Wyświetl plik

@ -86,11 +86,10 @@ const icons = {
const systemLibrary: IconLibrary = { const systemLibrary: IconLibrary = {
name: 'system', name: 'system',
resolver: (name: keyof typeof icons) => { resolver: (name: keyof typeof icons) => {
if (icons[name]) { if (name in icons) {
return `data:image/svg+xml,${encodeURIComponent(icons[name])}`; return `data:image/svg+xml,${encodeURIComponent(icons[name])}`;
} else {
return '';
} }
return '';
} }
}; };

Wyświetl plik

@ -1,6 +1,6 @@
import defaultLibrary from './library.default'; import defaultLibrary from './library.default';
import systemLibrary from './library.system'; import systemLibrary from './library.system';
import type SlIcon from '../icon/icon'; import type SlIcon from '~/components/icon/icon';
export type IconLibraryResolver = (name: string) => string; export type IconLibraryResolver = (name: string) => string;
export type IconLibraryMutator = (svg: SVGElement) => void; export type IconLibraryMutator = (svg: SVGElement) => void;
@ -22,7 +22,7 @@ export function unwatchIcon(icon: SlIcon) {
} }
export function getIconLibrary(name?: string) { export function getIconLibrary(name?: string) {
return registry.filter(lib => lib.name === name)[0]; return registry.find(lib => lib.name === name);
} }
export function registerIconLibrary( export function registerIconLibrary(
@ -37,7 +37,7 @@ export function registerIconLibrary(
}); });
// Redraw watched icons // Redraw watched icons
watchedIcons.map(icon => { watchedIcons.forEach(icon => {
if (icon.library === name) { if (icon.library === name) {
icon.redraw(); icon.redraw();
} }

Wyświetl plik

@ -1,36 +1,42 @@
interface IconFile { import { requestInclude } from '~/components/include/request';
type IconFile =
| {
ok: true;
status: number;
svg: string;
}
| {
ok: false;
status: number;
svg: null;
};
interface IconFileUnknown {
ok: boolean; ok: boolean;
status: number; status: number;
svg: string; svg: string | null;
} }
const iconFiles = new Map<string, Promise<IconFile>>(); const iconFiles = new Map<string, IconFile>();
export const requestIcon = (url: string) => { export async function requestIcon(url: string): Promise<IconFile> {
if (iconFiles.has(url)) { if (iconFiles.has(url)) {
return iconFiles.get(url); return iconFiles.get(url)!;
} else {
const request = fetch(url).then(async response => {
if (response.ok) {
const div = document.createElement('div');
div.innerHTML = await response.text();
const svg = div.firstElementChild;
return {
ok: response.ok,
status: response.status,
svg: svg && svg.tagName.toLowerCase() === 'svg' ? svg.outerHTML : ''
};
} else {
return {
ok: response.ok,
status: response.status,
svg: null
};
}
}) as Promise<IconFile>;
iconFiles.set(url, request);
return request;
} }
}; const fileData = await requestInclude(url);
const iconFileData: IconFileUnknown = {
ok: fileData.ok,
status: fileData.status,
svg: null
};
if (fileData.ok) {
const div = document.createElement('div');
div.innerHTML = fileData.html;
const svg = div.firstElementChild;
iconFileData.svg = svg?.tagName.toLowerCase() === 'svg' ? svg.outerHTML : '';
}
iconFiles.set(url, iconFileData as IconFile);
return iconFileData as IconFile;
}

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,13 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { clamp } from '../../internal/math';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import styles from './image-comparer.styles'; import styles from './image-comparer.styles';
import '~/components/icon/icon';
import '../icon/icon'; import { autoIncrement } from '~/internal/autoIncrement';
import { drag } from '~/internal/drag';
import { emit } from '~/internal/event';
import { clamp } from '~/internal/math';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -36,42 +37,19 @@ export default class SlImageComparer extends LitElement {
@query('.image-comparer') base: HTMLElement; @query('.image-comparer') base: HTMLElement;
@query('.image-comparer__handle') handle: HTMLElement; @query('.image-comparer__handle') handle: HTMLElement;
private readonly attrId = autoIncrement();
private readonly containerId = `comparer-container-${this.attrId}`;
/** The position of the divider as a percentage. */ /** The position of the divider as a percentage. */
@property({ type: Number, reflect: true }) position = 50; @property({ type: Number, reflect: true }) position = 50;
handleDrag(event: any) { handleDrag(event: Event) {
const { width } = this.base.getBoundingClientRect(); const { width } = this.base.getBoundingClientRect();
function drag(event: any, container: HTMLElement, onMove: (x: number) => void) {
const move = (event: any) => {
const { left } = container.getBoundingClientRect();
const { pageXOffset } = container.ownerDocument.defaultView!;
const offsetX = left + pageXOffset;
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
onMove(x);
};
// Move on init
move(event);
const stop = () => {
document.removeEventListener('mousemove', move);
document.removeEventListener('touchmove', move);
document.removeEventListener('mouseup', stop);
document.removeEventListener('touchend', stop);
};
document.addEventListener('mousemove', move);
document.addEventListener('touchmove', move);
document.addEventListener('mouseup', stop);
document.addEventListener('touchend', stop);
}
event.preventDefault(); event.preventDefault();
drag(event, this.base, x => { drag(this.base, x => {
this.position = Number(clamp((x / width) * 100, 0, 100).toFixed(2)); this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
}); });
} }
@ -82,10 +60,18 @@ export default class SlImageComparer extends LitElement {
event.preventDefault(); event.preventDefault();
if (event.key === 'ArrowLeft') newPosition = newPosition - incr; if (event.key === 'ArrowLeft') {
if (event.key === 'ArrowRight') newPosition = newPosition + incr; newPosition -= incr;
if (event.key === 'Home') newPosition = 0; }
if (event.key === 'End') newPosition = 100; if (event.key === 'ArrowRight') {
newPosition += incr;
}
if (event.key === 'Home') {
newPosition = 0;
}
if (event.key === 'End') {
newPosition = 100;
}
newPosition = clamp(newPosition, 0, 100); newPosition = clamp(newPosition, 0, 100);
this.position = newPosition; this.position = newPosition;
@ -99,7 +85,7 @@ export default class SlImageComparer extends LitElement {
render() { render() {
return html` return html`
<div part="base" class="image-comparer" @keydown=${this.handleKeyDown}> <div part="base" class="image-comparer" @keydown=${this.handleKeyDown} id=${this.containerId}>
<div class="image-comparer__image"> <div class="image-comparer__image">
<div part="before" class="image-comparer__before"> <div part="before" class="image-comparer__before">
<slot name="before"></slot> <slot name="before"></slot>
@ -117,7 +103,7 @@ export default class SlImageComparer extends LitElement {
<div <div
part="divider" part="divider"
class="image-comparer__divider" class="image-comparer__divider"
style=${styleMap({ left: this.position + '%' })} style=${styleMap({ left: `${this.position}%` })}
@mousedown=${this.handleDrag} @mousedown=${this.handleDrag}
@touchstart=${this.handleDrag} @touchstart=${this.handleDrag}
> >
@ -128,6 +114,7 @@ export default class SlImageComparer extends LitElement {
aria-valuenow=${this.position} aria-valuenow=${this.position}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
aria-controls=${this.containerId}
tabindex="0" tabindex="0"
> >
<slot name="handle-icon"> <slot name="handle-icon">

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,7 +1,5 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon'; import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlInclude from './include'; import type SlInclude from './include';
const stubbedFetchResponse: Response = { const stubbedFetchResponse: Response = {
@ -22,6 +20,14 @@ const stubbedFetchResponse: Response = {
clone: sinon.fake() clone: sinon.fake()
}; };
function delayResolve(resolveValue: string) {
return new Promise<string>(resolve => {
setTimeout(() => {
resolve(resolveValue);
});
});
}
describe('<sl-include>', () => { describe('<sl-include>', () => {
afterEach(() => { afterEach(() => {
sinon.verifyAndRestore(); sinon.verifyAndRestore();
@ -32,7 +38,7 @@ describe('<sl-include>', () => {
...stubbedFetchResponse, ...stubbedFetchResponse,
ok: true, ok: true,
status: 200, status: 200,
text: () => Promise.resolve('"id": 1') text: () => delayResolve('"id": 1')
}); });
const el = await fixture<SlInclude>(html` <sl-include src="/found"></sl-include> `); const el = await fixture<SlInclude>(html` <sl-include src="/found"></sl-include> `);
const loadHandler = sinon.spy(); const loadHandler = sinon.spy();
@ -49,7 +55,7 @@ describe('<sl-include>', () => {
...stubbedFetchResponse, ...stubbedFetchResponse,
ok: false, ok: false,
status: 404, status: 404,
text: () => Promise.resolve('{}') text: () => delayResolve('{}')
}); });
const el = await fixture<SlInclude>(html` <sl-include src="/not-found"></sl-include> `); const el = await fixture<SlInclude>(html` <sl-include src="/not-found"></sl-include> `);
const loadHandler = sinon.spy(); const loadHandler = sinon.spy();

Wyświetl plik

@ -1,9 +1,9 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { requestInclude } from './request';
import styles from './include.styles'; import styles from './include.styles';
import { requestInclude } from './request';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -16,7 +16,11 @@ import styles from './include.styles';
export default class SlInclude extends LitElement { export default class SlInclude extends LitElement {
static styles = styles; static styles = styles;
/** The location of the HTML file to include. */ /**
* The location of the HTML file to include.
*
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
*/
@property() src: string; @property() src: string;
/** The fetch mode to use. */ /** The fetch mode to use. */
@ -31,7 +35,9 @@ export default class SlInclude extends LitElement {
executeScript(script: HTMLScriptElement) { executeScript(script: HTMLScriptElement) {
// Create a copy of the script and swap it out so the browser executes it // Create a copy of the script and swap it out so the browser executes it
const newScript = document.createElement('script'); const newScript = document.createElement('script');
[...script.attributes].map(attr => newScript.setAttribute(attr.name, attr.value)); [...script.attributes].forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = script.textContent; newScript.textContent = script.textContent;
script.parentNode!.replaceChild(newScript, script); script.parentNode!.replaceChild(newScript, script);
} }
@ -47,7 +53,7 @@ export default class SlInclude extends LitElement {
return; return;
} }
if (!file) { if (typeof file === 'undefined') {
return; return;
} }
@ -59,7 +65,9 @@ export default class SlInclude extends LitElement {
this.innerHTML = file.html; this.innerHTML = file.html;
if (this.allowScripts) { if (this.allowScripts) {
[...this.querySelectorAll('script')].map(script => this.executeScript(script)); [...this.querySelectorAll('script')].forEach(script => {
this.executeScript(script);
});
} }
emit(this, 'sl-load'); emit(this, 'sl-load');

Wyświetl plik

@ -6,18 +6,17 @@ interface IncludeFile {
const includeFiles = new Map<string, Promise<IncludeFile>>(); const includeFiles = new Map<string, Promise<IncludeFile>>();
export const requestInclude = async (src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors') => { export function requestInclude(src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors'): Promise<IncludeFile> {
if (includeFiles.has(src)) { if (includeFiles.has(src)) {
return includeFiles.get(src); return includeFiles.get(src)!;
} else {
const request = fetch(src, { mode: mode }).then(async response => {
return {
ok: response.ok,
status: response.status,
html: await response.text()
};
});
includeFiles.set(src, request);
return request;
} }
}; const fileDataPromise = fetch(src, { mode: mode }).then(async response => {
return {
ok: response.ok,
status: response.status,
html: await response.text()
};
});
includeFiles.set(src, fileDataPromise);
return fileDataPromise;
}

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
import formControlStyles from '../../styles/form-control.styles'; import formControlStyles from '~/styles/form-control.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,13 +1,10 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlInput from './input'; import type SlInput from './input';
describe('<sl-input>', () => { describe('<sl-input>', () => {
it('should be disabled with the disabled attribute', async () => { it('should be disabled with the disabled attribute', async () => {
const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `); const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `);
const input = el.shadowRoot?.querySelector('[part="input"]') as HTMLInputElement; const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part="input"]')!;
expect(input.disabled).to.be.true; expect(input.disabled).to.be.true;
}); });

Wyświetl plik

@ -1,17 +1,15 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js'; import { customElement, property, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js'; import { live } from 'lit/directives/live.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { FormSubmitController, getLabelledBy, renderFormControl } from '../../internal/form-control';
import { HasSlotController } from '../../internal/slot';
import styles from './input.styles'; import styles from './input.styles';
import '~/components/icon/icon';
import '../icon/icon'; import { autoIncrement } from '~/internal/autoIncrement';
import { emit } from '~/internal/event';
let id = 0; import { FormSubmitController, getLabelledBy, renderFormControl } from '~/internal/form-control';
import { HasSlotController } from '~/internal/slot';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -49,12 +47,13 @@ export default class SlInput extends LitElement {
@query('.input__control') input: HTMLInputElement; @query('.input__control') input: HTMLInputElement;
// @ts-ignore // @ts-expect-error -- Controller is currently unused
private formSubmitController = new FormSubmitController(this); private readonly formSubmitController = new FormSubmitController(this);
private hasSlotController = new HasSlotController(this, 'help-text', 'label'); private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
private inputId = `input-${++id}`; private readonly attrId = autoIncrement();
private helpTextId = `input-help-text-${id}`; private readonly inputId = `input-${this.attrId}`;
private labelId = `input-label-${id}`; private readonly helpTextId = `input-help-text-${this.attrId}`;
private readonly labelId = `input-label-${this.attrId}`;
@state() private hasFocus = false; @state() private hasFocus = false;
@state() private isPasswordVisible = false; @state() private isPasswordVisible = false;
@ -79,7 +78,7 @@ export default class SlInput extends LitElement {
@property({ type: Boolean, reflect: true }) pill = false; @property({ type: Boolean, reflect: true }) pill = false;
/** The input's label. Alternatively, you can use the label slot. */ /** The input's label. Alternatively, you can use the label slot. */
@property() label: string; @property() label = '';
/** The input's help text. Alternatively, you can use the help-text slot. */ /** The input's help text. Alternatively, you can use the help-text slot. */
@property({ attribute: 'help-text' }) helpText = ''; @property({ attribute: 'help-text' }) helpText = '';
@ -146,7 +145,7 @@ export default class SlInput extends LitElement {
/** Gets or sets the current value as a `Date` object. Only valid when `type` is `date`. */ /** Gets or sets the current value as a `Date` object. Only valid when `type` is `date`. */
get valueAsDate() { get valueAsDate() {
return this.input.valueAsDate as Date; return this.input.valueAsDate!;
} }
set valueAsDate(newValue: Date) { set valueAsDate(newValue: Date) {
@ -156,7 +155,7 @@ export default class SlInput extends LitElement {
/** Gets or sets the current value as a number. */ /** Gets or sets the current value as a number. */
get valueAsNumber() { get valueAsNumber() {
return this.input.valueAsNumber as number; return this.input.valueAsNumber;
} }
set valueAsNumber(newValue: number) { set valueAsNumber(newValue: number) {
@ -180,7 +179,7 @@ export default class SlInput extends LitElement {
/** Selects all the text in the input. */ /** Selects all the text in the input. */
select() { select() {
return this.input.select(); this.input.select();
} }
/** Sets the start and end positions of the text selection (0-based). */ /** Sets the start and end positions of the text selection (0-based). */
@ -189,7 +188,7 @@ export default class SlInput extends LitElement {
selectionEnd: number, selectionEnd: number,
selectionDirection: 'forward' | 'backward' | 'none' = 'none' selectionDirection: 'forward' | 'backward' | 'none' = 'none'
) { ) {
return this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection); this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
} }
/** Replaces a range of text with a new string. */ /** Replaces a range of text with a new string. */
@ -239,13 +238,11 @@ export default class SlInput extends LitElement {
event.stopPropagation(); event.stopPropagation();
} }
@watch('disabled') @watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() { handleDisabledChange() {
// Disabled form controls are always valid, so we need to recheck validity when the state changes // Disabled form controls are always valid, so we need to recheck validity when the state changes
if (this.input) { this.input.disabled = this.disabled;
this.input.disabled = this.disabled; this.invalid = !this.input.checkValidity();
this.invalid = !this.input.checkValidity();
}
} }
handleFocus() { handleFocus() {
@ -266,11 +263,9 @@ export default class SlInput extends LitElement {
this.isPasswordVisible = !this.isPasswordVisible; this.isPasswordVisible = !this.isPasswordVisible;
} }
@watch('value') @watch('value', { waitUntilFirstUpdate: true })
handleValueChange() { handleValueChange() {
if (this.input) { this.invalid = !this.input.checkValidity();
this.invalid = !this.input.checkValidity();
}
} }
render() { render() {
@ -306,7 +301,7 @@ export default class SlInput extends LitElement {
'input--filled': this.filled, 'input--filled': this.filled,
'input--disabled': this.disabled, 'input--disabled': this.disabled,
'input--focused': this.hasFocus, 'input--focused': this.hasFocus,
'input--empty': this.value?.length === 0, 'input--empty': this.value.length === 0,
'input--invalid': this.invalid 'input--invalid': this.invalid
})} })}
> >
@ -355,7 +350,7 @@ export default class SlInput extends LitElement {
@blur=${this.handleBlur} @blur=${this.handleBlur}
/> />
${this.clearable && this.value?.length > 0 ${this.clearable && this.value.length > 0
? html` ? html`
<button <button
part="clear-button" part="clear-button"

Wyświetl plik

@ -1,6 +1,6 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import { focusVisibleSelector } from '~/internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,10 +1,9 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js'; import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { watch } from '../../internal/watch';
import styles from './menu-item.styles'; import styles from './menu-item.styles';
import '~/components/icon/icon';
import '../icon/icon'; import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0
@ -43,12 +42,12 @@ export default class SlMenuItem extends LitElement {
@watch('checked') @watch('checked')
handleCheckedChange() { handleCheckedChange() {
this.setAttribute('aria-checked', String(this.checked)); this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
} }
@watch('disabled') @watch('disabled')
handleDisabledChange() { handleDisabledChange() {
this.setAttribute('aria-disabled', String(this.disabled)); this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
} }
render() { render() {

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,10 +1,14 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, query } from 'lit/decorators.js'; import { customElement, query } from 'lit/decorators.js';
import { emit } from '../../internal/event';
import { getTextContent } from '../../internal/slot';
import { hasFocusVisible } from '../../internal/focus-visible';
import type SlMenuItem from '../menu-item/menu-item';
import styles from './menu.styles'; import styles from './menu.styles';
import type SlMenuItem from '~/components/menu-item/menu-item';
import { emit } from '~/internal/event';
import { hasFocusVisible } from '~/internal/focus-visible';
import { getTextContent } from '~/internal/slot';
export interface MenuSelectEventDetail {
item: SlMenuItem;
}
/** /**
* @since 2.0 * @since 2.0
@ -24,7 +28,7 @@ export default class SlMenu extends LitElement {
@query('slot') defaultSlot: HTMLSlotElement; @query('slot') defaultSlot: HTMLSlotElement;
private typeToSelectString = ''; private typeToSelectString = '';
private typeToSelectTimeout: any; private typeToSelectTimeout: NodeJS.Timeout;
firstUpdated() { firstUpdated() {
this.setAttribute('role', 'menu'); this.setAttribute('role', 'menu');
@ -36,7 +40,7 @@ export default class SlMenu extends LitElement {
return false; return false;
} }
if (!options?.includeDisabled && (el as SlMenuItem).disabled) { if (!options.includeDisabled && (el as SlMenuItem).disabled) {
return false; return false;
} }
@ -58,10 +62,12 @@ export default class SlMenu extends LitElement {
*/ */
setCurrentItem(item: SlMenuItem) { setCurrentItem(item: SlMenuItem) {
const items = this.getAllItems({ includeDisabled: false }); const items = this.getAllItems({ includeDisabled: false });
let activeItem = item.disabled ? items[0] : item; const activeItem = item.disabled ? items[0] : item;
// Update tab indexes // Update tab indexes
items.map(i => i.setAttribute('tabindex', i === activeItem ? '0' : '-1')); items.forEach(i => {
i.setAttribute('tabindex', i === activeItem ? '0' : '-1');
});
} }
/** /**
@ -78,13 +84,15 @@ export default class SlMenu extends LitElement {
// Restore focus in browsers that don't support :focus-visible when using the keyboard // Restore focus in browsers that don't support :focus-visible when using the keyboard
if (!hasFocusVisible) { if (!hasFocusVisible) {
items.map(item => item.classList.remove('sl-focus-invisible')); items.forEach(item => {
item.classList.remove('sl-focus-invisible');
});
} }
for (const item of items) { for (const item of items) {
const slot = item.shadowRoot!.querySelector('slot:not([name])') as HTMLSlotElement; const slot = item.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');
const label = getTextContent(slot).toLowerCase().trim(); const label = getTextContent(slot).toLowerCase().trim();
if (label.substring(0, this.typeToSelectString.length) === this.typeToSelectString) { if (label.startsWith(this.typeToSelectString)) {
this.setCurrentItem(item); this.setCurrentItem(item);
// Set focus here to force the browser to show :focus-visible styles // Set focus here to force the browser to show :focus-visible styles
@ -96,9 +104,9 @@ export default class SlMenu extends LitElement {
handleClick(event: MouseEvent) { handleClick(event: MouseEvent) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
const item = target.closest('sl-menu-item') as SlMenuItem; const item = target.closest('sl-menu-item');
if (item && !item.disabled) { if (item?.disabled === false) {
emit(this, 'sl-select', { detail: { item } }); emit(this, 'sl-select', { detail: { item } });
} }
} }
@ -107,7 +115,9 @@ export default class SlMenu extends LitElement {
// Restore focus in browsers that don't support :focus-visible when using the keyboard // Restore focus in browsers that don't support :focus-visible when using the keyboard
if (!hasFocusVisible) { if (!hasFocusVisible) {
const items = this.getAllItems(); const items = this.getAllItems();
items.map(item => item.classList.remove('sl-focus-invisible')); items.forEach(item => {
item.classList.remove('sl-focus-invisible');
});
} }
} }
@ -117,10 +127,8 @@ export default class SlMenu extends LitElement {
const item = this.getCurrentItem(); const item = this.getCurrentItem();
event.preventDefault(); event.preventDefault();
if (item) { // Simulate a click to support @click handlers on menu items that also work with the keyboard
// Simulate a click to support @click handlers on menu items that also work with the keyboard item?.click();
item.click();
}
} }
// Prevent scrolling when space is pressed // Prevent scrolling when space is pressed
@ -132,9 +140,9 @@ export default class SlMenu extends LitElement {
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
const items = this.getAllItems({ includeDisabled: false }); const items = this.getAllItems({ includeDisabled: false });
const activeItem = this.getCurrentItem(); const activeItem = this.getCurrentItem();
let index = activeItem ? items.indexOf(activeItem) : 0; let index = typeof activeItem !== 'undefined' ? items.indexOf(activeItem) : 0;
if (items.length) { if (items.length > 0) {
event.preventDefault(); event.preventDefault();
if (event.key === 'ArrowDown') { if (event.key === 'ArrowDown') {
@ -147,8 +155,12 @@ export default class SlMenu extends LitElement {
index = items.length - 1; index = items.length - 1;
} }
if (index < 0) index = 0; if (index < 0) {
if (index > items.length - 1) index = items.length - 1; index = 0;
}
if (index > items.length - 1) {
index = items.length - 1;
}
this.setCurrentItem(items[index]); this.setCurrentItem(items[index]);
items[index].focus(); items[index].focus();
@ -177,7 +189,7 @@ export default class SlMenu extends LitElement {
const items = this.getAllItems({ includeDisabled: false }); const items = this.getAllItems({ includeDisabled: false });
// Reset the roving tab index when the slotted items change // Reset the roving tab index when the slotted items change
if (items.length) { if (items.length > 0) {
this.setCurrentItem(items[0]); this.setCurrentItem(items[0]);
} }
} }

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,8 +1,4 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlMutationObserver from './mutation-observer';
describe('<sl-mutation-observer>', () => { describe('<sl-mutation-observer>', () => {
it('should render a component', async () => { it('should render a component', async () => {

Wyświetl plik

@ -1,8 +1,8 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import styles from './mutation-observer.styles'; import styles from './mutation-observer.styles';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/** /**
* @since 2.0 * @since 2.0

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlProgressBar from './progress-bar'; import type SlProgressBar from './progress-bar';
describe('<sl-progress-bar>', () => { describe('<sl-progress-bar>', () => {
let el: SlProgressBar; let el: SlProgressBar;
describe('when provided just a value parameter', async () => { describe('when provided just a value parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`); el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
}); });
@ -16,7 +14,7 @@ describe('<sl-progress-bar>', () => {
}); });
}); });
describe('when provided a title, and value parameter', async () => { describe('when provided a title, and value parameter', () => {
let base: HTMLDivElement; let base: HTMLDivElement;
let indicator: HTMLDivElement; let indicator: HTMLDivElement;
@ -24,43 +22,43 @@ describe('<sl-progress-bar>', () => {
el = await fixture<SlProgressBar>( el = await fixture<SlProgressBar>(
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>` html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
); );
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement; base = el.shadowRoot!.querySelector('[part="base"]')!;
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement; indicator = el.shadowRoot!.querySelector('[part="indicator"]')!;
}); });
it('should render a component that passes accessibility test.', async () => { it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('uses the value parameter on the base, as aria-valuenow', async () => { it('uses the value parameter on the base, as aria-valuenow', () => {
expect(base).attribute('aria-valuenow', '25'); expect(base).attribute('aria-valuenow', '25');
}); });
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', async () => { it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', () => {
expect(indicator).attribute('style', 'width:25%;'); expect(indicator).attribute('style', 'width:25%;');
}); });
}); });
describe('when provided an indeterminate parameter', async () => { describe('when provided an indeterminate parameter', () => {
let base: HTMLDivElement; let base: HTMLDivElement;
before(async () => { before(async () => {
el = await fixture<SlProgressBar>( el = await fixture<SlProgressBar>(
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>` html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
); );
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement; base = el.shadowRoot!.querySelector('[part="base"]')!;
}); });
it('should render a component that passes accessibility test.', async () => { it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('should append a progress-bar--indeterminate class to the "base" part.', async () => { it('should append a progress-bar--indeterminate class to the "base" part.', () => {
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate'); expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
}); });
}); });
describe('when provided a ariaLabel, and value parameter', async () => { describe('when provided a ariaLabel, and value parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlProgressBar>( el = await fixture<SlProgressBar>(
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>` html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
@ -72,7 +70,7 @@ describe('<sl-progress-bar>', () => {
}); });
}); });
describe('when provided a ariaLabelledBy, and value parameter', async () => { describe('when provided a ariaLabelledBy, and value parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlProgressBar>( el = await fixture<SlProgressBar>(
html` html`

Wyświetl plik

@ -1,10 +1,10 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { LocalizeController } from '../../utilities/localize';
import styles from './progress-bar.styles'; import styles from './progress-bar.styles';
import { LocalizeController } from '~/utilities/localize';
/** /**
* @since 2.0 * @since 2.0
@ -24,7 +24,7 @@ import styles from './progress-bar.styles';
@customElement('sl-progress-bar') @customElement('sl-progress-bar')
export default class SlProgressBar extends LitElement { export default class SlProgressBar extends LitElement {
static styles = styles; static styles = styles;
private localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
/** The current progress, 0 to 100. */ /** The current progress, 0 to 100. */
@property({ type: Number, reflect: true }) value = 0; @property({ type: Number, reflect: true }) value = 0;
@ -33,7 +33,7 @@ export default class SlProgressBar extends LitElement {
@property({ type: Boolean, reflect: true }) indeterminate = false; @property({ type: Boolean, reflect: true }) indeterminate = false;
/** A custom label for the progress bar's aria label. */ /** A custom label for the progress bar's aria label. */
@property() label: string; @property() label = '';
/** The locale to render the component in. */ /** The locale to render the component in. */
@property() lang: string; @property() lang: string;
@ -48,12 +48,12 @@ export default class SlProgressBar extends LitElement {
})} })}
role="progressbar" role="progressbar"
title=${ifDefined(this.title)} title=${ifDefined(this.title)}
aria-label=${this.label || this.localize.term('progress')} aria-label=${this.label.length > 0 ? this.label : this.localize.term('progress')}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
aria-valuenow=${this.indeterminate ? 0 : this.value} aria-valuenow=${this.indeterminate ? 0 : this.value}
> >
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.value + '%' })}> <div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: `${this.value}%` })}>
${!this.indeterminate ${!this.indeterminate
? html` ? html`
<span part="label" class="progress-bar__label"> <span part="label" class="progress-bar__label">

Wyświetl plik

@ -1,5 +1,5 @@
import { css } from 'lit'; import { css } from 'lit';
import componentStyles from '../../styles/component.styles'; import componentStyles from '~/styles/component.styles';
export default css` export default css`
${componentStyles} ${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing'; import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlProgressRing from './progress-ring'; import type SlProgressRing from './progress-ring';
describe('<sl-progress-ring>', () => { describe('<sl-progress-ring>', () => {
let el: SlProgressRing; let el: SlProgressRing;
describe('when provided just a value parameter', async () => { describe('when provided just a value parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`); el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
}); });
@ -16,30 +14,30 @@ describe('<sl-progress-ring>', () => {
}); });
}); });
describe('when provided a title, and value parameter', async () => { describe('when provided a title, and value parameter', () => {
let base: HTMLDivElement; let base: HTMLDivElement;
before(async () => { before(async () => {
el = await fixture<SlProgressRing>( el = await fixture<SlProgressRing>(
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>` html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
); );
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement; base = el.shadowRoot!.querySelector('[part="base"]')!;
}); });
it('should render a component that passes accessibility test.', async () => { it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible(); await expect(el).to.be.accessible();
}); });
it('uses the value parameter on the base, as aria-valuenow', async () => { it('uses the value parameter on the base, as aria-valuenow', () => {
expect(base).attribute('aria-valuenow', '25'); expect(base).attribute('aria-valuenow', '25');
}); });
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', async () => { it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', () => {
expect(base).attribute('style', '--percentage: 0.25'); expect(base).attribute('style', '--percentage: 0.25');
}); });
}); });
describe('when provided a ariaLabel, and value parameter', async () => { describe('when provided a ariaLabel, and value parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlProgressRing>( el = await fixture<SlProgressRing>(
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>` html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
@ -51,7 +49,7 @@ describe('<sl-progress-ring>', () => {
}); });
}); });
describe('when provided a ariaLabelledBy, and value parameter', async () => { describe('when provided a ariaLabelledBy, and value parameter', () => {
before(async () => { before(async () => {
el = await fixture<SlProgressRing>( el = await fixture<SlProgressRing>(
html` html`

Wyświetl plik

@ -1,7 +1,7 @@
import { LitElement, html } from 'lit'; import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js'; import { customElement, property, query, state } from 'lit/decorators.js';
import { LocalizeController } from '../../utilities/localize';
import styles from './progress-ring.styles'; import styles from './progress-ring.styles';
import { LocalizeController } from '~/utilities/localize';
/** /**
* @since 2.0 * @since 2.0
@ -20,7 +20,7 @@ import styles from './progress-ring.styles';
@customElement('sl-progress-ring') @customElement('sl-progress-ring')
export default class SlProgressRing extends LitElement { export default class SlProgressRing extends LitElement {
static styles = styles; static styles = styles;
private localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
@query('.progress-ring__indicator') indicator: SVGCircleElement; @query('.progress-ring__indicator') indicator: SVGCircleElement;
@ -30,12 +30,12 @@ export default class SlProgressRing extends LitElement {
@property({ type: Number, reflect: true }) value = 0; @property({ type: Number, reflect: true }) value = 0;
/** A custom label for the progress ring's aria label. */ /** A custom label for the progress ring's aria label. */
@property() label: string; @property() label = '';
/** The locale to render the component in. */ /** The locale to render the component in. */
@property() lang: string; @property() lang: string;
updated(changedProps: Map<string, any>) { updated(changedProps: Map<string, unknown>) {
super.updated(changedProps); super.updated(changedProps);
// //
@ -48,7 +48,7 @@ export default class SlProgressRing extends LitElement {
const circumference = 2 * Math.PI * radius; const circumference = 2 * Math.PI * radius;
const offset = circumference - (this.value / 100) * circumference; const offset = circumference - (this.value / 100) * circumference;
this.indicatorOffset = String(offset) + 'px'; this.indicatorOffset = `${offset}px`;
} }
} }
@ -58,7 +58,7 @@ export default class SlProgressRing extends LitElement {
part="base" part="base"
class="progress-ring" class="progress-ring"
role="progressbar" role="progressbar"
aria-label=${this.label || this.localize.term('progress')} aria-label=${this.label.length > 0 ? this.label : this.localize.term('progress')}
aria-valuemin="0" aria-valuemin="0"
aria-valuemax="100" aria-valuemax="100"
aria-valuenow="${this.value}" aria-valuenow="${this.value}"

Some files were not shown because too many files have changed in this diff Show More