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": [
"ms-vscode.vscode-typescript-tslint-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bierner.lit-html",
"bashmish.es6-string-css",

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,11 +1,12 @@
/* global lunr */
(() => {
if (!window.$docsify) {
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
hook.mounted(function () {
hook.mounted(() => {
const appName = document.querySelector('.sidebar .app-name');
const searchBox = document.createElement('div');
searchBox.classList.add('search-box');
@ -76,7 +77,6 @@
`;
document.body.append(siteSearch);
const searchButtons = [...document.querySelectorAll('[data-site-search]')];
const overlay = siteSearch.querySelector('.site-search__overlay');
const panel = siteSearch.querySelector('.site-search__panel');
const input = siteSearch.querySelector('.site-search__input');
@ -89,7 +89,7 @@
let map;
// Load search data
const searchData = fetch('../../../search.json')
fetch('../../../search.json')
.then(res => res.json())
.then(data => {
searchIndex = lunr.Index.load(data.searchIndex);
@ -203,7 +203,7 @@
}
// Update the selected item
items.map(item => {
items.forEach(item => {
if (item === nextEl) {
item.setAttribute('aria-selected', 'true');
nextEl.scrollIntoView({ block: 'nearest' });
@ -211,8 +211,6 @@
item.setAttribute('aria-selected', 'false');
}
});
return;
}
}
@ -228,27 +226,39 @@
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--no-results', hasQuery && !hasResults);
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
results.innerHTML = '';
matches.map((match, index) => {
matches.forEach((match, index) => {
const page = map[match.ref];
const li = document.createElement('li');
const a = document.createElement('a');
let icon = 'file-text';
if (page.url.includes('getting-started/')) icon = 'lightbulb';
if (page.url.includes('resources/')) icon = 'book';
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';
if (page.url.includes('getting-started/')) {
icon = 'lightbulb';
}
if (page.url.includes('resources/')) {
icon = 'book';
}
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 = `
<div class="site-search__result-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.');
}
window.$docsify.plugins.push((hook, vm) => {
hook.mounted(function () {
window.$docsify.plugins.push(hook => {
hook.mounted(() => {
function getTheme() {
return localStorage.getItem('theme') || 'auto';
}
@ -12,9 +12,8 @@
function isDark() {
if (theme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
return theme === 'dark';
}
return theme === 'dark';
}
function setTheme(newTheme) {
@ -62,7 +61,7 @@
menu.addEventListener('sl-select', event => setTheme(event.detail.item.value));
// 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
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.
```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.
@ -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.
```bash
npm start
npm run start
```
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.
To run tests only once, make sure to build the project first.
To run all tests only once:
```bash
npm run build
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
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 {
'*.{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'
};

15384
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -32,17 +32,19 @@
"scripts": {
"start": "node scripts/build.js --bundle --serve",
"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",
"prettier": "prettier --write --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",
"test": "web-test-runner \"src/**/*.test.ts\" --node-resolve --playwright --browsers chromium firefox webkit",
"test:watch": "web-test-runner \"src/**/*.test.ts\" --node-resolve --playwright --browsers chromium firefox webkit --watch",
"spellcheck": "cspell \"**/*.{js,ts,jsx,tsx,json,html,xml,css,scss,sass,md}\" --no-progress",
"test": "node scripts/build.js --bundle && web-test-runner",
"test:watch": "web-test-runner --watch",
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress",
"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": {
"node": ">=14.15.0"
@ -54,13 +56,17 @@
"@shoelace-style/localize": "^2.1.3",
"color": "4.1",
"lit": "^2.1.0",
"lit-html": "^2.1.1",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.5.7",
"@open-wc/testing": "^3.0.3",
"@types/color": "^3.0.2",
"@types/mocha": "^9.0.0",
"@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/test-runner": "^0.13.23",
"@web/test-runner-playwright": "^0.8.8",
@ -73,6 +79,13 @@
"del": "^6.0.0",
"download": "^8.0.0",
"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",
"get-port": "^6.0.0",
"globby": "^12.0.2",
@ -88,6 +101,7 @@
"sinon": "^12.0.1",
"strip-css-comments": "^5.0.0",
"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-css.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) {
console.error(chalk.red(err));
process.exit(1);

Wyświetl plik

@ -1,8 +1,8 @@
//
// 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 commandLineArgs from 'command-line-args';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,8 +1,4 @@
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 }}>', () => {
it('should render a component', async () => {

Wyświetl plik

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

Wyświetl plik

@ -1,33 +1,31 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlAlert from './alert';
describe('<sl-alert>', () => {
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 base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.false;
});
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 base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
expect(base.hidden).to.be.true;
});
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 base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler);
el.show();
void el.show();
await waitUntil(() => showHandler.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 () => {
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 afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler);
el.hide();
void el.hide();
await waitUntil(() => hideHandler.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 () => {
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 afterShowHandler = sinon.spy();
@ -75,7 +73,7 @@ describe('<sl-alert>', () => {
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 base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();

Wyświetl plik

@ -1,14 +1,12 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.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 '../icon-button/icon-button';
import '~/components/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' });
@ -41,7 +39,7 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
export default class SlAlert extends LitElement {
static styles = styles;
private autoHideTimeout: any;
private autoHideTimeout: NodeJS.Timeout;
@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 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() {
this.base.hidden = !this.open;
@ -67,7 +65,7 @@ export default class SlAlert extends LitElement {
/** Shows the alert. */
async show() {
if (this.open) {
return;
return undefined;
}
this.open = true;
@ -77,7 +75,7 @@ export default class SlAlert extends LitElement {
/** Hides the alert */
async hide() {
if (!this.open) {
return;
return undefined;
}
this.open = false;
@ -91,7 +89,7 @@ export default class SlAlert extends LitElement {
*/
async toast() {
return new Promise<void>(resolve => {
if (!toastStack.parentElement) {
if (toastStack.parentElement === null) {
document.body.append(toastStack);
}
@ -99,8 +97,9 @@ export default class SlAlert extends LitElement {
// Wait for the toast stack to render
requestAnimationFrame(() => {
this.clientWidth; // force a reflow for the initial transition
this.show();
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
this.clientWidth;
void this.show();
});
this.addEventListener(
@ -110,7 +109,7 @@ export default class SlAlert extends LitElement {
resolve();
// 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();
}
},
@ -122,12 +121,14 @@ export default class SlAlert extends LitElement {
restartAutoHide() {
clearTimeout(this.autoHideTimeout);
if (this.open && this.duration < Infinity) {
this.autoHideTimeout = setTimeout(() => this.hide(), this.duration);
this.autoHideTimeout = setTimeout(() => {
void this.hide();
}, this.duration);
}
}
handleCloseClick() {
this.hide();
void this.hide();
}
handleMouseMove() {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,10 +1,9 @@
import { LitElement, html } from 'lit';
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 '../icon/icon';
import '~/components/icon/icon';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/**
* @since 2.0
@ -63,7 +62,7 @@ export default class SlAnimatedImage extends LitElement {
}
@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
// won't trigger another request.
if (this.play) {

Wyświetl plik

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

Wyświetl plik

@ -1,9 +1,9 @@
import { LitElement, html } from 'lit';
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 { animations } from './animations';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/**
* @since 2.0
@ -20,7 +20,7 @@ import styles from './animation.styles';
export default class SlAnimation extends LitElement {
static styles = styles;
private animation: Animation;
private animation?: Animation;
private hasStarted = false;
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
@ -56,13 +56,13 @@ export default class SlAnimation extends LitElement {
@property() fill: FillMode = 'auto';
/** 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). */
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
/** 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
@ -73,18 +73,18 @@ export default class SlAnimation extends LitElement {
/** Gets and sets the current animation time. */
get currentTime(): number {
return this.animation?.currentTime || 0;
return this.animation?.currentTime ?? 0;
}
set currentTime(time: number) {
if (this.animation) {
if (typeof this.animation !== 'undefined') {
this.animation.currentTime = time;
}
}
connectedCallback() {
super.connectedCallback();
this.createAnimation();
void this.createAnimation();
this.handleAnimationCancel = this.handleAnimationCancel.bind(this);
this.handleAnimationFinish = this.handleAnimationFinish.bind(this);
}
@ -104,12 +104,12 @@ export default class SlAnimation extends LitElement {
@watch('iterations')
@watch('iterationsStart')
@watch('keyframes')
async handleAnimationChange() {
handleAnimationChange() {
if (!this.hasUpdated) {
return;
}
this.createAnimation();
void this.createAnimation();
}
handleAnimationFinish() {
@ -126,39 +126,43 @@ export default class SlAnimation extends LitElement {
@watch('play')
handlePlayChange() {
if (this.animation) {
if (typeof this.animation !== 'undefined') {
if (this.play && !this.hasStarted) {
this.hasStarted = true;
emit(this, 'sl-start');
}
this.play ? this.animation.play() : this.animation.pause();
if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
return true;
} else {
return false;
}
return false;
}
@watch('playbackRate')
handlePlaybackRateChange() {
if (this.animation) {
if (typeof this.animation !== 'undefined') {
this.animation.playbackRate = this.playbackRate;
}
}
handleSlotChange() {
this.destroyAnimation();
this.createAnimation();
void this.createAnimation();
}
async createAnimation() {
const easing = animations.easings[this.easing] || this.easing;
const keyframes: Keyframe[] = this.keyframes ? this.keyframes : (animations as any)[this.name];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- The specified easing may not exist
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 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;
}
@ -188,7 +192,7 @@ export default class SlAnimation extends LitElement {
}
destroyAnimation() {
if (this.animation) {
if (typeof this.animation !== 'undefined') {
this.animation.cancel();
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
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. */
cancel() {
try {
this.animation.cancel();
} catch {}
this.animation?.cancel();
}
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
finish() {
try {
this.animation.finish();
} catch {}
this.animation?.finish();
}
render() {

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlAvatar from './avatar';
describe('<sl-avatar>', () => {
let el: SlAvatar;
describe('when provided no parameters', async () => {
describe('when provided no parameters', () => {
before(async () => {
el = await fixture<SlAvatar>(html` <sl-avatar label="Avatar"></sl-avatar> `);
});
@ -15,14 +13,14 @@ describe('<sl-avatar>', () => {
await expect(el).to.be.accessible();
});
it('should default to circle styling', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('should default to circle styling', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(el.getAttribute('shape')).to.eq('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 label = 'Small transparent square';
before(async () => {
@ -40,20 +38,20 @@ describe('<sl-avatar>', () => {
await expect(el).to.be.accessible();
});
it('renders "image" part, with src and a role of presentation', async () => {
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
it('renders "image" part, with src and a role of presentation', () => {
const part = el.shadowRoot!.querySelector('[part="image"]')!;
expect(part.getAttribute('src')).to.eq(image);
});
it('renders the label attribute in the "base" part', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('renders the label attribute in the "base" part', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.getAttribute('aria-label')).to.eq(label);
});
});
describe('when provided initials parameter', async () => {
describe('when provided initials parameter', () => {
const initials = 'SL';
before(async () => {
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();
});
it('renders "initials" part, with initials as the text node', async () => {
const part = el.shadowRoot?.querySelector('[part="initials"]') as HTMLImageElement;
it('renders "initials" part, with initials as the text node', () => {
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="initials"]')!;
expect(part.innerText).to.eq(initials);
});
@ -73,15 +71,15 @@ describe('<sl-avatar>', () => {
['square', 'rounded', 'circle'].forEach(shape => {
describe(`when passed a shape attribute ${shape}`, () => {
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 () => {
await expect(el).to.be.accessible();
});
it('appends the appropriate class on the "base" part', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('appends the appropriate class on the "base" part', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(el.getAttribute('shape')).to.eq(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 () => {
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();
});
it('should accept as an assigned child in the shadow root', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=icon]');
const childNodes = slot.assignedNodes({ flatten: true });
it('should accept as an assigned child in the shadow root', () => {
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=icon]')!;
const childNodes = slot.assignedNodes({ flatten: true }) as HTMLElement[];
expect(childNodes.length).to.eq(1);
const span = <HTMLElement>childNodes[0];
const span = childNodes[0];
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 { classMap } from 'lit/directives/class-map.js';
import styles from './avatar.styles';
import '../icon/icon';
import '~/components/icon/icon';
/**
* @since 2.0
@ -27,13 +26,13 @@ export default class SlAvatar extends LitElement {
@state() private hasError = false;
/** 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. */
@property() label: string;
@property() label = '';
/** 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. */
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
@ -51,7 +50,7 @@ export default class SlAvatar extends LitElement {
role="img"
aria-label=${this.label}
>
${this.initials
${typeof this.initials !== 'undefined'
? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
: html`
<div part="icon" class="avatar__icon" aria-hidden="true">
@ -60,7 +59,7 @@ export default class SlAvatar extends LitElement {
</slot>
</div>
`}
${this.image && !this.hasError
${typeof this.image !== 'undefined' && !this.hasError
? html`
<img
part="image"

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBadge from './badge';
describe('<sl-badge>', () => {
let el: SlBadge;
describe('when provided no parameters', async () => {
describe('when provided no parameters', () => {
before(async () => {
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 () => {
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');
});
it('should render the child content provided', async () => {
it('should render the child content provided', () => {
expect(el.innerText).to.eq('Badge');
});
it('should default to square styling, with the primary color', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('should default to square styling, with the primary color', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
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 () => {
el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `);
});
@ -37,13 +35,13 @@ describe('<sl-badge>', () => {
await expect(el).to.be.accessible();
});
it('should append the pill class to the classlist to render a pill', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('should append the pill class to the classlist to render a pill', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
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 () => {
el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `);
});
@ -52,8 +50,8 @@ describe('<sl-badge>', () => {
await expect(el).to.be.accessible();
});
it('should append the pulse class to the classlist to render a pulse', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('should append the pulse class to the classlist to render a pulse', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
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 => {
describe(`when passed a variant attribute ${variant}`, () => {
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 () => {
await expect(el).to.be.accessible();
});
it('should default to square styling, with the primary color', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
it('should default to square styling, with the primary color', () => {
const part = el.shadowRoot!.querySelector('[part="base"]')!;
expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`);
});
});

Wyświetl plik

@ -1,6 +1,6 @@
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`
${componentStyles}

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBreadcrumbItem from './breadcrumb-item';
describe('<sl-breadcrumb-item>', () => {
let el: SlBreadcrumbItem;
describe('when not provided a href attribute', async () => {
describe('when not provided a href attribute', () => {
before(async () => {
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();
});
it('should hide the separator from screen readers', async () => {
const separator: HTMLSpanElement = el.shadowRoot.querySelector('[part="separator"]');
it('should hide the separator from screen readers', () => {
const separator = el.shadowRoot!.querySelector<HTMLSpanElement>('[part="separator"]');
expect(separator).attribute('aria-hidden', 'true');
});
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).attribute('type', 'button');
});
});
describe('when provided a href attribute', async () => {
describe('when provided a href attribute', () => {
describe('and no target', () => {
before(async () => {
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', () => {
const hyperlink: HTMLAnchorElement = el.shadowRoot.querySelector('[part="label"]');
const hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
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"', () => {
let hyperlink: HTMLAnchorElement;
let hyperlink: HTMLAnchorElement | null;
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', () => {
@ -87,10 +85,10 @@ describe('<sl-breadcrumb-item>', () => {
});
describe('should render a HTMLAnchorElement', () => {
let hyperlink: HTMLAnchorElement;
let hyperlink: HTMLAnchorElement | null;
before(() => {
hyperlink = el.shadowRoot.querySelector('a');
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('a');
});
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 () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item>
@ -119,19 +117,19 @@ describe('<sl-breadcrumb-item>', () => {
});
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 });
expect(childNodes.length).to.eq(1);
});
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');
});
});
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 () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item>
@ -146,14 +144,14 @@ describe('<sl-breadcrumb-item>', () => {
});
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 });
expect(childNodes.length).to.eq(1);
});
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');
});
});

Wyświetl plik

@ -2,8 +2,8 @@ import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { HasSlotController } from '../../internal/slot';
import styles from './breadcrumb-item.styles';
import { HasSlotController } from '~/internal/slot';
/**
* @since 2.0
@ -25,22 +25,22 @@ import styles from './breadcrumb-item.styles';
export default class SlBreadcrumbItem extends LitElement {
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
* 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. */
@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. */
@property() rel: string = 'noreferrer noopener';
@property() rel = 'noreferrer noopener';
render() {
const isLink = this.href ? true : false;
const isLink = typeof this.href !== 'undefined';
return html`
<div
@ -62,7 +62,7 @@ export default class SlBreadcrumbItem extends LitElement {
class="breadcrumb-item__label breadcrumb-item__label--link"
href="${this.href}"
target="${this.target}"
rel=${ifDefined(this.target ? this.rel : undefined)}
rel=${ifDefined(typeof this.target !== 'undefined' ? this.rel : undefined)}
>
<slot></slot>
</a>

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBreadcrumb from './breadcrumb';
describe('<sl-breadcrumb>', () => {
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 () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>
@ -22,18 +20,18 @@ describe('<sl-breadcrumb>', () => {
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);
});
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 lastNode = breadcrumbItems[3];
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 () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>
@ -49,20 +47,20 @@ describe('<sl-breadcrumb>', () => {
await expect(el).to.be.accessible();
});
it('should accept "separator" as an assigned child in the shadow root', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=separator]');
it('should accept "separator" as an assigned child in the shadow root', () => {
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=separator]')!;
const childNodes = slot.assignedNodes({ flatten: true });
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('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 () => {
el = await fixture<SlBreadcrumb>(html`
<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 () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -42,11 +42,11 @@ export default class SlButtonGroup extends LitElement {
handleSlotChange() {
const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[];
slottedElements.map(el => {
slottedElements.forEach(el => {
const index = slottedElements.indexOf(el);
const button = findButton(el);
if (button) {
if (button !== null) {
button.classList.add('sl-button-group__button');
button.classList.toggle('sl-button-group__button--first', index === 0);
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() {
// 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`
<div
part="base"

Wyświetl plik

@ -1,6 +1,6 @@
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`
${componentStyles}

Wyświetl plik

@ -1,14 +1,13 @@
import { LitElement } from 'lit';
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 { ifDefined } from 'lit/directives/if-defined.js';
import { emit } from '../../internal/event';
import { FormSubmitController } from '../../internal/form-control';
import { HasSlotController } from '../../internal/slot';
import { html, literal } from 'lit/static-html.js';
import styles from './button.styles';
import '../spinner/spinner';
import '~/components/spinner/spinner';
import { emit } from '~/internal/event';
import { FormSubmitController } from '~/internal/form-control';
import { HasSlotController } from '~/internal/slot';
/**
* @since 2.0
@ -35,8 +34,8 @@ export default class SlButton extends LitElement {
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
private formSubmitController = new FormSubmitController(this);
private hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
private readonly formSubmitController = new FormSubmitController(this);
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
@state() private hasFocus = false;
@ -72,19 +71,19 @@ export default class SlButton extends LitElement {
@property() type: 'button' | 'submit' = 'button';
/** 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. */
@property() value: string;
@property() value?: string;
/** 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. */
@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. */
@property() download: string;
@property() download?: string;
/** Simulates a click on the button. */
click() {
@ -124,9 +123,10 @@ export default class SlButton extends LitElement {
}
render() {
const isLink = this.href ? true : false;
const isLink = typeof this.href !== 'undefined';
const tag = isLink ? literal`a` : literal`button`;
/* eslint-disable lit/binding-positions, lit/no-invalid-html */
return html`
<${tag}
part="base"
@ -161,7 +161,7 @@ export default class SlButton extends LitElement {
href=${ifDefined(this.href)}
target=${ifDefined(this.target)}
download=${ifDefined(this.download)}
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
role="button"
aria-disabled=${this.disabled ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}
@ -199,6 +199,7 @@ export default class SlButton extends LitElement {
${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
</${tag}>
`;
/* eslint-enable lit/binding-positions, lit/no-invalid-html */
}
}

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlCard from './card';
describe('<sl-card>', () => {
let el: SlCard;
describe('when provided no parameters', async () => {
describe('when provided no parameters', () => {
before(async () => {
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> `
@ -17,17 +15,17 @@ describe('<sl-card>', () => {
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.');
});
it('should contain the class card.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
it('should contain the class card.', () => {
const card = el.shadowRoot!.querySelector('.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 () => {
el = await fixture<SlCard>(
html`<sl-card>
@ -41,29 +39,29 @@ describe('<sl-card>', () => {
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!');
});
it('render the header content provided.', async () => {
const header = <HTMLDivElement>el.querySelector('div[slot=header]');
it('render the header content provided.', () => {
const header = el.querySelector<HTMLElement>('div[slot=header]')!;
expect(header.innerText).eq('Header Title');
});
it('accept "header" as an assigned child in the shadow root.', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=header]');
it('accept "header" as an assigned child in the shadow root.', () => {
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=header]')!;
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should contain the class card--has-header.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
it('should contain the class card--has-header.', () => {
const card = el.shadowRoot!.querySelector('.card')!;
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 () => {
el = await fixture<SlCard>(
html`<sl-card>
@ -78,35 +76,35 @@ describe('<sl-card>', () => {
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!');
});
it('render the footer content provided.', async () => {
const footer = <HTMLDivElement>el.querySelector('div[slot=footer]');
it('render the footer content provided.', () => {
const footer = el.querySelector<HTMLElement>('div[slot=footer]')!;
expect(footer.innerText).eq('Footer Content');
});
it('accept "footer" as an assigned child in the shadow root.', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=footer]');
it('accept "footer" as an assigned child in the shadow root.', () => {
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=footer]')!;
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should contain the class card--has-footer.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
it('should contain the class card--has-footer.', () => {
const card = el.shadowRoot!.querySelector('.card')!;
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 () => {
el = await fixture<SlCard>(
html`<sl-card>
<img
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."
/>
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();
});
it('should render the child content provided.', async () => {
it('should render the child content provided.', () => {
expect(el.innerText).to.contain(
'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 () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=image]');
it('accept "image" as an assigned child in the shadow root.', () => {
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=image]')!;
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should contain the class card--has-image.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
it('should contain the class card--has-image.', () => {
const card = el.shadowRoot!.querySelector('.card')!;
expect(card.classList.value.trim()).to.eq('card card--has-image');
});
});

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
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`
${componentStyles}

Wyświetl plik

@ -1,7 +1,5 @@
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import '../../../dist/shoelace.js';
import type SlCheckbox from './checkbox';
describe('<sl-checkbox>', () => {
@ -9,7 +7,7 @@ describe('<sl-checkbox>', () => {
const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `);
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 () => {
@ -20,7 +18,9 @@ describe('<sl-checkbox>', () => {
it('should fire sl-change when clicked', async () => {
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');
expect(event.target).to.equal(el);
expect(el.checked).to.be.true;
@ -28,9 +28,11 @@ describe('<sl-checkbox>', () => {
it('should fire sl-change when toggled via keyboard', async () => {
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
const input = el.shadowRoot?.querySelector('input');
const input = el.shadowRoot!.querySelector('input')!;
input.focus();
setTimeout(() => sendKeys({ press: ' ' }));
setTimeout(() => {
void sendKeys({ press: ' ' });
});
const event = await oneEvent(el, 'sl-change');
expect(event.target).to.equal(el);
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 { ifDefined } from 'lit/directives/if-defined.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';
let id = 0;
import { autoIncrement } from '~/internal/autoIncrement';
import { emit } from '~/internal/event';
import { FormSubmitController } from '~/internal/form-control';
import { watch } from '~/internal/watch';
/**
* @since 2.0
@ -32,12 +31,13 @@ export default class SlCheckbox extends LitElement {
@query('input[type="checkbox"]') input: HTMLInputElement;
// @ts-ignore
private formSubmitController = new FormSubmitController(this, {
// @ts-expect-error -- Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this, {
value: (control: SlCheckbox) => (control.checked ? control.value : undefined)
});
private inputId = `checkbox-${++id}`;
private labelId = `checkbox-label-${id}`;
private readonly attrId = autoIncrement();
private readonly inputId = `checkbox-${this.attrId}`;
private readonly labelId = `checkbox-label-${this.attrId}`;
@state() private hasFocus = false;
@ -103,13 +103,11 @@ export default class SlCheckbox extends LitElement {
emit(this, 'sl-blur');
}
@watch('disabled')
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// 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.invalid = !this.input.checkValidity();
}
this.input.disabled = this.disabled;
this.invalid = !this.input.checkValidity();
}
handleFocus() {
@ -146,7 +144,6 @@ export default class SlCheckbox extends LitElement {
.checked=${live(this.checked)}
.disabled=${this.disabled}
.required=${this.required}
role="checkbox"
aria-checked=${this.checked ? 'true' : 'false'}
aria-labelledby=${this.labelId}
@click=${this.handleClick}

Wyświetl plik

@ -1,6 +1,6 @@
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`
${componentStyles}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
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`
${componentStyles}

Wyświetl plik

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

Wyświetl plik

@ -1,14 +1,12 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.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 '../icon/icon';
import '~/components/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
@ -58,7 +56,7 @@ export default class SlDetails extends LitElement {
/** Shows the details. */
async show() {
if (this.open) {
return;
return undefined;
}
this.open = true;
@ -68,16 +66,24 @@ export default class SlDetails extends LitElement {
/** Hides the details */
async hide() {
if (!this.open) {
return;
return undefined;
}
this.open = false;
return waitForEvent(this, 'sl-after-hide');
}
toggleOpen() {
if (this.open) {
void this.hide();
} else {
void this.show();
}
}
handleSummaryClick() {
if (!this.disabled) {
this.open ? this.hide() : this.show();
this.toggleOpen();
this.header.focus();
}
}
@ -85,17 +91,17 @@ export default class SlDetails extends LitElement {
handleSummaryKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.open ? this.hide() : this.show();
this.toggleOpen();
}
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
event.preventDefault();
this.hide();
void this.hide();
}
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.preventDefault();
this.show();
void this.show();
}
}

Wyświetl plik

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

Wyświetl plik

@ -1,8 +1,6 @@
// cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDialog from './dialog';
describe('<sl-dialog>', () => {
@ -10,7 +8,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html`
<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;
});
@ -19,7 +17,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(
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;
});
@ -28,13 +26,13 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(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"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler);
el.show();
void el.show();
await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce);
@ -48,13 +46,13 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html`
<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 afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler);
el.hide();
void el.hide();
await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce);
@ -68,7 +66,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(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"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
@ -88,7 +86,7 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html`
<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 afterHideHandler = sinon.spy();
@ -108,9 +106,11 @@ describe('<sl-dialog>', () => {
const el = await fixture<SlDialog>(html`
<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();
expect(el.open).to.be.true;
@ -118,14 +118,14 @@ describe('<sl-dialog>', () => {
it('should allow initial focus to be set', async () => {
const el = await fixture<SlDialog>(html` <sl-dialog><input /></sl-dialog> `);
const input = el.querySelector('input');
const initialFocusHandler = sinon.spy(event => {
const input = el.querySelector('input')!;
const initialFocusHandler = sinon.spy((event: Event) => {
event.preventDefault();
input.focus();
});
el.addEventListener('sl-initial-focus', initialFocusHandler);
el.show();
void el.show();
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 { classMap } from 'lit/directives/class-map.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 '../icon-button/icon-button';
import '~/components/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();
@ -65,7 +63,7 @@ export default class SlDialog extends LitElement {
@query('.dialog__panel') panel: HTMLElement;
@query('.dialog__overlay') overlay: HTMLElement;
private hasSlotController = new HasSlotController(this, 'footer');
private readonly hasSlotController = new HasSlotController(this, 'footer');
private modal: Modal;
private originalTrigger: HTMLElement | null;
@ -106,7 +104,7 @@ export default class SlDialog extends LitElement {
/** Shows the dialog. */
async show() {
if (this.open) {
return;
return undefined;
}
this.open = true;
@ -116,7 +114,7 @@ export default class SlDialog extends LitElement {
/** Hides the dialog */
async hide() {
if (!this.open) {
return;
return undefined;
}
this.open = false;
@ -127,11 +125,11 @@ export default class SlDialog extends LitElement {
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
if (slRequestClose.defaultPrevented) {
const animation = getAnimation(this, 'dialog.denyClose');
animateTo(this.panel, animation.keyframes, animation.options);
void animateTo(this.panel, animation.keyframes, animation.options);
return;
}
this.hide();
void this.hide();
}
handleKeyDown(event: KeyboardEvent) {
@ -197,8 +195,10 @@ export default class SlDialog extends LitElement {
// Restore focus to the original trigger
const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') {
setTimeout(() => trigger.focus());
if (typeof trigger?.focus === 'function') {
setTimeout(() => {
trigger.focus();
});
}
emit(this, 'sl-after-hide');
@ -216,7 +216,13 @@ export default class SlDialog extends LitElement {
})}
@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
part="panel"
@ -232,7 +238,7 @@ export default class SlDialog extends LitElement {
? html`
<header part="header" class="dialog__header">
<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>
<sl-icon-button
exportparts="base:close-button"

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,8 +1,6 @@
// cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDrawer from './drawer';
describe('<sl-drawer>', () => {
@ -10,7 +8,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html`
<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;
});
@ -19,7 +17,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(
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;
});
@ -28,13 +26,13 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(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"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler);
el.show();
void el.show();
await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce);
@ -48,13 +46,13 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html`
<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 afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler);
el.hide();
void el.hide();
await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce);
@ -68,7 +66,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(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"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
@ -88,7 +86,7 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html`
<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 afterHideHandler = sinon.spy();
@ -108,9 +106,11 @@ describe('<sl-drawer>', () => {
const el = await fixture<SlDrawer>(html`
<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();
expect(el.open).to.be.true;
@ -118,14 +118,14 @@ describe('<sl-drawer>', () => {
it('should allow initial focus to be set', async () => {
const el = await fixture<SlDrawer>(html` <sl-drawer><input /></sl-drawer> `);
const input = el.querySelector('input');
const initialFocusHandler = sinon.spy(event => {
const input = el.querySelector<HTMLInputElement>('input')!;
const initialFocusHandler = sinon.spy((event: InputEvent) => {
event.preventDefault();
input.focus();
});
el.addEventListener('sl-initial-focus', initialFocusHandler);
el.show();
void el.show();
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 { classMap } from 'lit/directives/class-map.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 '../icon-button/icon-button';
import '~/components/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();
@ -73,7 +71,7 @@ export default class SlDrawer extends LitElement {
@query('.drawer__panel') panel: HTMLElement;
@query('.drawer__overlay') overlay: HTMLElement;
private hasSlotController = new HasSlotController(this, 'footer');
private readonly hasSlotController = new HasSlotController(this, 'footer');
private modal: Modal;
private originalTrigger: HTMLElement | null;
@ -123,7 +121,7 @@ export default class SlDrawer extends LitElement {
/** Shows the drawer. */
async show() {
if (this.open) {
return;
return undefined;
}
this.open = true;
@ -133,7 +131,7 @@ export default class SlDrawer extends LitElement {
/** Hides the drawer */
async hide() {
if (!this.open) {
return;
return undefined;
}
this.open = false;
@ -144,11 +142,11 @@ export default class SlDrawer extends LitElement {
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
if (slRequestClose.defaultPrevented) {
const animation = getAnimation(this, 'drawer.denyClose');
animateTo(this.panel, animation.keyframes, animation.options);
void animateTo(this.panel, animation.keyframes, animation.options);
return;
}
this.hide();
void this.hide();
}
handleKeyDown(event: KeyboardEvent) {
@ -217,8 +215,10 @@ export default class SlDrawer extends LitElement {
// Restore focus to the original trigger
const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') {
setTimeout(() => trigger.focus());
if (typeof trigger?.focus === 'function') {
setTimeout(() => {
trigger.focus();
});
}
emit(this, 'sl-after-hide');
@ -242,7 +242,13 @@ export default class SlDrawer extends LitElement {
})}
@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
part="panel"
@ -259,7 +265,7 @@ export default class SlDrawer extends LitElement {
<header part="header" class="drawer__header">
<span part="title" class="drawer__title" id="title">
<!-- 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>
<sl-icon-button
exportparts="base:close-button"

Wyświetl plik

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

Wyświetl plik

@ -1,7 +1,5 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlDropdown from './dropdown';
describe('<sl-dropdown>', () => {
@ -16,7 +14,7 @@ describe('<sl-dropdown>', () => {
</sl-menu>
</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;
});
@ -32,7 +30,7 @@ describe('<sl-dropdown>', () => {
</sl-menu>
</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;
});
@ -48,13 +46,13 @@ describe('<sl-dropdown>', () => {
</sl-menu>
</sl-dropdown>
`);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
el.addEventListener('sl-show', showHandler);
el.addEventListener('sl-after-show', afterShowHandler);
el.show();
void el.show();
await waitUntil(() => showHandler.calledOnce);
await waitUntil(() => afterShowHandler.calledOnce);
@ -75,13 +73,13 @@ describe('<sl-dropdown>', () => {
</sl-menu>
</sl-dropdown>
`);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const hideHandler = sinon.spy();
const afterHideHandler = sinon.spy();
el.addEventListener('sl-hide', hideHandler);
el.addEventListener('sl-after-hide', afterHideHandler);
el.hide();
void el.hide();
await waitUntil(() => hideHandler.calledOnce);
await waitUntil(() => afterHideHandler.calledOnce);
@ -102,7 +100,7 @@ describe('<sl-dropdown>', () => {
</sl-menu>
</sl-dropdown>
`);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const showHandler = sinon.spy();
const afterShowHandler = sinon.spy();
@ -129,7 +127,7 @@ describe('<sl-dropdown>', () => {
</sl-menu>
</sl-dropdown>
`);
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
const hideHandler = 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 { customElement, property, query } from 'lit/decorators.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 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
@ -40,7 +40,7 @@ export default class SlDropdown extends LitElement {
@query('.dropdown__panel') panel: 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. */
@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;
/** 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. */
@property({ type: Number }) distance = 0;
@ -94,12 +94,12 @@ export default class SlDropdown extends LitElement {
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
if (!this.containingElement) {
if (typeof this.containingElement === 'undefined') {
this.containingElement = this;
}
// Create the popover after render
this.updateComplete.then(() => {
void this.updateComplete.then(() => {
this.popover = createPopper(this.trigger, this.positioner, {
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
@ -127,30 +127,30 @@ export default class SlDropdown extends LitElement {
disconnectedCallback() {
super.disconnectedCallback();
this.hide();
void void this.hide();
if (this.popover) {
this.popover.destroy();
}
this.popover?.destroy();
}
focusOnTrigger() {
const slot = this.trigger.querySelector('slot')!;
const trigger = slot.assignedElements({ flatten: true })[0] as any;
if (trigger && typeof trigger.focus === 'function') {
const trigger = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
if (typeof trigger?.focus === 'function') {
trigger.focus();
}
}
getMenu() {
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) {
// Close when escape is pressed
if (event.key === 'Escape') {
this.hide();
void this.hide();
this.focusOnTrigger();
return;
}
@ -160,7 +160,7 @@ export default class SlDropdown extends LitElement {
// Tabbing within an open menu should close the dropdown and refocus the trigger
if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') {
event.preventDefault();
this.hide();
void this.hide();
this.focusOnTrigger();
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.
setTimeout(() => {
const activeElement =
this.containingElement.getRootNode() instanceof ShadowRoot
this.containingElement?.getRootNode() instanceof ShadowRoot
? document.activeElement?.shadowRoot?.activeElement
: document.activeElement;
if (activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement) {
this.hide();
return;
if (
typeof this.containingElement === 'undefined' ||
activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
) {
void this.hide();
}
});
}
@ -185,10 +187,9 @@ export default class SlDropdown extends LitElement {
handleDocumentMouseDown(event: MouseEvent) {
// Close when clicking outside of the containing element
const path = event.composedPath() as Array<EventTarget>;
if (!path.includes(this.containingElement)) {
this.hide();
return;
const path = event.composedPath();
if (typeof this.containingElement !== 'undefined' && !path.includes(this.containingElement)) {
void this.hide();
}
}
@ -202,7 +203,7 @@ export default class SlDropdown extends LitElement {
// Hide the dropdown when a menu item is selected
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') {
this.hide();
void this.hide();
this.focusOnTrigger();
}
}
@ -212,42 +213,44 @@ export default class SlDropdown extends LitElement {
@watch('placement')
@watch('skidding')
handlePopoverOptionsChange() {
if (this.popover) {
this.popover.setOptions({
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
void this.popover?.setOptions({
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
]
});
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
}
handleTriggerClick() {
this.open ? this.hide() : this.show();
if (this.open) {
void this.hide();
} else {
void this.show();
}
}
handleTriggerKeyDown(event: KeyboardEvent) {
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 lastMenuItem = menuItems[menuItems.length - 1];
// Close when escape or tab is pressed
if (event.key === 'Escape') {
this.focusOnTrigger();
this.hide();
void this.hide();
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.
if ([' ', 'Enter'].includes(event.key)) {
event.preventDefault();
this.open ? this.hide() : this.show();
this.handleTriggerClick();
return;
}
@ -267,19 +270,18 @@ export default class SlDropdown extends LitElement {
// Show the menu if it's not already open
if (!this.open) {
this.show();
void this.show();
}
// Focus on a menu item
if (event.key === 'ArrowDown' && firstMenuItem) {
const menu = this.getMenu();
menu.setCurrentItem(firstMenuItem);
if (event.key === 'ArrowDown' && typeof firstMenuItem !== 'undefined') {
menu!.setCurrentItem(firstMenuItem);
firstMenuItem.focus();
return;
}
if (event.key === 'ArrowUp' && lastMenuItem) {
menu.setCurrentItem(lastMenuItem);
if (event.key === 'ArrowUp' && typeof lastMenuItem !== 'undefined') {
menu!.setCurrentItem(lastMenuItem);
lastMenuItem.focus();
return;
}
@ -287,9 +289,8 @@ export default class SlDropdown extends LitElement {
// Other keys bring focus to the menu and initiate type-to-select behavior
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
if (this.open && menu && !ignoredKeys.includes(event.key)) {
menu.typeToSelect(event.key);
return;
if (this.open && !ignoredKeys.includes(event.key)) {
menu?.typeToSelect(event.key);
}
}
@ -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."
//
updateAccessibleTrigger() {
if (this.trigger) {
const slot = this.trigger.querySelector('slot') as HTMLSlotElement;
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
const slot = this.trigger.querySelector('slot')!;
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
if (accessibleTrigger) {
accessibleTrigger.setAttribute('aria-haspopup', 'true');
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
}
if (typeof accessibleTrigger !== 'undefined') {
accessibleTrigger.setAttribute('aria-haspopup', 'true');
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
}
}
/** Shows the dropdown panel. */
async show() {
if (this.open) {
return;
return undefined;
}
this.open = true;
@ -340,7 +339,7 @@ export default class SlDropdown extends LitElement {
/** Hides the dropdown panel */
async hide() {
if (!this.open) {
return;
return undefined;
}
this.open = false;
@ -356,7 +355,7 @@ export default class SlDropdown extends LitElement {
return;
}
this.popover.update();
void this.popover?.update();
}
@watch('open', { waitUntilFirstUpdate: true })
@ -376,7 +375,7 @@ export default class SlDropdown extends LitElement {
document.addEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this);
this.popover.update();
void this.popover?.update();
this.panel.hidden = false;
const { keyframes, options } = getAnimation(this, 'dropdown.show');
await animateTo(this.panel, keyframes, options);

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
import { LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { LocalizeController } from '../../utilities/localize';
import { LocalizeController } from '~/utilities/localize';
/**
* @since 2.0
@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
*/
@customElement('sl-format-date')
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. */
@property() date: Date | string = new Date();
@ -55,7 +55,7 @@ export default class SlFormatDate extends LitElement {
// Check for an invalid date
if (isNaN(date.getMilliseconds())) {
return;
return undefined;
}
return this.localize.date(date, {

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
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`
${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 { ifDefined } from 'lit/directives/if-defined.js';
import styles from './icon-button.styles';
import '../icon/icon';
import '~/components/icon/icon';
/**
* @since 2.0
@ -21,22 +20,22 @@ export default class SlIconButton extends LitElement {
@query('button') button: HTMLButtonElement | HTMLLinkElement;
/** The name of the icon to draw. */
@property() name: string;
@property() name?: string;
/** The name of a registered custom icon library. */
@property() library: string;
@property() library?: string;
/** 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>`. */
@property() href: string;
@property() href?: string;
/** 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. */
@property() download: string;
@property() download?: string;
/**
* 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;
render() {
const isLink = this.href ? true : false;
const isLink = typeof this.href !== 'undefined';
const interior = html`
<sl-icon
@ -67,7 +66,7 @@ export default class SlIconButton extends LitElement {
href=${ifDefined(this.href)}
target=${ifDefined(this.target)}
download=${ifDefined(this.download)}
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
role="button"
aria-disabled=${this.disabled ? 'true' : 'false'}
aria-label="${this.label}"

Wyświetl plik

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

Wyświetl plik

@ -2,11 +2,11 @@ import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import styles from './icon.styles';
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
import { requestIcon } from './request';
import styles from './icon.styles';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
const parser = new DOMParser();
@ -26,13 +26,17 @@ export default class SlIcon extends LitElement {
@state() private svg = '';
/** 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. */
@property() label: string;
@property() label = '';
/** The name of a registered custom icon library. */
@property() library = 'default';
@ -43,7 +47,7 @@ export default class SlIcon extends LitElement {
}
firstUpdated() {
this.setIcon();
void this.setIcon();
}
disconnectedCallback() {
@ -51,18 +55,17 @@ export default class SlIcon extends LitElement {
unwatchIcon(this);
}
private getUrl(): string {
private getUrl() {
const library = getIconLibrary(this.library);
if (this.name && library) {
if (typeof this.name !== 'undefined' && typeof library !== 'undefined') {
return library.resolver(this.name);
} else {
return this.src;
}
return this.src;
}
/** @internal Fetches the icon and redraws it. Used to handle library registrations. */
redraw() {
this.setIcon();
void this.setIcon();
}
@watch('name')
@ -71,7 +74,7 @@ export default class SlIcon extends LitElement {
async setIcon() {
const library = getIconLibrary(this.library);
const url = this.getUrl();
if (url) {
if (typeof url !== 'undefined' && url.length > 0) {
try {
const file = await requestIcon(url)!;
if (url !== this.getUrl()) {
@ -81,10 +84,8 @@ export default class SlIcon extends LitElement {
const doc = parser.parseFromString(file.svg, 'text/html');
const svgEl = doc.body.querySelector('svg');
if (svgEl) {
if (library && library.mutator) {
library.mutator(svgEl);
}
if (svgEl !== null) {
library?.mutator?.(svgEl);
this.svg = svgEl.outerHTML;
emit(this, 'sl-load');
@ -99,14 +100,14 @@ export default class SlIcon extends LitElement {
} catch {
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
this.svg = '';
}
}
handleChange() {
this.setIcon();
void this.setIcon();
}
render() {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,6 @@
import defaultLibrary from './library.default';
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 IconLibraryMutator = (svg: SVGElement) => void;
@ -22,7 +22,7 @@ export function unwatchIcon(icon: SlIcon) {
}
export function getIconLibrary(name?: string) {
return registry.filter(lib => lib.name === name)[0];
return registry.find(lib => lib.name === name);
}
export function registerIconLibrary(
@ -37,7 +37,7 @@ export function registerIconLibrary(
});
// Redraw watched icons
watchedIcons.map(icon => {
watchedIcons.forEach(icon => {
if (icon.library === name) {
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;
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)) {
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;
return iconFiles.get(url)!;
}
};
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 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`
${componentStyles}

Wyświetl plik

@ -1,12 +1,13 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.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 '../icon/icon';
import '~/components/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
@ -36,42 +37,19 @@ export default class SlImageComparer extends LitElement {
@query('.image-comparer') base: 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. */
@property({ type: Number, reflect: true }) position = 50;
handleDrag(event: any) {
handleDrag(event: Event) {
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();
drag(event, this.base, x => {
this.position = Number(clamp((x / width) * 100, 0, 100).toFixed(2));
drag(this.base, x => {
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
});
}
@ -82,10 +60,18 @@ export default class SlImageComparer extends LitElement {
event.preventDefault();
if (event.key === 'ArrowLeft') newPosition = newPosition - incr;
if (event.key === 'ArrowRight') newPosition = newPosition + incr;
if (event.key === 'Home') newPosition = 0;
if (event.key === 'End') newPosition = 100;
if (event.key === 'ArrowLeft') {
newPosition -= incr;
}
if (event.key === 'ArrowRight') {
newPosition += incr;
}
if (event.key === 'Home') {
newPosition = 0;
}
if (event.key === 'End') {
newPosition = 100;
}
newPosition = clamp(newPosition, 0, 100);
this.position = newPosition;
@ -99,7 +85,7 @@ export default class SlImageComparer extends LitElement {
render() {
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 part="before" class="image-comparer__before">
<slot name="before"></slot>
@ -117,7 +103,7 @@ export default class SlImageComparer extends LitElement {
<div
part="divider"
class="image-comparer__divider"
style=${styleMap({ left: this.position + '%' })}
style=${styleMap({ left: `${this.position}%` })}
@mousedown=${this.handleDrag}
@touchstart=${this.handleDrag}
>
@ -128,6 +114,7 @@ export default class SlImageComparer extends LitElement {
aria-valuenow=${this.position}
aria-valuemin="0"
aria-valuemax="100"
aria-controls=${this.containerId}
tabindex="0"
>
<slot name="handle-icon">

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,9 +1,9 @@
import { LitElement, html } from 'lit';
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 { requestInclude } from './request';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
/**
* @since 2.0
@ -16,7 +16,11 @@ import styles from './include.styles';
export default class SlInclude extends LitElement {
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;
/** The fetch mode to use. */
@ -31,7 +35,9 @@ export default class SlInclude extends LitElement {
executeScript(script: HTMLScriptElement) {
// Create a copy of the script and swap it out so the browser executes it
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;
script.parentNode!.replaceChild(newScript, script);
}
@ -47,7 +53,7 @@ export default class SlInclude extends LitElement {
return;
}
if (!file) {
if (typeof file === 'undefined') {
return;
}
@ -59,7 +65,9 @@ export default class SlInclude extends LitElement {
this.innerHTML = file.html;
if (this.allowScripts) {
[...this.querySelectorAll('script')].map(script => this.executeScript(script));
[...this.querySelectorAll('script')].forEach(script => {
this.executeScript(script);
});
}
emit(this, 'sl-load');

Wyświetl plik

@ -6,18 +6,17 @@ interface 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)) {
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;
return includeFiles.get(src)!;
}
};
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 componentStyles from '../../styles/component.styles';
import formControlStyles from '../../styles/form-control.styles';
import componentStyles from '~/styles/component.styles';
import formControlStyles from '~/styles/form-control.styles';
export default css`
${componentStyles}

Wyświetl plik

@ -1,13 +1,10 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
import type SlInput from './input';
describe('<sl-input>', () => {
it('should be disabled with the disabled attribute', async () => {
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;
});

Wyświetl plik

@ -1,17 +1,15 @@
import { LitElement, html } from 'lit';
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 { ifDefined } from 'lit/directives/if-defined.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 '../icon/icon';
let id = 0;
import '~/components/icon/icon';
import { autoIncrement } from '~/internal/autoIncrement';
import { emit } from '~/internal/event';
import { FormSubmitController, getLabelledBy, renderFormControl } from '~/internal/form-control';
import { HasSlotController } from '~/internal/slot';
import { watch } from '~/internal/watch';
/**
* @since 2.0
@ -49,12 +47,13 @@ export default class SlInput extends LitElement {
@query('.input__control') input: HTMLInputElement;
// @ts-ignore
private formSubmitController = new FormSubmitController(this);
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
private inputId = `input-${++id}`;
private helpTextId = `input-help-text-${id}`;
private labelId = `input-label-${id}`;
// @ts-expect-error -- Controller is currently unused
private readonly formSubmitController = new FormSubmitController(this);
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
private readonly attrId = autoIncrement();
private readonly inputId = `input-${this.attrId}`;
private readonly helpTextId = `input-help-text-${this.attrId}`;
private readonly labelId = `input-label-${this.attrId}`;
@state() private hasFocus = false;
@state() private isPasswordVisible = false;
@ -79,7 +78,7 @@ export default class SlInput extends LitElement {
@property({ type: Boolean, reflect: true }) pill = false;
/** 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. */
@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`. */
get valueAsDate() {
return this.input.valueAsDate as Date;
return this.input.valueAsDate!;
}
set valueAsDate(newValue: Date) {
@ -156,7 +155,7 @@ export default class SlInput extends LitElement {
/** Gets or sets the current value as a number. */
get valueAsNumber() {
return this.input.valueAsNumber as number;
return this.input.valueAsNumber;
}
set valueAsNumber(newValue: number) {
@ -180,7 +179,7 @@ export default class SlInput extends LitElement {
/** Selects all the text in the input. */
select() {
return this.input.select();
this.input.select();
}
/** Sets the start and end positions of the text selection (0-based). */
@ -189,7 +188,7 @@ export default class SlInput extends LitElement {
selectionEnd: number,
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. */
@ -239,13 +238,11 @@ export default class SlInput extends LitElement {
event.stopPropagation();
}
@watch('disabled')
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// 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.invalid = !this.input.checkValidity();
}
this.input.disabled = this.disabled;
this.invalid = !this.input.checkValidity();
}
handleFocus() {
@ -266,11 +263,9 @@ export default class SlInput extends LitElement {
this.isPasswordVisible = !this.isPasswordVisible;
}
@watch('value')
@watch('value', { waitUntilFirstUpdate: true })
handleValueChange() {
if (this.input) {
this.invalid = !this.input.checkValidity();
}
this.invalid = !this.input.checkValidity();
}
render() {
@ -306,7 +301,7 @@ export default class SlInput extends LitElement {
'input--filled': this.filled,
'input--disabled': this.disabled,
'input--focused': this.hasFocus,
'input--empty': this.value?.length === 0,
'input--empty': this.value.length === 0,
'input--invalid': this.invalid
})}
>
@ -355,7 +350,7 @@ export default class SlInput extends LitElement {
@blur=${this.handleBlur}
/>
${this.clearable && this.value?.length > 0
${this.clearable && this.value.length > 0
? html`
<button
part="clear-button"

Wyświetl plik

@ -1,6 +1,6 @@
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`
${componentStyles}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,10 +1,14 @@
import { LitElement, html } from 'lit';
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 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
@ -24,7 +28,7 @@ export default class SlMenu extends LitElement {
@query('slot') defaultSlot: HTMLSlotElement;
private typeToSelectString = '';
private typeToSelectTimeout: any;
private typeToSelectTimeout: NodeJS.Timeout;
firstUpdated() {
this.setAttribute('role', 'menu');
@ -36,7 +40,7 @@ export default class SlMenu extends LitElement {
return false;
}
if (!options?.includeDisabled && (el as SlMenuItem).disabled) {
if (!options.includeDisabled && (el as SlMenuItem).disabled) {
return false;
}
@ -58,10 +62,12 @@ export default class SlMenu extends LitElement {
*/
setCurrentItem(item: SlMenuItem) {
const items = this.getAllItems({ includeDisabled: false });
let activeItem = item.disabled ? items[0] : item;
const activeItem = item.disabled ? items[0] : item;
// 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
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) {
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();
if (label.substring(0, this.typeToSelectString.length) === this.typeToSelectString) {
if (label.startsWith(this.typeToSelectString)) {
this.setCurrentItem(item);
// 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) {
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 } });
}
}
@ -107,7 +115,9 @@ export default class SlMenu extends LitElement {
// Restore focus in browsers that don't support :focus-visible when using the keyboard
if (!hasFocusVisible) {
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();
event.preventDefault();
if (item) {
// Simulate a click to support @click handlers on menu items that also work with the keyboard
item.click();
}
// Simulate a click to support @click handlers on menu items that also work with the keyboard
item?.click();
}
// Prevent scrolling when space is pressed
@ -132,9 +140,9 @@ export default class SlMenu extends LitElement {
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
const items = this.getAllItems({ includeDisabled: false });
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();
if (event.key === 'ArrowDown') {
@ -147,8 +155,12 @@ export default class SlMenu extends LitElement {
index = items.length - 1;
}
if (index < 0) index = 0;
if (index > items.length - 1) index = items.length - 1;
if (index < 0) {
index = 0;
}
if (index > items.length - 1) {
index = items.length - 1;
}
this.setCurrentItem(items[index]);
items[index].focus();
@ -177,7 +189,7 @@ export default class SlMenu extends LitElement {
const items = this.getAllItems({ includeDisabled: false });
// Reset the roving tab index when the slotted items change
if (items.length) {
if (items.length > 0) {
this.setCurrentItem(items[0]);
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlProgressBar from './progress-bar';
describe('<sl-progress-bar>', () => {
let el: SlProgressBar;
describe('when provided just a value parameter', async () => {
describe('when provided just a value parameter', () => {
before(async () => {
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 indicator: HTMLDivElement;
@ -24,43 +22,43 @@ describe('<sl-progress-bar>', () => {
el = await fixture<SlProgressBar>(
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
);
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement;
base = el.shadowRoot!.querySelector('[part="base"]')!;
indicator = el.shadowRoot!.querySelector('[part="indicator"]')!;
});
it('should render a component that passes accessibility test.', async () => {
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');
});
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%;');
});
});
describe('when provided an indeterminate parameter', async () => {
describe('when provided an indeterminate parameter', () => {
let base: HTMLDivElement;
before(async () => {
el = await fixture<SlProgressBar>(
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 () => {
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');
});
});
describe('when provided a ariaLabel, and value parameter', async () => {
describe('when provided a ariaLabel, and value parameter', () => {
before(async () => {
el = await fixture<SlProgressBar>(
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 () => {
el = await fixture<SlProgressBar>(
html`

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,12 +1,10 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlProgressRing from './progress-ring';
describe('<sl-progress-ring>', () => {
let el: SlProgressRing;
describe('when provided just a value parameter', async () => {
describe('when provided just a value parameter', () => {
before(async () => {
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;
before(async () => {
el = await fixture<SlProgressRing>(
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 () => {
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');
});
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');
});
});
describe('when provided a ariaLabel, and value parameter', async () => {
describe('when provided a ariaLabel, and value parameter', () => {
before(async () => {
el = await fixture<SlProgressRing>(
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 () => {
el = await fixture<SlProgressRing>(
html`

Wyświetl plik

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

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