diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..d2ba9478
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,9 @@
+.cache
+docs/dist
+docs/search.json
+docs/**/*.min.js
+dist
+examples
+node_modules
+src/react
+scripts
\ No newline at end of file
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 00000000..716f5478
--- /dev/null
+++ b/.eslintrc.cjs
@@ -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'
+ }
+};
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 254c87de..cb77e258 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,6 +1,6 @@
{
"recommendations": [
- "ms-vscode.vscode-typescript-tslint-plugin",
+ "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bierner.lit-html",
"bashmish.es6-string-css",
diff --git a/cspell.json b/cspell.json
index 2a48d033..24664ced 100644
--- a/cspell.json
+++ b/cspell.json
@@ -26,6 +26,7 @@
"csspart",
"cssproperty",
"datetime",
+ "describedby",
"Docsify",
"dropdowns",
"easings",
@@ -79,6 +80,7 @@
"rgba",
"roadmap",
"Roboto",
+ "saturationl",
"Schilp",
"Segoe",
"semibold",
diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js
index d4f122bc..fab08e03 100644
--- a/custom-elements-manifest.config.js
+++ b/custom-elements-manifest.config.js
@@ -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)}`;
});
}
+ }
}
}
}
diff --git a/docs/assets/plugins/code-block/code-block.js b/docs/assets/plugins/code-block/code-block.js
index b5f3ab53..b3fb3920 100644
--- a/docs/assets/plugins/code-block/code-block.js
+++ b/docs/assets/plugins/code-block/code-block.js
@@ -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 @@
`;
- [...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 =
`\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(
<${tag}>
`;
ul.appendChild(li);
@@ -266,7 +266,11 @@
}
function escapeHtml(html) {
- return (html + '')
+ if (typeof html === 'undefined') {
+ return '';
+ }
+ return html
+ .toString()
.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 += `