kopia lustrzana https://github.com/shoelace-style/shoelace
feat: add ESLint, improve types, improve a11y
rodzic
2ad00deb38
commit
9fb3b5cfed
|
@ -0,0 +1,9 @@
|
|||
.cache
|
||||
docs/dist
|
||||
docs/search.json
|
||||
docs/**/*.min.js
|
||||
dist
|
||||
examples
|
||||
node_modules
|
||||
src/react
|
||||
scripts
|
|
@ -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'
|
||||
}
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html",
|
||||
"bashmish.es6-string-css",
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"csspart",
|
||||
"cssproperty",
|
||||
"datetime",
|
||||
"describedby",
|
||||
"Docsify",
|
||||
"dropdowns",
|
||||
"easings",
|
||||
|
@ -79,6 +80,7 @@
|
|||
"rgba",
|
||||
"roadmap",
|
||||
"Roboto",
|
||||
"saturationl",
|
||||
"Schilp",
|
||||
"Segoe",
|
||||
"semibold",
|
||||
|
|
|
@ -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)}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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><${tag}></code>`;
|
||||
ul.appendChild(li);
|
||||
|
@ -266,7 +266,11 @@
|
|||
}
|
||||
|
||||
function escapeHtml(html) {
|
||||
return (html + '')
|
||||
if (typeof html === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return html
|
||||
.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
|
Plik diff jest za duży
Load Diff
28
package.json
28
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
Ładowanie…
Reference in New Issue